I’ve spent years cycling through color schemes in my terminal and text editor - installing them, using them for a few days, then switching to something else. The Tomorrow theme was the first one that stuck. Its author later built Base16, a system for generating consistent color schemes across different applications. While Base16 hasn’t been actively maintained in recent years, the concept stuck around.
I recently discovered tinty, a maintained Base16 theme manager that applies color schemes to your terminal using escape sequences. It can switch themes on the fly without restarting the terminal.
Before I could use tinty with Konsole, I needed to add support for it. The tinted-terminal project generates terminal color schemes for various emulators, but Konsole was missing. I sent a PR which was quickly merged (thanks!).
With Konsole support in place, the next problem was making it automatic. KDE Plasma 6.5 recently added automatic light/dark mode switching based on time of day, but terminal color schemes don’t follow along. You can manually switch them, but that defeats the purpose.
Building the plugin
I wanted a zsh plugin that would:
- Detect when the desktop switches between light/dark mode
- Apply the appropriate tinty theme automatically
- Update all open terminal tabs, not just one
Detecting theme changes
Modern desktops expose theme settings through the XDG Desktop Portal over D-Bus. The org.freedesktop.appearance interface has a color-scheme setting that returns:
0- No preference (treat as light)1- Dark2- Light
You can query it with dbus-send:
dbus-send --session --print-reply --dest=org.freedesktop.portal.Desktop \
/org/freedesktop/portal/desktop \
org.freedesktop.portal.Settings.Read \
string:'org.freedesktop.appearance' \
string:'color-scheme'And monitor changes with dbus-monitor:
dbus-monitor --session \
"type='signal',interface='org.freedesktop.portal.Settings',member='SettingChanged',arg0='org.freedesktop.appearance',arg1='color-scheme'"The broadcasting problem
The first version worked for new tabs but failed when switching themes. Running tinty apply from a background job only updated whichever tab the job happened to be associated with. The other tabs stayed on the old theme.
I tried several approaches:
- Writing to parent process file descriptors (
/proc/$PPID/fd/1) - permission denied - Using a queue file and
precmdhooks - timing issues with initial theme on new tabs - Broadcasting to all
/dev/pts/*devices - triggered desktop notifications from KDE daemons
The solution: shell registration
Each shell that loads the plugin registers itself by writing its PID to /tmp/tinty-shells/<pts-number>:
local my_tty=$(_tinty_get_tty)
local my_pts_num=""
[[ "$my_tty" =~ /dev/pts/([0-9]+)$ ]] && my_pts_num="${match[1]}"
if [[ -n "$my_pts_num" ]]; then
mkdir -p /tmp/tinty-shells
echo $$ > "/tmp/tinty-shells/$my_pts_num"
fiWhen a theme change is detected, the plugin:
- Acquires a lock (so only one tab does the work)
- Runs
tinty applyonce and captures the output - Writes the escape sequences to each registered terminal device
_tinty_apply_for_scheme() {
local color_scheme=$1
{
flock -n 9 || exit 0 # Skip if another tab is applying
local theme=$(_tinty_theme_for_scheme "$color_scheme")
local tinty_output=$($TINTY_BIN apply "$theme" 2>/dev/null)
[[ -d /tmp/tinty-shells ]] || exit 0
for pts_file in /tmp/tinty-shells/*; do
[[ -e "$pts_file" ]] || continue
local pts="/dev/pts/$(basename "$pts_file")"
local pid=$(cat "$pts_file" 2>/dev/null)
# Verify shell is running and terminal is writable
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && [[ -w "$pts" ]]; then
printf '%s' "$tinty_output" > "$pts" 2>/dev/null
else
rm -f "$pts_file" # Clean up stale registration
fi
done
} 9>/tmp/tinty-portal.lock
}This way:
tinty applyruns once, not once per tab- Only registered shells (running this plugin) get updated
- Stale registrations are cleaned up automatically
- Lock prevents race conditions between multiple watchers
ZLE-safe initialization
Running the D-Bus watcher immediately on plugin load caused issues with cursor positioning and widgets. The solution was to defer initialization until ZLE is ready:
autoload -Uz add-zle-hook-widget
tinty_portal_zle_init() {
[[ -n "$TINTY_PORTAL_WATCHER_RUNNING" ]] && return 0
export TINTY_PORTAL_WATCHER_RUNNING=1
add-zle-hook-widget -d zle-line-init tinty_portal_zle_init
# ... start watcher
}
add-zle-hook-widget zle-line-init tinty_portal_zle_initThis ensures the watcher starts only after the prompt is ready and widgets are stable.
Installation
Clone the plugin to your oh-my-zsh custom plugins directory:
git clone https://github.com/shanemcd/zsh-auto-tinty \
${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/auto-tintyConfigure your light and dark themes in ~/.zshrc:
export ZSH_TINTY_LIGHT="base16-ia-light"
export ZSH_TINTY_DARK="base16-ia-dark"
plugins+=(auto-tinty)Reload your shell:
exec zshHow it works
When you open a new terminal tab:
- Plugin loads and registers the shell in
/tmp/tinty-shells/ - Queries current theme via D-Bus
- Applies the appropriate tinty theme directly to that terminal
- Starts a
dbus-monitorbackground job (once per shell) - On shell exit, cleans up registration and kills the watcher
When the desktop theme changes:
- One of the D-Bus watchers detects the signal
- Waits 200ms for signals to settle (debouncing)
- Acquires lock in
/tmp/tinty-portal.lock - Runs
tinty applyonce - Broadcasts escape sequences to all registered terminals
- Releases lock
All open terminal tabs switch themes simultaneously.
Results
Now when my desktop switches to light mode in the morning, all my terminal tabs follow along. When it switches back to dark mode in the evening, same thing.
No manual theme switching, no forgetting to update that one terminal tab you opened three days ago.
The plugin is at github.com/shanemcd/zsh-auto-tinty. It should work with any terminal that supports tinty’s escape sequences and any desktop that implements the XDG Desktop Portal. If you run into problems or have improvements, please open an issue or send a PR.