i3 Theme Switcher
2026-07-22
The problem and the approach to solving it
After installing Linux Mint and i3 on my ThinkPad T480, I started making changes to the appearance of the desktop. Not with the intention of ricing things out too much, but to get more time editing configs, understanding more about how things work together, and all that.
At first, I was only working with one general design.
I had:
A wallpaper
Custom Polybar colors
Custom i3 window colors
Alacritty colors
dmenu colors
Dunst notification colors
That worked fine until I started wanting additional themes.
Changing the wallpaper was easy, but changing the entire appearance of the desktop meant editing several different configuration files and restarting several different applications.
I did not want a theme change to mean manually updating i3, Polybar, Alacritty, dmenu, Dunst, and the wallpaper every time.
I wanted to keep:
i3
Polybar
Alacritty
dmenu
Dunst
feh
I also wanted to avoid some of the clutter people add when “ricing”. I wanted to strethc my legs a bit and make it my own, but always revert to boring and stable.
The challenge was figuring out how to tie all of those together into one system that was easy to use and easy to expand.
The result was a custom theme switcher built around Bash scripts, individual theme directories, and a symbolic link that identifies the currently active theme.
Overview of How It Works
Each theme lives in its own directory under:
/home/redacted/Documents/themes
Each directory contains the configuration files needed for that theme.
The active theme is selected with a symbolic link located at:
/home/redactd/.config/theme-switch/current
For example, with my UFO theme active:
/home/redacted/.config/theme-switch/current
-> /home/redacted/Documents/themes/ufo
i3, Polybar, dmenu, and several custom scripts all reference files
through that current path.
Changing the theme mainly involves pointing the symbolic link at a different directory and then reloading the applications that use those files.
Themes can be selected through dmenu:
theme-menuOr applied directly:
apply-theme ufoOnce selected, the script updates:
The active theme symbolic link.
The wallpaper.
The Alacritty theme.
The Dunst notification theme.
The i3 window colors.
The Polybar colors.
The warning colors used by the system-monitoring scripts.
The Theme Directory Structure
Every theme follows the same basic structure.
colors.sh
polybar.ini
alacritty.toml
dmenu.sh
dunstrc
i3-theme.conf
wallpaper.jpg
For example:
/home/redacted/Documents/themes/ufo/
colors.sh
polybar.ini
alacritty.toml
dmenu.sh
dunstrc
i3-theme.conf
wallpaper.jpg
ufo-image.png
The source wallpaper may have another name, but every theme provides
a standardized wallpaper.jpg path.
In many cases, that is a symbolic link.
wallpaper.jpg -> ufo-image.png
Other examples include:
wallpaper.jpg -> rover-background.jpg
wallpaper.jpg -> terminal-background.png
wallpaper.jpg -> aloha-background.jpg
The apply script does not need to know the original image filename.
It only looks for:
wallpaper.jpg
This allows the source image to retain a descriptive name while every theme still follows the same required structure.
The Technicals
1. Start with a Simple Command
A theme can be applied directly with:
apply-theme THEME_NAMEFor example:
apply-theme terminalThe script first confirms that a theme name was provided.
It then checks that the directory exists.
THEME_ROOT="/home/redacted/Documents/themes"
THEME_DIR="$THEME_ROOT/$theme"If the requested directory does not exist, the script stops.
Theme not found: example-theme
This prevents the symbolic link from being pointed at an invalid location.
2. Validate the Theme Before Applying It
A theme is not accepted simply because a directory exists.
The script checks for every required file:
for required in colors.sh polybar.ini alacritty.toml dmenu.sh dunstrc i3-theme.conf wallpaper.jpg; do
if [ ! -e "$THEME_DIR/$required" ]; then
echo "Theme is missing: $THEME_DIR/$required" >&2
exit 1
fi
doneIf even one required file is missing, the script stops before changing the active theme.
This was important because I did not want a partially completed theme to result in:
A new wallpaper with old bar colors
A new Polybar theme with old i3 colors
A broken dmenu launcher
Missing Alacritty colors
Missing notification settings
Every theme has to satisfy the same basic contract before it can be activated.
3. Use a Symbolic Link for the Active Theme
Once validation is complete, the active theme is changed with:
ln -sfn "$THEME_DIR" "$CURRENT"The CURRENT path is:
/home/redacted/.config/theme-switch/current
Several parts of the desktop reference files through that location.
The main i3 configuration includes:
/home/redacted/.config/theme-switch/current/i3-theme.conf
The main Polybar configuration includes:
/home/redacted/.config/theme-switch/current/polybar.ini
The dmenu scripts source:
/home/redacted/.config/theme-switch/current/dmenu.sh
The system-monitoring scripts source:
/home/redacted/.config/theme-switch/current/colors.sh
Those applications and scripts do not need to know whether the active
theme is ufo, aloha, or
terminal.
They always use the same path.
The files available through that path change when the symbolic link changes.
This ended up being the most important design decision in the entire system.
4. Use colors.sh as the Main Palette
Every theme contains a colors.sh file.
This acts as the main color palette for the theme.
For example, the UFO theme includes:
BG="#10231D"
BG_ALT="#1A332A"
FG="#F4E8C6"
FG_ALT="#D7CBA8"
MUTED="#78947F"
DIM="#355246"
ACCENT="#F26F32"
ACCENT_2="#A9BE9F"
ACCENT_DARK="#8F3A1E"
ACCENT_LIGHT="#FF9A5B"
ERROR="#D9573D"
WARN="#DDAA4B"
SUCCESS="#7EA36C"It also includes colors used by Polybar and the system-monitoring scripts.
BAR_BG="#dd10231D"
BAR_BORDER="#66F26F32"
CAUTION="#DDAA4B"
CRITICAL="#D9573D"
BATTERY_LOW="#DDAA4B"
BATTERY_CRITICAL="#D9573D"The intention is for colors.sh to serve as the source of
truth.
The other configuration files are created from those values.
colors.sh
-> polybar.ini
-> alacritty.toml
-> dmenu.sh
-> dunstrc
-> i3-theme.conf
Polybar, Alacritty, Dunst, and i3 cannot all read Bash variables directly, so their finished configuration files still contain actual color values.
That is fine.
The important part is that those values originate from one palette rather than being chosen separately for every application.
5. Keep the Main Polybar Configuration Separate
The theme directories do not contain the entire Polybar configuration.
They only contain the color section.
For example:
[colors]
background = #dd10231D
background-alt = #1A332A
foreground = #F4E8C6
foreground-alt = #D7CBA8
primary = #F26F32
secondary = #A9BE9F
alert = #D9573D
disabled = #78947F
border = #66F26F32The main Polybar configuration includes the active theme file:
include-file = /home/redacted/.config/theme-switch/current/polybar.iniThe bar layout, modules, fonts, sizing, and positioning remain in the main configuration.
Only the colors change with the theme.
This means a theme does not need to contain a complete copy of the entire Polybar setup.
It only needs to supply the color values required by the existing layout.
6. Use Three Separate Polybar Sections
The Polybar setup consists of three separate floating sections rather than one bar across the full width of the screen.
The first section contains the i3 workspaces.
[bar/workspaces]
modules-left = i3The middle section contains system information.
[bar/system]
modules-center = load cpu memory filesystem tempThe final section contains general status information.
[bar/status]
modules-center = wifi pulseaudio battery date theme trayThis creates separate rounded bars for:
Workspaces
System health
Wi-Fi, volume, battery, date, theme selection, and tray icons
The theme colors are applied to all three bars through the included
polybar.ini file.
7. Reload i3 Before Restarting Polybar
The order of operations became important because the bars float near the bottom of the screen.
The Polybar configurations use:
override-redirect = trueThat allows the bars to float independently, but it also means i3 does not automatically reserve space for them.
The launch script creates a bottom gap:
i3-msg "gaps bottom all set 52"This keeps windows from extending behind the bars.
The problem was that reloading i3 could reset that runtime gap.
The apply script therefore reloads i3 first:
i3-msg reloadIt then runs the Polybar launch script:
/home/redacted/.config/polybar/launch.shThe launch script reapplies the bottom gap and starts the three bars.
polybar workspaces --config=/home/redacted/.config/polybar/config.ini &
polybar system --config=/home/redacted/.config/polybar/config.ini &
polybar status --config=/home/redacted/.config/polybar/config.ini &Polybar has to be launched last.
Otherwise, the i3 reload can remove the gap and allow windows to overlap the bars.
This was one of those small sequencing problems that was not obvious until the rest of the system was already working.
8. Apply i3 Window Colors
The main i3 configuration includes the active theme file:
include /home/redacted/.config/theme-switch/current/i3-theme.conf
The theme file defines colors for:
Focused windows
Focused but inactive windows
Unfocused windows
Urgent windows
For example:
set $bg #10231D
set $bg_alt #1A332A
set $fg #F4E8C6
set $fg_alt #D7CBA8
set $muted #78947F
set $accent #F26F32
set $accent2 #A9BE9F
set $urgent #D9573D
Those variables are then used in the normal i3 client color definitions.
client.focused $accent $bg_alt $fg $accent2 $accent
client.focused_inactive $muted $bg $fg_alt $muted $muted
client.unfocused $bg $bg $fg_alt $bg $bg
client.urgent $urgent $urgent $fg $urgent $urgent
Reloading i3 causes it to reread the included file and apply the new window colors.
9. The Alacritty Problem
Alacritty caused an issue that required a slightly different approach.
Originally, Alacritty imported its theme directly through the moving
current symbolic link.
That seemed like the cleanest option.
The problem was that after changing themes, existing and newly opened Alacritty windows could disagree about which theme was active.
The solution was to give Alacritty a stable theme file:
/home/redacted/.config/alacritty/theme.toml
The main Alacritty configuration imports that file:
import = ["/home/redacted/.config/alacritty/theme.toml"]When a new theme is applied, the switcher copies the selected theme into that stable location.
cp "$CURRENT/alacritty.toml" /home/redacted/.config/alacritty/theme.tomlThe theme directory is still the source of the configuration.
Alacritty just receives a copied version at a path that never changes.
This fixed the inconsistency between old and new terminal windows.
10. Apply Dunst Notification Colors
Dunst is handled in a similar way.
Each theme contains a dunstrc file that defines the
colors for different notification urgency levels.
For example:
[urgency_low]
background = "#10231D"
foreground = "#D7CBA8"
frame_color = "#355246"
[urgency_normal]
background = "#1A332A"
foreground = "#F4E8C6"
frame_color = "#F26F32"
[urgency_critical]
background = "#8F3A1E"
foreground = "#F4E8C6"
frame_color = "#D9573D"The selected configuration is copied to:
/home/redacted/.config/dunst/dunstrc
If Dunst is already running, the script attempts to reload it.
If that fails, Dunst is stopped and started again.
dunstctl reload || {
pkill -x dunst || true
dunst &
}If Dunst is not running, it is started.
This lets notification colors change along with the rest of the desktop without requiring a logout.
11. Theme dmenu Too
The application launcher and theme selector both use dmenu.
I wanted dmenu to follow the active theme instead of retaining one fixed appearance.
Every theme includes a dmenu.sh file.
That file sources the active colors.sh:
source /home/redacted/.config/theme-switch/current/colors.shIt then builds the dmenu arguments.
DMENU_ARGS=(
-fn "DejaVu Sans Mono-11"
-nb "$BG"
-nf "$FG"
-sb "$ACCENT"
-sf "$BG"
)The application launcher and theme menu both load those arguments before opening dmenu.
This means the tool used to select a new theme still matches the currently active theme.
12. Do Not Use Polybar Alpha Colors in dmenu
One problem I ran into involved the difference between Polybar and dmenu color formats.
Polybar can use colors with transparency.
For example:
BAR_BG="#dd10231D"The first two characters after the # control the alpha
value.
dmenu expects normal six-digit colors.
Passing an eight-digit Polybar color into dmenu can prevent dmenu from opening.
The correct dmenu configuration uses:
-nb "$BG"
-nf "$FG"
-sb "$ACCENT"
-sf "$BG"It does not use:
-nb "$BAR_BG"The normal background and accent colors are six digits and work correctly.
This was a good example of why a shared palette still needs application-specific configuration files.
The same general color may need to be represented differently depending on the application using it.
13. Use dmenu_path Instead of Scanning Everything
An earlier version of the application launcher manually scanned the
directories in $PATH for available commands.
It worked, but it caused around ten seconds of delay before dmenu appeared.
That was obviously too slow for an application launcher.
The current version uses:
dmenu_pathThe command is:
choice="$(dmenu_path | dmenu -i -p "run:" "${DMENU_ARGS[@]}")"dmenu_path maintains a cache of available commands.
This made the launcher considerably faster.
If the cache ever needs to be rebuilt, it can be removed manually.
rm ~/.cache/dmenu_run
dmenu_path >/dev/nullThe theme switcher itself does not need this cache because it is only scanning a small number of theme directories.
14. Discover Themes Automatically
The theme menu does not contain a hard-coded list of themes.
It scans for directories under:
/home/redacted/Documents/themes
The relevant part is:
find "$THEME_ROOT" -mindepth 1 -maxdepth 1 -type d -printf '%f\n'The results are sorted and passed to dmenu.
choice="$(
find "$THEME_ROOT" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \
| sort \
| dmenu -i -p "theme:" "${DMENU_ARGS[@]}"
)"This means a new theme becomes available automatically once its directory is created.
There is no list that has to be updated separately.
There is also no database or additional configuration file containing theme names.
The theme directories themselves are the inventory.
15. Make the System-Monitoring Colors Theme-Aware
The center Polybar section displays:
System load
CPU usage
RAM usage
Root filesystem usage
CPU temperature
The status section also displays battery information.
These values are generated by custom Bash scripts.
The scripts change the text color when a value reaches a caution or critical threshold.
For example, CPU usage uses:
Normal: below 70%
Caution: 70–89%
Critical: 90% or higher
RAM uses:
Normal: below 75%
Caution: 75–89%
Critical: 90% or higher
Disk usage uses:
Normal: below 80%
Caution: 80–89%
Critical: 90% or higher
Temperature uses:
Normal: below 70°C
Caution: 70–84°C
Critical: 85°C or higher
The monitoring scripts include default colors in case the active theme cannot be read.
CAUTION="#ebcb8b"
CRITICAL="#bf616a"They then source the active theme:
CURRENT="/home/redacted/.config/theme-switch/current"
if [ -f "$CURRENT/colors.sh" ]; then
source "$CURRENT/colors.sh"
fiThis allows each theme to define its own caution and critical colors.
The thresholds do not change.
A CPU temperature of 85°C is critical regardless of which theme is active.
Only the exact shade used to represent that condition changes.
I wanted the themes to control the appearance of the warning without changing the meaning of the warning.
16. Reapply the Theme at Startup
Changing the symbolic link preserves which theme was selected.
However, the wallpaper, copied configuration files, Dunst, and Polybar still need to be applied when i3 starts.
The i3 configuration contains:
exec_always --no-startup-id /home/redacted/.local/bin/apply-theme-current
The apply-theme-current script checks the current
symbolic link.
theme="$(basename "$(readlink -f "$CURRENT")")"It then applies that theme with:
apply-theme "$theme" --quiet --no-i3-reloadThe --quiet option prevents the normal “Theme changed”
notification from appearing during startup.
The --no-i3-reload option prevents the script from
reloading i3 while i3 is already loading its configuration.
If the current symbolic link does not exist, the script falls back to:
linux-theme
This allows the selected theme to survive logout, reboot, or an i3 restart.
Creating a New Theme
Creating a new theme does not require changing the main switcher.
A new theme directory is created:
mkdir -p /home/redacted/Documents/themes/example-themeThe required files are added:
colors.sh
polybar.ini
alacritty.toml
dmenu.sh
dunstrc
i3-theme.conf
wallpaper.jpg
The new directory can be validated with:
cd /home/redacted/Documents/themes/example-theme
for f in colors.sh polybar.ini alacritty.toml dmenu.sh dunstrc i3-theme.conf wallpaper.jpg; do
[ -e "$f" ] && echo "OK $f" || echo "MISS $f"
doneThe Bash-based files can also be checked before applying the theme.
bash -n colors.sh
bash -n dmenu.shOnce everything is present:
apply-theme example-themeBecause the menu scans the theme directory automatically,
example-theme will also appear the next time
theme-menu is opened.
Lessons Learned
The biggest lesson from this project was that changing a wallpaper and a few colors is the easy part.
The difficult part is coordinating several unrelated applications that all handle configuration and reloading differently.
i3 and Polybar work well when including files through the active-theme symbolic link.
Alacritty worked better with a stable imported file that gets replaced when the theme changes.
Dunst needed its configuration copied and then reloaded or restarted.
dmenu needed normal six-digit colors rather than Polybar colors containing alpha values.
Polybar needed to be restarted after i3 so the runtime bottom gap would remain in place.
The dmenu application launcher needed to use the cached
dmenu_path instead of scanning every available executable
whenever it opened.
None of those issues were particularly difficult on their own.
The main challenge was handling all of them through one predictable process.
Another important decision was keeping the layout separate from the theme.
A theme controls the appearance of Polybar, but it does not redefine the bars, modules, fonts, or positioning.
A theme can also control the colors used by system warnings, but it does not change the warning thresholds.
This keeps the theme files focused on appearance without allowing them to change how the desktop operates.
Conclusion
The final result is a theme switcher built entirely from tools that were already part of my i3 setup.
Bash handles the validation and application process.
A symbolic link identifies the active theme.
i3 and Polybar include files from that active theme.
Alacritty and Dunst receive copied configuration files at stable locations.
feh changes the wallpaper.
dmenu provides the selection menu.
The custom system-monitoring scripts use warning colors from the active theme.
The entire desktop can be changed with:
theme-menuOr directly with:
apply-theme THEME_NAMENew themes can be added without modifying the switcher itself.
As long as a theme follows the required directory structure, it is automatically available and can be applied through the same process.
What started as a way to avoid manually editing several configuration files turned into a reusable theme system for the entire i3 desktop.