Managing Themes on Linux
I enjoy maximizing sunlight in every room I spend time in. However, when working
during the day, having my computer set to dark mode makes it much harder to see
the screen clearly. For this reason, I wanted an easy way to switch between
light and dark modes across my entire system.
On many operating systems, there’s a built-in way to toggle themes. Even on
Linux, if you’re using a desktop environment like GNOME (common on distributions
like Ubuntu), switching themes is straightforward and integrated into the
settings.
But my setup is different. I use ArcoLinux with a window manager called i3
.
Unlike full desktop environments, i3
doesn't come with a default way to manage
themes. It prioritizes giving users full control, which means configuring themes
is something you need to figure out for yourself.
In this blog, I’ll walk you through the approach I've taken to manage light and
dark themes on my system. If you’re using a similar setup, this guide will give
you a starting point to achieve the same.
Overview
There are a lot of things that require to be managed - things like NeoVim
(Editor), Brave (Browser), Alacritty (Terminal), VSCode (Editor), Tmux
(Multiplexer), etc.
Unfortunately, we're going to have to configure ALL of these individually.
Some of them default to using the "system theme" but that doesn't always work.
For applications that use a configuration file like Alacritty, Rofi, Tmux, etc,
we can just swap out a configuration file and control the colors using this
method. We're going to use symlinks to achieve this in a clean way.
Some applications need to be configure through command line operations and some
need to be configured through the GUI (like VSCode).
Preview
Light Mode


Wallpaper from [1].
Dark Mode


