This commit is contained in:
Barrett Ruth 2025-11-30 01:50:28 -05:00
parent 15401ee1de
commit 336e4018ab

View file

@ -1,14 +1,29 @@
--- ---
title: "auto-theme.nvim" title: "auto-theme.nvim"
slug: "auto-theme.nvim" slug: "auto-theme.nvim"
date: "11/28/2025" date: "28/11/2025"
--- ---
# the problem # the problem
I toggle between light and dark mode at at around 17:00 every day. Resetting my environment and theme across all my applications was a pain. I toggle between light and dark mode at at around 17:00 every day. Resetting my environment and theme across all my applications was a pain. I built auto-theme.nvim to update my neovim theme automatically.
I use my color scheme [midnight.nvim](/git/midnight.nvim.html) on macOS and linux with these applications: ## the solution
I spawn a OS-specific "watcher" (python process) to listen, in non-polling manner, for theme updates:
- linux: I integrate with D-Bus, parsing and filtering messages for settings changes to the system appearance.
- macOS: I integrate with the Apple toolkit AppKit to do the same thing--wait for a theme changed notification to be pushed out to the running app object and print accordingly.
Stdout is continually parsed from the job and the appropriate theme is set.
# the interesting problem
OK, cool. Now, how can I implement automatic theme-switching for _all_ programs I use?
> I use my color scheme [midnight.nvim](/git/midnight.nvim.html)
I most commonly use the following applications:
1. [neovim](https://neovim.io/) 1. [neovim](https://neovim.io/)
2. [ghostty](https://ghostty.org/) 2. [ghostty](https://ghostty.org/)
@ -21,30 +36,86 @@ I use my color scheme [midnight.nvim](/git/midnight.nvim.html) on macOS and linu
9. [ripgrep](https://github.com/BurntSushi/ripgrep) 9. [ripgrep](https://github.com/BurntSushi/ripgrep)
10. [zsh](https://www.zsh.org/) 10. [zsh](https://www.zsh.org/)
# criteria for solution > All of the code can be found in my [dotfiles](https://github.com/barrett-ruth/dots). The implementations are scattered and I provide no guarantee that files will not be moved. Unfortunately, that makes this post a wall of text.
## success criteria
I run or trigger _one command_--every application updates automatically. I run or trigger _one command_--every application updates automatically.
The feasability of this depends on the underlying support software has for dynamic reloading of configuration. In many cases this is not possible. The feasability of this depends on the underlying support software has for dynamic reloading of configuration. In many cases this is not possible.
# the solution ## the solution
As of November 28, 2025, I've created [this script](https://github.com/barrett-ruth/dots/blob/main/scripts/theme) which is bound by a [karabiner](https://karabiner-elements.pqrs.org/) and [keyd](https://github.com/rvaiya/keyd) binding for macOS and linux, respectively. As of November 28, 2025, I've created [this script](https://github.com/barrett-ruth/dots/blob/main/scripts/theme) which is bound by a [karabiner](https://karabiner-elements.pqrs.org/) and [keyd](https://github.com/rvaiya/keyd) binding for macOS and linux, respectively, for quick access.
## successes ### successes
1. neovim: I use my non-polling, macOS/linux-supporting automatic theme switcher [auto-theme.nvim](https://github.com/barrett-ruth/auto-theme.nvim) 1. neovim: [auto-theme.nvim](https://github.com/barrett-ruth/auto-theme.nvim)
2. ghostty: Ghostty supports [light and dark themes based on the system appearance](https://github.com/tmux/tmux/wiki)--easy. 2. ghostty: Ghostty supports [light and dark themes based on the system appearance](https://github.com/tmux/tmux/wiki)--easy.
3. sioyek: Any changes to user configuration are automatically reloaded--a script updates user preferences in-place 3. sioyek: Any changes to user configuration are automatically reloaded--my script updates the program's settings file `prefs_user.config` in-place
4. ungoogled-chromium: I folded and used the default system theme which automatically reads and updates according to the system environment 4. ungoogled-chromium: I folded and used the default system theme which automatically reads and updates according to the system environment
5. swaywm (linux): sway reads from a symlink'ed theme file `sed`ed, updated and reloaded by the script 5. swaywm (linux): sway reads from a symlink'ed theme file updated by the scripts; `swaymsg reload` triggers an instant window-manager-wide reload
6. rofi: `config.rasi` also reads from a symlink'ed theme files that is updated and reloaded by the script 6. rofi: the config file, `config.rasi`, derives its theme from a symlink'ed file updated and reloaded by the script
7. tmux: `tmux.conf` reads from symlink'ed theme files that are automatically reloaded with `source-file` 7. tmux: similarly to rofi, the config `tmux.conf` reads from symlink'ed theme files that are automatically reloaded with `source-file`. I also refresh the UI with `refresh-client`:
## failures ```sh
ln -sf "$XDG_CONFIG_HOME/tmux/themes/$theme.tmux" "$XDG_CONFIG_HOME/tmux/themes/theme.tmux"
tmux source-file "$XDG_CONFIG_HOME/tmux/themes/$theme.tmux"
tmux refresh-client -S 2>/dev/null || true
```
### failures
Unfortunately, the following programs I've found nearly impossible to dynamically reload: Unfortunately, the following programs I've found nearly impossible to dynamically reload:
8. fzf: Overwriting fzf's themes, from the interactive shell `fzf` binary to `fzf-{cd,file}-widget` to integration with [fzf-lua](https://github.com/ibhagwan/fzf-lua/), I found this potentially doable but just _way too complex_. Feel free to investigate yourself--I'm going with the default theme. 8. fzf: Overwriting fzf's themes, from the interactive shell `fzf` binary to `fzf-{cd,file}-widget` to integration with [fzf-lua](https://github.com/ibhagwan/fzf-lua/), I found this potentially doable but just _way too complex_. Feel free to investigate yourself--I'm going with the default theme.
9. ripgrep: I use the default theme. The ripgrep global configuration file does not support environment variables, exterminating the option to provide a `${THEME}`-based path in the global configuration file. 9. ripgrep: I use the default theme. The ripgrep global configuration file does not support environment variables, exterminating the option to provide a `${THEME}`-based path in the global configuration file.
10. zsh: it's impossible to update `$THEME` across all existing shells (simply a limit of posix). However, all affected _programs_ will read the proper `$THEME`--I'm fine compromising here. 10. zsh: it's impossible to update `$THEME` across all existing shells (simply a limit of posix). However, all affected _programs_ will read the proper `$THEME`--I'm fine compromising here.
### UPD: 8, 9 <span class="date">30/11/2025</span>
After some _extreme_ amounts of finagling I'm now able to automatically update fzf and ripgrep themes both in the shell (after re-rendering the prompt\*) and in new fzf-lua instances. I consider this a 99% win.
I do it with the following strategy for:
a) cli programs
Since, according to \#10 above, it is impossible to update all running shells, one way I can come to auto-updating program themes is in between terminal prompts. To get around this, I added a zsh precmd hook to my `zshrc` for both programs which checks if a symlink has been updated:
- fzf: If `$XDG_CONFIG_HOME/fzf/themes/theme` is updated, re-export `$FZF_DEFAULT_OPTS` to include the new colors:
```zsh
_fzf_theme_precmd() {
local theme_file="$XDG_CONFIG_HOME/fzf/themes/theme"
local theme_target=$(readlink "$theme_file" 2>/dev/null) || return
typeset -g _FZF_THEME_TARGET
test "$theme_target" = "$_FZF_THEME_TARGET" && return
_FZF_THEME_TARGET="$theme_target"
test -r "$theme_file" && export FZF_DEFAULT_OPTS="$(<"$theme_file") $FZF_OPTS"
}
add-zsh-hook precmd _fzf_theme_precmd
```
- ripgrep: If `$XDG_CONFIG_HOME/rg/config` is updated, re-build the `$RIPGREP_CONFIG_PATH` file as a concatentation of the _new_ theme and a separete _base_ configuration file and re-export the environment variable.
Any and all shells, after re-rendering their terminal prompts, will see proper colors for all fzf and ripgrep commands.
b) neovim
This is a bit trickier. How can a running neovim process automatically update its internal colors used by fzf-lua? There are two aspects of this problem:
1. Finding and interacting with all existing Neovim instances: RPCs with remote neovim features provide the ability to remotely probe neovim instances.
I use an RPC to trigger the update (see \#2 below). Of course, this requires automatically configuring a socket for each neovim instance to listen on. I use the process id, unique to the neovim instance--any 1:1 mapping from neovim instance to socket identifier will do:
```lua
local socket_path = ('/tmp/nvim-%d.sock'):format(vim.fn.getpid())
vim.fn.serverstart(socket_path)
```
Neovim instances can be found by just listing `/tmp/nvim-*.sock`.
2. Re-configuring fzf-lua: fzf-lua does not support "dynamic" reconfiguration but you can re-initialize the plugin with `require('fzf-lua').setup(opts)`.
- fzf: I expose a function for RPC calls `fzf_theme.reload_colors()` which re-initializes the fzf environment. Special care must be taken to store and pass down the _initial_ fzf-lua configuration and update it with the new environments colors.
- ripgrep: automatically re-reads from `$RIPGREP_CONFIG_PATH`, a symlink updated by my theme script