Skip to content

Commit d2fdc7c

Browse files
shanemcdclaude
andcommitted
Add post about auto-switching Konsole themes with tinty
Covers the journey from Tomorrow theme through Base16 to tinty, adding Konsole support to tinted-terminal, and building a zsh plugin that automatically switches terminal themes based on KDE Plasma's light/dark mode changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 228c718 commit d2fdc7c

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
---
2+
title: Auto-switching Konsole themes with tinty and KDE light/dark mode
3+
---
4+
5+
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](https://github.com/chriskempson/tomorrow-theme) was the first one that stuck. Its author later built [Base16](https://github.com/chriskempson/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.
6+
7+
I recently discovered [tinty](https://github.com/tinted-theming/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.
8+
9+
Before I could use tinty with Konsole, I needed to add support for it. The [tinted-terminal](https://github.com/tinted-theming/tinted-terminal) project generates terminal color schemes for various emulators, but Konsole was missing. I [sent a PR](https://github.com/tinted-theming/tinted-terminal/pull/28) which was quickly merged (thanks!).
10+
11+
With Konsole support in place, the next problem was making it automatic. KDE Plasma 6.5 [recently added](https://blogs.kde.org/2025/08/02/this-week-in-plasma-day/night-theme-switching/) 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.
12+
13+
## Building the plugin
14+
15+
I wanted a zsh plugin that would:
16+
1. Detect when the desktop switches between light/dark mode
17+
2. Apply the appropriate tinty theme automatically
18+
3. Update all open terminal tabs, not just one
19+
20+
### Detecting theme changes
21+
22+
Modern desktops expose theme settings through the [XDG Desktop Portal](https://flatpak.github.io/xdg-desktop-portal/) over D-Bus. The `org.freedesktop.appearance` interface has a `color-scheme` setting that returns:
23+
- `0` - No preference (treat as light)
24+
- `1` - Dark
25+
- `2` - Light
26+
27+
You can query it with `dbus-send`:
28+
29+
```bash
30+
dbus-send --session --print-reply --dest=org.freedesktop.portal.Desktop \
31+
/org/freedesktop/portal/desktop \
32+
org.freedesktop.portal.Settings.Read \
33+
string:'org.freedesktop.appearance' \
34+
string:'color-scheme'
35+
```
36+
37+
And monitor changes with `dbus-monitor`:
38+
39+
```bash
40+
dbus-monitor --session \
41+
"type='signal',interface='org.freedesktop.portal.Settings',member='SettingChanged',arg0='org.freedesktop.appearance',arg1='color-scheme'"
42+
```
43+
44+
### The broadcasting problem
45+
46+
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.
47+
48+
I tried several approaches:
49+
- Writing to parent process file descriptors (`/proc/$PPID/fd/1`) - permission denied
50+
- Using a queue file and `precmd` hooks - timing issues with initial theme on new tabs
51+
- Broadcasting to all `/dev/pts/*` devices - triggered desktop notifications from KDE daemons
52+
53+
### The solution: shell registration
54+
55+
Each shell that loads the plugin registers itself by writing its PID to `/tmp/tinty-shells/<pts-number>`:
56+
57+
```zsh
58+
local my_tty=$(_tinty_get_tty)
59+
local my_pts_num=""
60+
[[ "$my_tty" =~ /dev/pts/([0-9]+)$ ]] && my_pts_num="${match[1]}"
61+
62+
if [[ -n "$my_pts_num" ]]; then
63+
mkdir -p /tmp/tinty-shells
64+
echo $$ > "/tmp/tinty-shells/$my_pts_num"
65+
fi
66+
```
67+
68+
When a theme change is detected, the plugin:
69+
1. Acquires a lock (so only one tab does the work)
70+
2. Runs `tinty apply` once and captures the output
71+
3. Writes the escape sequences to each registered terminal device
72+
73+
```zsh
74+
_tinty_apply_for_scheme() {
75+
local color_scheme=$1
76+
77+
{
78+
flock -n 9 || exit 0 # Skip if another tab is applying
79+
80+
local theme=$(_tinty_theme_for_scheme "$color_scheme")
81+
local tinty_output=$($TINTY_BIN apply "$theme" 2>/dev/null)
82+
83+
[[ -d /tmp/tinty-shells ]] || exit 0
84+
for pts_file in /tmp/tinty-shells/*; do
85+
[[ -e "$pts_file" ]] || continue
86+
87+
local pts="/dev/pts/$(basename "$pts_file")"
88+
local pid=$(cat "$pts_file" 2>/dev/null)
89+
90+
# Verify shell is running and terminal is writable
91+
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null && [[ -w "$pts" ]]; then
92+
printf '%s' "$tinty_output" > "$pts" 2>/dev/null
93+
else
94+
rm -f "$pts_file" # Clean up stale registration
95+
fi
96+
done
97+
} 9>/tmp/tinty-portal.lock
98+
}
99+
```
100+
101+
This way:
102+
- `tinty apply` runs once, not once per tab
103+
- Only registered shells (running this plugin) get updated
104+
- Stale registrations are cleaned up automatically
105+
- Lock prevents race conditions between multiple watchers
106+
107+
### ZLE-safe initialization
108+
109+
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:
110+
111+
```zsh
112+
autoload -Uz add-zle-hook-widget
113+
114+
tinty_portal_zle_init() {
115+
[[ -n "$TINTY_PORTAL_WATCHER_RUNNING" ]] && return 0
116+
export TINTY_PORTAL_WATCHER_RUNNING=1
117+
118+
add-zle-hook-widget -d zle-line-init tinty_portal_zle_init
119+
# ... start watcher
120+
}
121+
122+
add-zle-hook-widget zle-line-init tinty_portal_zle_init
123+
```
124+
125+
This ensures the watcher starts only after the prompt is ready and widgets are stable.
126+
127+
## Installation
128+
129+
Clone the plugin to your oh-my-zsh custom plugins directory:
130+
131+
```bash
132+
git clone https://github.com/shanemcd/zsh-auto-tinty \
133+
${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/auto-tinty
134+
```
135+
136+
Configure your light and dark themes in `~/.zshrc`:
137+
138+
```zsh
139+
export ZSH_TINTY_LIGHT="base16-ia-light"
140+
export ZSH_TINTY_DARK="base16-ia-dark"
141+
plugins+=(auto-tinty)
142+
```
143+
144+
Reload your shell:
145+
146+
```bash
147+
exec zsh
148+
```
149+
150+
## How it works
151+
152+
When you open a new terminal tab:
153+
1. Plugin loads and registers the shell in `/tmp/tinty-shells/`
154+
2. Queries current theme via D-Bus
155+
3. Applies the appropriate tinty theme directly to that terminal
156+
4. Starts a `dbus-monitor` background job (once per shell)
157+
5. On shell exit, cleans up registration and kills the watcher
158+
159+
When the desktop theme changes:
160+
1. One of the D-Bus watchers detects the signal
161+
2. Waits 200ms for signals to settle (debouncing)
162+
3. Acquires lock in `/tmp/tinty-portal.lock`
163+
4. Runs `tinty apply` once
164+
5. Broadcasts escape sequences to all registered terminals
165+
6. Releases lock
166+
167+
All open terminal tabs switch themes simultaneously.
168+
169+
## Results
170+
171+
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.
172+
173+
No manual theme switching, no forgetting to update that one terminal tab you opened three days ago.
174+
175+
The plugin is at [github.com/shanemcd/zsh-auto-tinty](https://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.

0 commit comments

Comments
 (0)