86 Windows

Journey into Linux

← Back to all posts

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:

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:

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-menu

Or applied directly:

apply-theme ufo

Once selected, the script updates:

  1. The active theme symbolic link.

  2. The wallpaper.

  3. The Alacritty theme.

  4. The Dunst notification theme.

  5. The i3 window colors.

  6. The Polybar colors.

  7. 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_NAME

For example:

apply-theme terminal

The 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
done

If 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:

Every theme has to satisfy the same basic contract before it can be activated.

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 = #66F26F32

The main Polybar configuration includes the active theme file:

include-file = /home/redacted/.config/theme-switch/current/polybar.ini

The 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 = i3

The middle section contains system information.

[bar/system]
modules-center = load cpu memory filesystem temp

The final section contains general status information.

[bar/status]
modules-center = wifi pulseaudio battery date theme tray

This creates separate rounded bars for:

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 = true

That 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 reload

It then runs the Polybar launch script:

/home/redacted/.config/polybar/launch.sh

The 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:

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.toml

The 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.sh

It 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_path

The 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/null

The 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:

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"
fi

This 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-reload

The --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-theme

The 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"
done

The Bash-based files can also be checked before applying the theme.

bash -n colors.sh
bash -n dmenu.sh

Once everything is present:

apply-theme example-theme

Because 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-menu

Or directly with:

apply-theme THEME_NAME

New 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.