Wallpaper from [2].
Primary Script
Overall Structure
Here's the structure of the main script that's going to do all the heavy
lifting:
#!/usr/bin/env bash
THEME_FILE="$HOME/.config/suchi-configs/theme.txt"
GTK4_LIGHT="Arc-Lighter"
GTK4_DARK="Arc-Darkest"
light="light"
dark="dark"
function get_theme() {
cat "$THEME_FILE"
}
function set_theme_file() {
theme="$1"
if [[ "$theme" == "$light" ]]; then
echo "$light" > "$THEME_FILE"
elif [[ "$theme" == "$dark" ]]; then
echo "$dark" > "$THEME_FILE"
else
echo "Unable to set invalid theme $1."
exit 1
fi
}
# we'll flush these functions out soon
# function simple_set_file() { ... }
# function set_alacritty() { ... }
# function set_yazi() { ... }
# function set_tmux() { ... }
# function set_polybar() { ... }
# function set_dunst() { ... }
# function set_rofi() { ... }
# function set_xsettings() { ... }
# function set_gtk3() { ... }
# function set_gtk4() { ... }
function update_theme() {
theme="$1"
set_theme_file "$theme"
set_alacritty "$theme"
set_yazi "$theme"
set_tmux "$theme"
set_polybar "$theme"
set_dunst "$theme"
set_rofi "$theme"
set_gtk3 "$theme"
set_gtk4 "$theme"
set_xsettings "$theme"
$HOME/.local/scripts/setWallpaper.sh
}
case "$1" in
"$light")
theme="$light"
;;
"$dark")
theme="$dark"
;;
"toggle")
old_theme=$(get_theme)
if [[ "$old_theme" == "$light" ]]; then
theme="$dark"
elif [[ "$old_theme" == "$dark" ]]; then
theme="$light"
else
echo "Unable to set invalid theme $1."
exit 1
fi
;;
*)
echo "Invalid command."
exit 1
;;
esac
update_theme "$theme"
i3-msg reload
echo "$theme theme has been set."
Now these are all the applications I use right now, however, what's important is
the overall structure and understanding how to handle your own applications.
That said, I'm going to include the detail for each application by the end of
this.
Simple Set File
This is going to be the primary function I use in the script to change themes
for stuff that uses configuration files:
function simple_set_file() {
# $1 -> theme choice
# $2 -> main file / theme file (the symbolic link)
# $3 -> light file
# $4 -> dark file
theme="$1"
main_file="$2"
light_file="$3"
dark_file="$4"
if [ ! -f "$light_file" ] || [ ! -f "$dark_file" ]; then
echo "missing either light or dark file"
exit 1
fi
if [[ "$theme" == "$light" ]]; then
ln -sf "$light_file" "$main_file"
elif [[ "$theme" == "$dark" ]]; then
ln -sf "$dark_file" "$main_file"
else
echo "Unable to set invalid theme $1 for simple set file."
exit 1
fi
}
This works by changing the $main_file
, which is the applications config file,
from being linked to either the $dark_file
or the $light_file
. The ln -sf
creates a "symbolic link" and the -f
forces so that it replaces the existing
file.
Here's the example for my Alacritty config:
❯ pwd
~/.config/alacritty
❯ ls -la
.rw------- [redacted] alacritty.toml
.rw-r--r-- [redacted] dark.toml
.rw-r--r-- [redacted] light.toml
lrwxrwxrwx [redacted] theme.toml -> dark.toml
As you can see, right now I'm using the dark mode and therefore the theme.toml
links to the dark.toml
file. If we call the function with "light" as our theme
then theme.toml
will now point to light.toml
.
This works well for most configurations, especially if the application
auto-refreshes if the configuration is changed.
Sometimes a file doesn't realize that it was updated and therefore you can use
something like:
touch --no-dereference "$config_base"
This updates the file without actually updating the file, which can lead to the
application configuration being refreshed.
GTK Theme
One of the main things we'll have to change is the GTK theme as this controls a
lot of the GUI based applications. I learnt about xsettingsd
from the Reddit
user from [5] which is used to update the theme dynamically.
First you install xsettingsd
using your package manager and then create a
configuration file like:
#~/.config/xsettingsd/xsettingsd.conf
Net/ThemeName "Arc-Darkest"
Net/CursorBlink 1
Net/IconThemeName "Sardi-Arc"
Gtk/CursorThemeName "Bibata-Modern-Ice"
Gtk/ApplicationPreferDarkTheme 1
But we're going to use the simple_set_file
function for this too. Therefore,
we maintain a light.conf
and a dark.conf
which are then used to create the
file xsettingsd.conf
as a symlink.
Then, we need to start the process like any other: xsettings &
. You can add
this to the list of startup applications based on however you control that. I've
added it to my i3
config file.
Then, after a theme change, we need to use the following command to "reload" the
config for xsettingd
:
You can read more details about xsettingsd
from [7].
This was a life saver because this allows you to change the theme without having
to restart all the applications. All the other methods I found require you to
restart the applications.
function set_xsettings() {
theme="$1"
dir="$HOME/.config/xsettingsd/"
main_file="$dir/xsettingsd.conf"
light_file="$dir/light.conf"
dark_file="$dir/dark.conf"
simple_set_file "$theme" "$main_file" "$light_file" "$dark_file"
killall -HUP xsettingsd
}
Other Considerations
I used to use these before I learnt about xsettingsd
and I'm not sure if it
still is necessary. I'm including it here for completeness sake:
function set_gtk3() {
theme="$1"
gtk3_dir="$HOME/.config/gtk-3.0"
main_file="$gtk3_dir/settings.ini"
light_file="$gtk3_dir/light.ini"
dark_file="$gtk3_dir/dark.ini"
simple_set_file "$theme" "$main_file" "$light_file" "$dark_file"
}
function set_gtk4() {
theme="$1"
if [[ "$theme" == "$light" ]]; then
gsettings set org.gnome.desktop.interface gtk-theme "$GTK4_LIGHT"
else
gsettings set org.gnome.desktop.interface gtk-theme "$GTK4_DARK"
fi
}
Applications
Rather than including the details manually here, I've just pasted all the
contents of the script into a Gist:
https://gist.github.com/SuchithSridhar/4f8128d56efdc718df4b652a7024ba67.
Most of these use the simple_set_file
function to switch out the configuration
file. Some require a little more handling but they all depend on the
application's behaviour.
Neovim
This is one of the few applications that didn't have a clean way to dynamically
switch themes by just changing the configuration file. That's probably because
it's loaded the config file into memory and reloading a lua config could cause
problems.
That's why I have a little "plugin" that checks that
~/.config/suchi-configs/theme.txt
file and whenever that file changes, neovim
reloads the theme.
Github theme-loader.nvim
is the plugin created and the README has instructions on how to setup and use
the plugin.
GUI Based Theme Control
I'm not going to elaborate on this because it really depends on the individual
applications but usually they have an option to "follow system theme". Example:

Another useful and important tool is the "Dark Reader" extension for brave and
other browsers. This styles pages to be dark mode if they don't already have a
dark mode [8].

Similarly with VSCode:

