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