Dark/Light-Mode Switcher for Arch Linux + i3wm
Table of Contents ๐
- Changelog
- Architectural Overview
- Quickstart (Copy & Paste)
- Recommended Approach: Set up Step-by-Step
- Theme Choice: Dracula โ Solarized Light
- Migrating from a hand-themed setup
- Step 1: Install Prerequisites
- Step 2: Create Directory Structure
- Step 3: Prepare Qt Apps (KeePassXC, nheko)
- Step 4: Split i3 Config
- Step 5: Integrate Polybar
- Step 6: Chromium Flags
- Step 7: The Dark-Mode Hook Script
- Step 8: The Light-Mode Hook Script
- Step 9: Initial State and Persistence on Login
- Step 10: Obtain Theme Files
- Caveats and Honest Expectations
- Tips
- Extension Ideas
- Summary
Welcome to this detailed guide showing you how to set up an effective Dark/Light-Mode switcher for your Arch Linux system with i3wm. In a world where most applications have their own theming logic, we will build an orchestration solution that controls many components simultaneously to create a consistent experience.
We will integrate GTK, Qt, terminal, and even CLI applications into our theme switcher, all controlled by a single keypress.
Changelog
| Date | Change |
|---|---|
| 2026-05-04 | Initial Version: Comprehensive guide for Dark/Light-Mode switching on Arch Linux and i3wm. |
Architectural Overview
i3wm itself does not provide global theme management. Styling is handled by various components such as GTK, Qt, Xresources, and individual applications. Our solution is based on an orchestrator architecture that uses darkman as the central control unit.
darkman performs two main tasks:
- It executes your customized hook scripts, which adapt specific applications and environments.
- It propagates the dark mode status via the XDG-Desktop-Portal to modern applications (e.g., Firefox, Thunderbird), which then switch their theme without needing a restart.
$mod+Shift+d โ darkman toggle โ Hook Scripts โ Individual Apps
โ
โโโโบ XDG-Portal (Firefox, Thunderbird, etc. via Signal)Quickstart (Copy & Paste)
If you just want a minimal, working setup โ GTK + Firefox + i3 + Alacritty controlled by $mod+Shift+d โ follow this Quickstart. You can extend with Polybar, VSCodium, neomutt, and Qt apps later by following the full guide below.
# 1. Install prerequisites
sudo pacman -S darkman xdg-desktop-portal xdg-desktop-portal-gtk \
jq libnotify alacritty
yay -S dracula-gtk-theme # or fall back to Adwaita-dark (handled by hook)
# 2. Create directory layout
mkdir -p ~/.local/share/dark-mode.d \
~/.local/share/light-mode.d \
~/.config/themes/{dracula,solarized-light} \
~/.config/alacritty \
~/.config/xdg-desktop-portal
# 3. Tell xdg-desktop-portal to use darkman as the Settings backend.
# REQUIRED since xdg-desktop-portal 1.17.0 โ without this, Firefox &
# other portal-aware apps will not see darkman's color-scheme.
cat > ~/.config/xdg-desktop-portal/portals.conf <<'EOF'
[preferred]
org.freedesktop.impl.portal.Settings=darkman
EOF
# 4. Pull terminal theme files
curl -fsSL https://raw.githubusercontent.com/dracula/alacritty/master/dracula.toml \
-o ~/.config/themes/dracula/alacritty.toml
curl -fsSL https://raw.githubusercontent.com/alacritty/alacritty-theme/master/themes/solarized_light.toml \
-o ~/.config/themes/solarized-light/alacritty.toml
# 5. Wire up i3 (idempotent โ safe to run twice)
grep -q "bindsym \$mod+Shift+d exec --no-startup-id darkman toggle" ~/.config/i3/config \
|| echo 'bindsym $mod+Shift+d exec --no-startup-id darkman toggle' >> ~/.config/i3/config
grep -q "include ~/.config/i3/theme.conf" ~/.config/i3/config \
|| echo 'include ~/.config/i3/theme.conf' >> ~/.config/i3/config
# 6. Enable the service
systemctl --user enable --now darkman.service
# 7. Quick smoke test
# Note: these only have a *visible* effect after you create the
# hook scripts in Steps 7 & 8. Until then darkman just flips an
# internal flag โ nothing recolors yet.
darkman get # current mode
darkman toggle # switch without using the keybindingThen create the i3 theme stubs and the hook scripts as described in Step 7 and Step 8. After that, $mod+Shift+d should already give you a working basic switch.
Expected result after the Quickstart + minimal hooks: Pressing
$mod+Shift+d(or runningdarkman toggle) should immediately recolor i3 borders, switch the Alacritty theme without restarting the terminal, and โ if Firefox is open withwidget.use-xdg-desktop-portal.settings = 1set inabout:configโ flip Firefoxโs UI between light and dark. If any of these donโt react, see the Reality Check section.
Recommended Approach: Set up Step-by-Step
To avoid frustration and facilitate troubleshooting, you should approach the setup in phases. This way, youโll always know which component is responsible for problems.
- Phase 1 โ Basic:
darkman+ GTK + Firefox/Thunderbird + i3 + Alacritty. These components are reliable and cover the majority of the visual interface. - Phase 2 โ Extension: Polybar + VSCodium + neomutt. This requires a bit more configuration, but the functionality is stable.
- Phase 3 โ Problem Children: Qt apps (KeePassXC, nheko), Chromium, urxvt live-reload. These applications can present app-specific challenges and should be tested separately.
Follow this tutorial linearly and test the functionality after each phase before moving on to the next.
Theme Choice: Dracula โ Solarized Light
For dark mode, we will use Dracula, a popular and well-supported theme. For the light mode counterpart, several options are available:
- Solarized Light: A classic with well-documented color palettes for many tools, known for its good readability.
- Catppuccin Latte: A modern alternative that aesthetically complements Catppuccin Mocha (if you decide to replace Dracula later).
- GitHub Light: A pragmatic choice, as many editor themes and browser extensions are available for it.
This guide will use the Dracula โ Solarized Light pair.
Migrating from a hand-themed setup
If youโve already painstakingly themed each application by hand, read this section before running any of the hook scripts. The hooks are designed for a setup built with darkman in mind โ applied to an existing setup, they will overwrite or symlink-replace several files without asking.
What the hooks will overwrite (destructive)
These files are rewritten in full on every toggle:
~/.config/gtk-3.0/settings.iniand~/.config/gtk-4.0/settings.iniโ replaced viacat > ... <<EOF. Any custom keys you have here (cursor theme, font name, dconf-style overrides) will be lost on first toggle.~/.config/qt5ct/qt5ct.conf,~/.config/qt6ct/qt6ct.confโ only thecolor_scheme_pathline is rewritten viased. The rest of the file is preserved.~/.config/VSCodium/User/settings.jsonโ only theworkbench.colorThemekey is updated viajq. Caveat: if your settings.json contains JSONC-style comments,jqwill strip them.
What the hooks will replace via symlink (non-destructive but pathing changes)
These paths become symlinks pointing into ~/.config/themes/<theme>/. If a real file exists at the path, the symlink replaces it (the original file stays where it was, but is no longer reachable through the original path):
~/.Xresources~/.config/i3/theme.conf~/.config/alacritty/theme.toml~/.config/polybar/config.ini~/.config/neomutt/colors~/.config/nvim/colorscheme.vim
What is changed system-wide via gsettings
org.gnome.desktop.interface gtk-theme and color-scheme โ affects every GTK app on your system that reads gsettings, regardless of whether you intended to theme it through darkman.
What is NOT touched
Your main configuration files (~/.config/i3/config, ~/.config/alacritty/alacritty.toml, your Polybar bar layout, your .muttrc, your Neovim init, your shell rc files, Firefox/Chromium profiles) are only referenced via include/import/source lines. They stay where they are.
Recommended: snapshot before the first toggle
mkdir -p ~/theme-switcher-backup
for path in \
~/.config/gtk-3.0 \
~/.config/gtk-4.0 \
~/.config/qt5ct \
~/.config/qt6ct \
~/.Xresources \
~/.config/VSCodium/User/settings.json
do
[[ -e "$path" ]] && cp -a "$path" ~/theme-switcher-backup/
doneAfter the first toggle, diff what changed:
diff -r ~/theme-switcher-backup/gtk-3.0 ~/.config/gtk-3.0 || trueIf you find that lines you cared about were lost, the right fix is not to protect your old config but to teach the hooks about your custom keys โ open the hook scripts in Step 7 and Step 8 and add your custom lines into the cat > settings.ini <<EOF blocks. Same logic applies to qt5ct, VSCodium, etc.: extend the hook, donโt fight it.
Step 1: Install Prerequisites
darkman and Portal Infrastructure
sudo pacman -S darkman xdg-desktop-portal xdg-desktop-portal-gtk jq libnotify
systemctl --user enable --now darkman.servicejqis needed for patching VSCodium settings.libnotifyenables notifications vianotify-send.
Configure xdg-desktop-portal to use darkman (REQUIRED)
This is the single most overlooked step in dark-mode setups. Since xdg-desktop-portal 1.17.0, the portal must be told which backend implements the Settings interface โ otherwise Firefox, Thunderbird, and other portal-aware apps will never see darkmanโs color-scheme value, no matter how correctly darkman itself is configured.
Create ~/.config/xdg-desktop-portal/portals.conf:
mkdir -p ~/.config/xdg-desktop-portal
cat > ~/.config/xdg-desktop-portal/portals.conf <<'EOF'
[preferred]
org.freedesktop.impl.portal.Settings=darkman
EOFYou can verify the portal really hands darkmanโs value to clients with:
gdbus call --session \
--dest org.freedesktop.portal.Desktop \
--object-path /org/freedesktop/portal/desktop \
--method org.freedesktop.portal.Settings.ReadOne \
org.freedesktop.appearance color-schemeThis should return 1 (dark) or 2 (light) depending on darkmanโs current mode. If it returns 0 (โno preferenceโ) or fails, your portals.conf isnโt being read โ restart xdg-desktop-portal.service and check XDG_CURRENT_DESKTOP is set in your session environment.
Firefox-side counterpart: In
about:config, also setwidget.use-xdg-desktop-portal.settings = 1. Without both halves โ system-sideportals.confAND Firefox-side preference โ the bridge stays broken.
GTK Theme
Dracula is not in the official Arch repository. You will need an AUR helper like yay or paru:
yay -S dracula-gtk-theme
# or: paru -S dracula-gtk-themeIf you prefer not to use an AUR helper, the setup will automatically fall back to Adwaita-dark and Adwaita respectively (see hook script).
Qt Control (for Phase 3)
sudo pacman -S qt5ct qt6ct kvantumi3 Keybinding
Add the following keybinding to your i3 configuration file ~/.config/i3/config:
bindsym $mod+Shift+d exec --no-startup-id darkman toggleStep 2: Create Directory Structure
A clean directory structure is crucial for maintainability:
mkdir -p ~/.local/share/dark-mode.d
mkdir -p ~/.local/share/light-mode.d
mkdir -p ~/.config/themes/{dracula,solarized-light}
mkdir -p ~/.config/qt5ct/colors ~/.config/qt6ct/colorsIn the ~/.config/themes/ directory, theme-specific files for each application will be stored. The hook scripts will then create symlinks to the currently desired theme files.
Step 3: Prepare Qt Apps (KeePassXC, nheko)
Qt applications do not automatically follow GTK configuration or the XDG-Portal. You must set the QT_QPA_PLATFORMTHEME environment variable in ~/.xprofile, as this file is reliably read by i3 at startup.
Pragmatic starting point โ begin with
qt5ctalone:export QT_QPA_PLATFORMTHEME=qt5ctThis setting usually works most reliably in practice and often affects Qt6 applications via a compatibility path.
If Qt6 apps ignore theming (e.g., KeePassXC remains unstyled), you can escalate to the following configuration:
export QT_QPA_PLATFORMTHEME=qt5ct:qt6ctThis combined syntax is officially supported from Qt 6.5, but its effectiveness can vary depending on the appโs build and Qt version. Only escalate if
qt5ctalone is insufficient.Note on path:
~/.config/environment.d/would be the โmore modernโ way, but itโs read bysystemd --userand doesnโt reliably reach the X process in every i3 session โ especially not instartxsetups without a display manager..xprofileis the more robust choice for i3 + X11.
Last-resort fallback: per-app wrapper
If neither variable above gets a stubborn Qt6 app to pick up the theme, set the variable just for that one app:
QT_QPA_PLATFORMTHEME=qt6ct keepassxcYou can wrap this in a small shell script and place it in ~/.local/bin/, or make a desktop file with the env-var prepended in Exec=. Inelegant, but reliable when the global escalation chain fails.
Obtain Qt Color Schemes
Qt color schemes are stored as .conf files under ~/.config/qt5ct/colors/ and ~/.config/qt6ct/colors/. Do not store them under /usr/share/qt5ct/colors/, as these directories are reserved for system defaults and can be overwritten by Pacman updates.
For Dracula, an official Qt5ct color scheme exists in the Dracula project:
curl -fsSL https://raw.githubusercontent.com/dracula/qt5/master/Dracula.conf \
-o ~/.config/qt5ct/colors/Dracula.conf
cp ~/.config/qt5ct/colors/Dracula.conf ~/.config/qt6ct/colors/Dracula.confFor Solarized Light, no official Qt5ct color scheme exists. The most reliable approach is to create one once via the GUI:
- Run
qt5ctfrom a terminal. - Tab โStyleโ โ next to โColor schemeโ click โEdit color schemeโ โ โCreateโ.
- Set Window Background
#fdf6e3, Window Text#586e75, Base#fdf6e3, Highlight#268bd2, etc. โ the eight Solarized base values are documented at ethanschoonover.com/solarized. - Save as
SolarizedLight.confunder~/.config/qt5ct/colors/. - Copy the file to
~/.config/qt6ct/colors/SolarizedLight.conf.
Why not use a third-party Solarized Qt scheme? Various community ports exist, but their quality and maintenance vary, and the
qt5ct.confformat is fairly strict. A one-time GUI creation gives you a result that exactly matches your other Solarized apps, without surprises.
Step 4: Split i3 Config
The i3 configuration cannot be recolored at runtime. You must switch it and reload.
Option A: include file (recommended, from i3 4.20)
Add the following line to ~/.config/i3/config:
include ~/.config/i3/theme.confThen create two theme-specific configuration files. Minimal working examples:
# Dracula i3 theme
cat > ~/.config/themes/dracula/i3-theme.conf <<'EOF'
# class border bground text indicator child_border
client.focused #6272a4 #44475a #f8f8f2 #bd93f9 #6272a4
client.focused_inactive #44475a #44475a #f8f8f2 #44475a #44475a
client.unfocused #282a36 #282a36 #bfbfbf #282a36 #282a36
client.urgent #44475a #ff5555 #f8f8f2 #ff5555 #ff5555
client.placeholder #282a36 #282a36 #f8f8f2 #282a36 #282a36
client.background #282a36
EOF
# Solarized Light i3 theme
cat > ~/.config/themes/solarized-light/i3-theme.conf <<'EOF'
# class border bground text indicator child_border
client.focused #268bd2 #eee8d5 #586e75 #268bd2 #268bd2
client.focused_inactive #93a1a1 #eee8d5 #586e75 #93a1a1 #93a1a1
client.unfocused #fdf6e3 #fdf6e3 #93a1a1 #fdf6e3 #fdf6e3
client.urgent #93a1a1 #dc322f #fdf6e3 #dc322f #dc322f
client.placeholder #fdf6e3 #fdf6e3 #586e75 #fdf6e3 #fdf6e3
client.background #fdf6e3
EOFThe hook script will then symlink the correct file to ~/.config/i3/theme.conf and execute i3-msg reload.
Option B: Symlink the entire config
If you prefer not to use the include directive, you can symlink the entire i3 configuration file. This is less flexible but also works.
Step 5: Integrate Polybar
Polybar loads its configuration at startup. There are two ways to restart Polybar:
Option 1: polybar-msg cmd restart (clean, from Polybar 3.6)
polybar-msg cmd restartOption 2: pkill + Launch Script (universal)
pkill polybar
sleep 0.3
~/.config/polybar/launch.shMinimal theme files
Youโll already have a working ~/.config/polybar/config.ini from your existing setup. The simplest approach is to extract just the colors into theme-specific files and include-file them. Minimal examples:
# Dracula Polybar palette
cat > ~/.config/themes/dracula/polybar.ini <<'EOF'
[colors]
background = #282a36
background-alt = #44475a
foreground = #f8f8f2
foreground-alt = #6272a4
primary = #bd93f9
secondary = #8be9fd
alert = #ff5555
disabled = #6272a4
EOF
# Solarized Light Polybar palette
cat > ~/.config/themes/solarized-light/polybar.ini <<'EOF'
[colors]
background = #fdf6e3
background-alt = #eee8d5
foreground = #586e75
foreground-alt = #93a1a1
primary = #268bd2
secondary = #2aa198
alert = #dc322f
disabled = #93a1a1
EOFIn your main Polybar config, reference the colors via include-file = ~/.config/polybar/colors.ini, and let the hook script symlink colors.ini to one of the two palette files. (If your existing config keeps colors and bar layout in a single file, store two complete copies in ~/.config/themes/<theme>/polybar.ini and let the hook symlink the whole config.ini instead โ both approaches work.)
Step 6: Chromium Flags
Chromium does not react to theme changes at runtime. Itโs important to clearly separate two things:
- UI-Dark-Mode: Affects the browser interface (toolbar, tabs, menus).
- Webpage-Force-Dark: Forces a dark mode for every webpage. However, this can distort images, diagrams, and carefully designed pages.
Recommended: UI-Dark-Mode only
Create the file ~/.config/chromium-flags.conf:
--force-dark-mode
--gtk-version=4--gtk-version=4 ensures that Chromium respects the current GTK4 theme and colors its toolbar to match other GTK applications.
Optional: Additionally force dark content for webpages
If you want to force dark mode for webpage content as well, add:
--enable-features=WebContentsForceDarkWarning: This flag name has changed multiple times across Chromium versions (
WebContentsForceDark,ForceWebContentsDarkMode, or only viachrome://flags). If it doesnโt work, checkchrome://flagsfor โForce Dark Mode for Web Contentsโ and enable it there. The recommendation remains: only enable if youโre comfortable with potential content distortion.
A true live toggle is not practical here without a browser restart.
Step 7: The Dark-Mode Hook Script
This script will be executed when darkman switches to dark mode. Create the file ~/.local/share/dark-mode.d/apply-theme:
A note on hook directory layout: This guide uses
~/.local/share/dark-mode.d/and~/.local/share/light-mode.d/, which thedarkman(1)manpage describes as the legacy format (kept for backwards compatibility). The current format is a single~/.local/share/darkman/directory with one script that receives the mode as$1. Both formats work; the split-directory layout is used here because it makes the dark and light scripts easy to read side-by-side. If you want the modern single-script style, consolidate the two scripts and branch oncase "$1" in dark|light) ... esac.
#!/usr/bin/env bash
# Robustness: undefined vars are errors, but individual app errors
# should not abort the entire hook.
set -u
trap 'echo "Hook error in: $BASH_COMMAND" >&2' ERR
THEME_DIR="$HOME/.config/themes/dracula"
GTK_THEME="Dracula"
# Fallback if dracula-gtk-theme is not installed
if ! [[ -d /usr/share/themes/Dracula || -d ~/.themes/Dracula ]]; then
GTK_THEME="Adwaita-dark"
fi
# โโโ GTK 3 / GTK 4 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
mkdir -p ~/.config/gtk-3.0 ~/.config/gtk-4.0
cat > ~/.config/gtk-3.0/settings.ini <<EOF
[Settings]
gtk-theme-name=$GTK_THEME
gtk-icon-theme-name=Adwaita
gtk-application-prefer-dark-theme=1
EOF
cp ~/.config/gtk-3.0/settings.ini ~/.config/gtk-4.0/settings.ini
# Also via gsettings (for apps that read this)
gsettings set org.gnome.desktop.interface gtk-theme "$GTK_THEME" 2>/dev/null || true
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' 2>/dev/null || true
# โโโ Qt5 / Qt6 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Redirect ColorScheme path in qt5ct/qt6ct โ user config, not /usr/share!
if [[ -f ~/.config/qt5ct/qt5ct.conf ]]; then
sed -i "s|^color_scheme_path=.*|color_scheme_path=$HOME/.config/qt5ct/colors/Dracula.conf|" \
~/.config/qt5ct/qt5ct.conf
fi
if [[ -f ~/.config/qt6ct/qt6ct.conf ]]; then
sed -i "s|^color_scheme_path=.*|color_scheme_path=$HOME/.config/qt6ct/colors/Dracula.conf|" \
~/.config/qt6ct/qt6ct.conf
fi
# โโโ Xresources / urxvt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/Xresources" ~/.Xresources
xrdb -merge ~/.Xresources
# Recolor running urxvt instances via escape sequences (best effort).
# -O tests if the executing user is the owner of the PTS โ protects in
# multi-user/SSH setups from writing to foreign terminals.
# Note: These sequences only change Background/Foreground, not ANSI colors.
for pts in /dev/pts/[0-9]*; do
if [[ -O "$pts" && -w "$pts" ]]; then
printf '\033]708;#282a36\007\033]11;#282a36\007\033]10;#f8f8f2\007' \
> "$pts" 2>/dev/null || true
fi
done
# โโโ Alacritty โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/alacritty.toml" ~/.config/alacritty/theme.toml
# Alacritty reloads automatically with live_config_reload=true
# โโโ i3 Theme โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/i3-theme.conf" ~/.config/i3/theme.conf
i3-msg reload >/dev/null
# โโโ Polybar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/polybar.ini" ~/.config/polybar/config.ini
if command -v polybar-msg >/dev/null 2>&1; then
polybar-msg cmd restart >/dev/null 2>&1 || {
pkill polybar; sleep 0.3; ~/.config/polybar/launch.sh &
}
else
pkill polybar; sleep 0.3; ~/.config/polybar/launch.sh &
fi
# โโโ neomutt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/neomutt-colors" ~/.config/neomutt/colors
# Running neomutt instances require :source ~/.config/neomutt/colors
# โโโ VSCodium โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Patch settings.json. VSCodium usually reacts live via file watcher,
# in rare cases a "Reload Window" (Ctrl+Shift+P) is needed.
SETTINGS="$HOME/.config/VSCodium/User/settings.json"
if [[ -f "$SETTINGS" ]] && command -v jq >/dev/null 2>&1; then
tmp=$(mktemp)
jq '."workbench.colorTheme" = "Dracula"' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
fi
# โโโ Vim/Neovim (if used) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/nvim-colorscheme.vim" ~/.config/nvim/colorscheme.vim 2>/dev/null || true
# โโโ Firefox / Thunderbird โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Nothing to do โ they follow darkman's XDG-Portal automatically.
# Prerequisite: in about:config widget.use-xdg-desktop-portal.settings = 1
# โโโ Chromium โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Does not react live; flags in ~/.config/chromium-flags.conf take effect
# on next start. See Step 6.
# โโโ User Notification โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
notify-send -i weather-clear-night "Theme" "Dark Mode activated (Dracula)" 2>/dev/null || true
echo "Dark mode activated (Dracula)"Make the script executable:
chmod +x ~/.local/share/dark-mode.d/apply-themeStep 8: The Light-Mode Hook Script
This script will be executed when darkman switches to light mode. Create the file ~/.local/share/light-mode.d/apply-theme:
#!/usr/bin/env bash
set -u
trap 'echo "Hook error in: $BASH_COMMAND" >&2' ERR
THEME_DIR="$HOME/.config/themes/solarized-light"
GTK_THEME="Adwaita"
# โโโ GTK 3 / GTK 4 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
mkdir -p ~/.config/gtk-3.0 ~/.config/gtk-4.0
cat > ~/.config/gtk-3.0/settings.ini <<EOF
[Settings]
gtk-theme-name=$GTK_THEME
gtk-icon-theme-name=Adwaita
gtk-application-prefer-dark-theme=0
EOF
cp ~/.config/gtk-3.0/settings.ini ~/.config/gtk-4.0/settings.ini
gsettings set org.gnome.desktop.interface gtk-theme "$GTK_THEME" 2>/dev/null || true
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light' 2>/dev/null || true
# โโโ Qt5 / Qt6 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if [[ -f ~/.config/qt5ct/qt5ct.conf ]]; then
sed -i "s|^color_scheme_path=.*|color_scheme_path=$HOME/.config/qt5ct/colors/SolarizedLight.conf|" \
~/.config/qt5ct/qt5ct.conf
fi
if [[ -f ~/.config/qt6ct/qt6ct.conf ]]; then
sed -i "s|^color_scheme_path=.*|color_scheme_path=$HOME/.config/qt6ct/colors/SolarizedLight.conf|" \
~/.config/qt6ct/qt6ct.conf
fi
# โโโ Xresources / urxvt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/Xresources" ~/.Xresources
xrdb -merge ~/.Xresources
for pts in /dev/pts/[0-9]*; do
if [[ -O "$pts" && -w "$pts" ]]; then
printf '\033]708;#fdf6e3\007\033]11;#fdf6e3\007\033]10;#586e75\007' \
> "$pts" 2>/dev/null || true
fi
done
# โโโ Alacritty โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/alacritty.toml" ~/.config/alacritty/theme.toml
# โโโ i3 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/i3-theme.conf" ~/.config/i3/theme.conf
i3-msg reload >/dev/null
# โโโ Polybar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/polybar.ini" ~/.config/polybar/config.ini
if command -v polybar-msg >/dev/null 2>&1; then
polybar-msg cmd restart >/dev/null 2>&1 || {
pkill polybar; sleep 0.3; ~/.config/polybar/launch.sh &
}
else
pkill polybar; sleep 0.3; ~/.config/polybar/launch.sh &
fi
# โโโ neomutt โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/neomutt-colors" ~/.config/neomutt/colors
# โโโ VSCodium โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
SETTINGS="$HOME/.config/VSCodium/User/settings.json"
if [[ -f "$SETTINGS" ]] && command -v jq >/dev/null 2>&1; then
tmp=$(mktemp)
jq '."workbench.colorTheme" = "Solarized Light"' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
fi
# โโโ Neovim โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
ln -sf "$THEME_DIR/nvim-colorscheme.vim" ~/.config/nvim/colorscheme.vim 2>/dev/null || true
# โโโ User Notification โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
notify-send -i weather-clear "Theme" "Light Mode activated (Solarized Light)" 2>/dev/null || true
echo "Light mode activated (Solarized Light)"Make the script executable:
chmod +x ~/.local/share/light-mode.d/apply-themeStep 9: Initial State and Persistence on Login
darkman is designed as a user service that persists its own state. The service typically remembers the last mode after a restart and executes the corresponding hooks at startup.
Important โ no hardcoded
darkman setcalls in.xprofile! It might be tempting to writedarkman set darkin.xprofileto โalways start with Dark mode.โ However, this is counterproductive:
- It overwrites the last manual toggle on every login.
- It completely disables auto-switching based on sunrise/sunset, if you enable it later.
Just let the service run.
Reality Check: Verify Persistence
In practice, reliable persistence depends on the interplay between the user service, login manager, and XDG state directory. Do not blindly trust it โ check it after the first login cycle:
# What does darkman currently think?
darkman get
# Was the service started cleanly at login and did it trigger hooks?
journalctl --user -u darkman.service -bIf darkman get returns a different mode than expected, or if the hook scripts were not executed at login, you have a persistence problem (see next section).
If Persistence is Unreliable
Some possible countermeasures:
- Ensure
darkman.serviceis truly enabled:systemctl --user is-enabled darkman.service - In rare setups (TTY login +
startx, no display manager), it helps to setdbus-update-activation-environment --systemd DISPLAY XAUTHORITYin.xprofileso that user services can see the X session. - Workaround: In
.xprofile, calldarkman getonce and only set a fallback mode if itโs empty. However, this is a hack โ itโs better to address the root cause of the problem.
Optional: Auto-Switching Based on Sunrise/Sunset
If darkman should automatically switch based on time of day, create the file ~/.config/darkman/config.yaml:
lat: 48.2082
lng: 16.3738
usegeoclue: false(The coordinates provided here are for Vienna โ adjust them to your location.)
The service calculates sunrise and sunset times from this and automatically triggers the hooks.
If you really need a default value on the very first start
If youโve just installed darkman and want a defined initial state for the very first toggle, perform this manually once in the shell:
darkman set darkโ not in .xprofile. From the next login onwards, the service will remember.
Step 10: Obtain Theme Files
For each application, place a dark and a light variant in ~/.config/themes/<theme>/. The following commands cover most cases. You can paste these directly:
Alacritty
curl -fsSL https://raw.githubusercontent.com/dracula/alacritty/master/dracula.toml \
-o ~/.config/themes/dracula/alacritty.toml
curl -fsSL https://raw.githubusercontent.com/alacritty/alacritty-theme/master/themes/solarized_light.toml \
-o ~/.config/themes/solarized-light/alacritty.tomlIn your main ~/.config/alacritty/alacritty.toml, import the symlink the hook will manage and confirm live-reload is on. Live-reload defaults to enabled in current Alacritty versions, but being explicit avoids confusion if a future upstream change flips the default:
import = ["~/.config/alacritty/theme.toml"]
[general]
live_config_reload = true # default; explicit for clarityTOML note: Pre-0.13 Alacritty used a top-level
live_config_reload = true. From 0.13 onward, it lives under[general]. If your Alacritty is older, drop the[general]table.
Xresources / urxvt
# Dracula
curl -fsSL https://raw.githubusercontent.com/dracula/xresources/master/Xresources \
-o ~/.config/themes/dracula/XresourcesFor Solarized Light, the upstream solarized/xresources file uses C preprocessor #define statements. Many display managers and login flows invoke xrdb with -nocpp, which silently strips these defines and leaves you with default colors. To avoid this entirely, write a flat (preprocessor-free) version directly:
cat > ~/.config/themes/solarized-light/Xresources <<'EOF'
! Solarized Light โ flat, no #define preprocessing required
*background: #fdf6e3
*foreground: #657b83
*fadeColor: #fdf6e3
*cursorColor: #586e75
*pointerColorBackground:#93a1a1
*pointerColorForeground:#586e75
! black
*color0: #073642
*color8: #002b36
! red
*color1: #dc322f
*color9: #cb4b16
! green
*color2: #859900
*color10: #586e75
! yellow
*color3: #b58900
*color11: #657b83
! blue
*color4: #268bd2
*color12: #839496
! magenta
*color5: #d33682
*color13: #6c71c4
! cyan
*color6: #2aa198
*color14: #93a1a1
! white
*color7: #eee8d5
*color15: #fdf6e3
EOFThe values above are taken from the canonical Solarized palette and match the structure of the upstream file with the #defines already resolved.
Polybar
See Step 5 โ minimal Dracula and Solarized Light palettes are inlined there. Copy them into ~/.config/themes/<theme>/polybar.ini if you havenโt already.
neomutt
# Dracula
mkdir -p ~/.config/themes/dracula
curl -fsSL https://raw.githubusercontent.com/dracula/mutt/master/dracula.muttrc \
-o ~/.config/themes/dracula/neomutt-colors
# Solarized Light (16-color version recommended for terminal accuracy)
curl -fsSL https://raw.githubusercontent.com/altercation/mutt-colors-solarized/master/mutt-colors-solarized-light-16.muttrc \
-o ~/.config/themes/solarized-light/neomutt-colorsVSCodium
VSCodium (like VS Code) ships with Solarized Light and Solarized Dark as built-in themes โ no extension required. Dracula, however, must be installed separately:
codium --install-extension dracula-theme.theme-draculaThe hook scripts already set the correct theme name via jq. If your VSCodium uses a different binary name (e.g., vscodium), adjust the command accordingly.
GTK and Qt
GTK Dracula via AUR (yay -S dracula-gtk-theme); Adwaita is shipped by default. Qt color schemes: see Step 3.
Reference table
| App | File | Source |
|---|---|---|
| GTK | Theme via AUR/Pacman | dracula-gtk-theme, Adwaita is default |
| Xresources/urxvt | Xresources | dracula/xresources, solarized/xresources |
| Alacritty | alacritty.toml | dracula/alacritty, alacritty/alacritty-theme |
| i3 | i3-theme.conf | inlined examples in Step 4 |
| Polybar | polybar.ini | inlined examples in Step 5 |
| neomutt | neomutt-colors | dracula/mutt, altercation/mutt-colors-solarized |
| VSCodium | built-in / Extension | Solarized built-in, โDracula Officialโ via marketplace |
| qt5ct/qt6ct | *.conf | dracula/qt5, Solarized via qt5ct GUI |
You can find Dracula themes collected at https://draculatheme.com โ Solarized at https://ethanschoonover.com/solarized.
Caveats and Honest Expectations
What switches live (without restart):
- GTK apps via gsettings/portal: Thunar, Firefox, Thunderbird (with Portal setting)
- Alacritty (with
live_config_reload = true) - i3 (after
reload) - Polybar (after restart)
- VSCodium (mostly, via file watcher on settings.json)
What requires an app restart:
- KeePassXC, nheko (Qt apps do not react live)
- GIMP, LibreOffice (own theme logic)
- urxvt terminals (except with escape sequence trick, which only affects Background/Foreground, not ANSI colors)
- neomutt (or
:sourcewithin the session) - Chromium (flags file takes effect on start)
What needs to be configured manually within the app:
- LibreOffice: set Light/Dark once in
Tools โ Options โ Application Colors - GIMP: has its own theme system under
Preferences โ Theme - Inkscape: follows GTK, but restart required
Tips
- Geo-based Auto-Switching:
darkmancan do this automatically based on sunrise/sunset โ see Step 9. - Debugging:
journalctl --user -u darkman.service -fshows if hooks are being executed. - Manual Hook Testing: Execute
~/.local/share/dark-mode.d/apply-themedirectly to see errors in isolation. - Consistency Across Multiple Machines: Version
~/.config/themes/,~/.local/share/dark-mode.d/,~/.local/share/light-mode.d/, and~/.config/qt5ct/colors/in your dotfiles repository. - Forced KeePassXC Reload (optional, usually undesirable): If you absolutely need the KeePassXC theme to update at the moment of switching, you can include
pkill keepassxcin the hook โ but remember that this will lose all unsaved changes and the open database. Realistically: just let it switch on the next start.
Extension Ideas
- base16-Framework instead of Dracula/Solarized: Generate a unified scheme for hundreds of apps from a single palette (https://github.com/chriskempson/base16).
- Per-Workspace-Themes: Theoretically possible via i3-IPC, but rarely practical.
- Polybar Module with current theme status as an indicator.
Summary
The implemented architecture is:
darkman (Trigger + Portal + Persistence)
โโโ Hooks โ GTK, Qt, Xresources, App Configs
โโโ Live-Reload: i3, Polybar, Alacritty, GTK Apps via Portal, VSCodium
โโโ Accepted: Some apps require restartYou switch with $mod+Shift+d. Most apps react immediately, some require a restart โ this is the current state of Linux desktop reality, not a flaw in your setup.
When setting up for the first time, itโs worthwhile to follow the phased strategy from the beginning of the tutorial: first the basics (GTK, Firefox, i3, Alacritty), then extensions, and finally the โproblem children.โ This way, you can more easily identify the cause of problems if they arise.