Set Preference for System
There's a couple of settings you can set to indicate to applications that you
prefer a dark theme. Here are a few settings:
# just as a command for gtk-4
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
# xsettingsd config to manage gtk (.config/xsettingsd/settings.conf)
Gtk/ApplicationPreferDarkTheme 1
# gtk3 file config (.config/gtk-3.0/settings.ini)
gtk-application-prefer-dark-theme=1
This makes applications list to the system theme preference.
Auto Control of Theme
I have my theme toggler bound to a hotkey so that I can change it easily.
However, I've since discovered better methods to auto change the theme.
Fixed Time Based Change
Initially, I just had a time based way to decide the theme on startup. It only
ran once after startup at which point it decided the theme based on the current
time.
DARK_MODE_TIME_START="18"
DARK_MODE_TIME_END="6"
function set_theme_based_on_time() {
current_hour=$(date +"%H")
if [ "$current_hour" -ge "$DARK_MODE_TIME_START" ] || [ "$current_hour" -lt "$DARK_MODE_TIME_END" ]; then
$HOME/.local/scripts/set-theme.sh dark
else
$HOME/.local/scripts/set-theme.sh light
fi
}
However, now I only default back to this if I don't have an active internet connection.
Auto Change Based On Sunrise And Sunset
With the help of ChatGPT, I wrote a simple script using the "Sunrise-Sunset" API
[6], which decides the theme based on the sunrise and sunset times for that day.
This script runs every 15-mins so that it can automatically change the theme
whenever it gets dark.
#!/usr/bin/env python3
import os
import subprocess
from datetime import datetime, timezone
import requests
# Expected to be added as a cronjob:
# */15 * * * * /..../theme-auto-swticher.py
# City (Latitude, Longitude)
LAT = 9999
LON = 9999
CONFIG_PATH = os.path.expanduser("~/.config/suchi-configs/theme.txt")
CACHE_PATH = os.path.expanduser("~/.cache/suchi-cache/sunrise-sunset.txt")
SET_THEME_SCRIPT = os.path.expanduser("~/.local/scripts/set-theme.sh")
API_URL = "https://api.sunrise-sunset.org/json"
def get_current_theme():
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, "r") as file:
return file.read().strip()
return None
def fetch_and_cache_sun_times():
params = {"lat": LAT, "lng": LON, "formatted": 0}
print("Accessing Sunrise-Sunset API")
response = requests.get(API_URL, params=params)
response.raise_for_status()
data = response.json()
sunrise_utc = datetime.fromisoformat(data["results"]["sunrise"]).replace(
tzinfo=timezone.utc
)
sunset_utc = datetime.fromisoformat(data["results"]["sunset"]).replace(
tzinfo=timezone.utc
)
# Convert to local time
sunrise_local = sunrise_utc.astimezone().replace(tzinfo=None)
sunset_local = sunset_utc.astimezone().replace(tzinfo=None)
# Write cache with labeled fields and header
os.makedirs(os.path.dirname(CACHE_PATH), exist_ok=True)
with open(CACHE_PATH, "w") as cache_file:
cache_file.write(
"# Cached sunrise and sunset times generated "
"by theme-auto-switcher.py\n"
f"Date: {datetime.now().date()}\n"
f"Sunrise: {sunrise_local.isoformat()}\n"
f"Sunset: {sunset_local.isoformat()}\n"
)
return sunrise_local, sunset_local
def read_cached_sun_times():
if not os.path.exists(CACHE_PATH):
return None, None
with open(CACHE_PATH, "r") as cache_file:
lines = cache_file.readlines()
if len(lines) < 4:
return None, None
# Skip the header and parse labeled fields
cache_date = lines[1].split("Date:")[1].strip()
sunrise = datetime.fromisoformat(lines[2].split("Sunrise:")[1].strip())
sunset = datetime.fromisoformat(lines[3].split("Sunset:")[1].strip())
if cache_date == str(datetime.now().date()):
return sunrise, sunset
return None, None
def get_sun_times():
sunrise, sunset = read_cached_sun_times()
if sunrise and sunset:
return sunrise, sunset
return fetch_and_cache_sun_times()
def is_daytime():
sunrise, sunset = get_sun_times()
now = datetime.now()
return sunrise <= now <= sunset
def main():
expected_theme = "light" if is_daytime() else "dark"
current_theme = get_current_theme()
if current_theme != expected_theme:
subprocess.run([SET_THEME_SCRIPT, expected_theme])
print(f"Theme changed to {expected_theme}.")
else:
print(f"Theme is already {current_theme}. No change needed.")
if __name__ == "__main__":
main()
Auto Change After Wake
When you schedule the theme-auto-switcher.py
using cron, it only runs at every
15th minute of the hour. However, I want to see the right theme when I first
open my laptop after I've put it to sleep for a while. It doesn't make sense to
wait up to 15 mins for the theme to change.
Therefore, I've added a little script/service that runs after waking up from
sleep that checks if the theme needs to be changed. I used Daniel Garajau's
method from [4] to write the systemd
service.
We need to run the change theme script as a regular user and not the root user
since it needs access to all the user level controls.
I recommend reading the blog [4] but here's the files I've adapted:
Usually, systemd
doesn't call user level suspend handling services so we
install this so that user level services can run on suspend.target
.
There's however, a clean way to proxy out these hooks from the system to the
user instance. To do that, you will need to create a system unit that will
trigger a user target (directly taken from [4]):
; /etc/systemd/system/suspend@.service
; Enable it by running sudo systemctl daemon-reload && sudo systemctl enable suspend@$(whoami)
[Unit]
Description=Call user's suspend target after system suspend
After=suspend.target
[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl --user --machine=%i@ start --wait suspend.target
[Install]
WantedBy=suspend.target
Here's the command to enable it:
sudo systemctl daemon-reload && sudo systemctl enable suspend@$(whoami)
We need a service that handles the suspend action:
; ~/.config/systemd/user/suspend.target
[Unit]
Description=User-level suspend target
StopWhenUnneeded=yes
Then, we need to actually write the service that runs the script:
; ~/.config/systemd/user/wakeup-script.service
[Unit]
Description=Run script after waking from sleep
After=suspend.target
[Service]
Type=oneshot
ExecStart=/home/user/.local/scripts/wakeup-script.sh
[Install]
WantedBy=suspend.target
After this, we want to enable the system service for the user:
systemctl --user daemon-reload
systemctl --user enable wakeup-script.service
The wakeup-script.sh
is just a simple bash script that exists. I didn't
directly call the theme script because I wanted the option to do other things as
well:
# wakeup-script.sh
#!/usr/bin/env bash
# Run after waking up from sleep.
# Maximum wait time for network connection (in seconds)
MAX_WAIT=240 # 4 minutes
CHECK_INTERVAL=1 # Check every second
# Function to check internet connectivity
check_internet() {
ping -q -c 1 -W 1 8.8.8.8 > /dev/null 2>&1
return $?
}
# Wait for internet connection
SECONDS_WAITED=0
while ! check_internet; do
sleep $CHECK_INTERVAL
SECONDS_WAITED=$((SECONDS_WAITED + CHECK_INTERVAL))
# Exit if maximum wait time is exceeded
if [[ $SECONDS_WAITED -ge $MAX_WAIT ]]; then
echo "Internet connection not restored after $MAX_WAIT seconds. Exiting."
exit 1
fi
done
# Internet is connected; run the theme switcher script
echo "Internet connected after $SECONDS_WAITED seconds. Running theme auto-switcher."
/home/user/.local/scripts/theme-auto-swticher.py
References
I'm going to include as many references as I can but this has been a long term
project and I might not have all the original sources for everything.
-
[1] Basic Apple Guy, "OS X Rancho Cucamonga",
[Online]. Available: https://basicappleguy.com/basicappleblog/os-x-rancho-cucamonga.
[Accessed: Jan 24, 2025].
-
[2] Basic Apple Guy, "Rancho At Night",
[Online]. Available: https://basicappleguy.com/basicappleblog/rancho-at-night.
[Accessed: Jan 24, 2025].
-
[3] H. Hauswedell, "Commandline dark-mode switching for Qt, GTK and websites",
[Online]. Available: https://hannes.hauswedell.net/post/2023/12/10/darkmode/.
[Accessed: Jan 24, 2025].
-
[4] D. Garajau, "Using systemd user units to react to sleep/suspend",
[Online]. Available: https://garajau.com.br/2022/08/systemd-suspend-user-level.
[Accessed: Jan 24, 2025].
-
[5] u/Glow_berry, "How to change the GTK without killing all instances and restarting?", Reddit,
[Online]. Available: https://www.reddit.com/r/bspwm/comments/qm4p9r/comment/hj915qm/.
[Accessed: Jan 24, 2025].
-
[6] "Sunrise-Sunset",
[Online]. Available: https://sunrise-sunset.org/api.
[Accessed: Jan 24, 2025].
-
[7] Derat, xsettingsd
,
[Online]. Available: https://codeberg.org/derat/xsettingsd.
[Accessed: Jan 24, 2025].
-
[8] Dark Reader,
[Online]. Available: https://darkreader.org/.
[Accessed: Jan 24, 2025].