Making Neovim and Emacs Adapt to the System's Dark Mode
Table of Contents
In this post we'll see how to make Neovim and Emacs automatically react when the system's theme changes in Linux and macOS. The process is roughly the same, but specific instructions for each operating system will be provided when needed.
Neovim
Unfortunately for Vim users, this method only works with Neovim, as we'll be using its API to create socket files and run Lua code from an external script. Additionally, for Neovim users with a config written in Vimscript, note that you can embed Lua code in you config as shown below. Otherwise, the code should go in the init.lua
file.
" vimscript
lua << EOF
print("This is some Lua code")
EOF
" vimscript
Creating Socket Files
The following code creates a unique socket file for each running nvim
session under /tmp/nvim
, named using the instace's PID: /tmp/nvim/nvim[PID].sock
.
function createSocket()
pid = vim.fn.getpid()
socket_name = '/tmp/nvim/nvim' .. pid .. '.sock'
vim.fn.mkdir('/tmp/nvim', 'p')
vim.fn.serverstart(socket_name)
end
Creating a Function that Updates the Theme
We'll write a Lua function that sets the appropriate colorscheme by querying the current system theme. This function will run once every time a new nvim
session starts, and we'll then call it again from a Python script whenever the system theme changes, through the sockets we created.1
Linux
This code works in environments using the Freedesktop colorscheme preference implementation, such as GNOME or KDE Plasma. If you're using a standalone window manager and/or managing themes yourself, you already know what to do instead.
function updateColorscheme()
command = "dbus-send --session --dest=org.freedesktop.portal.Desktop --print-reply /org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read string:'org.freedesktop.appearance' string:'color-scheme' | grep -o 'uint32 .' | cut -d' ' -f2"
handle = io.popen(command)
output = handle:read("*a")
handle:close()
output = string.gsub(output, "\n", "")
if output == "1" then
-- Set dark colorscheme
else
-- Set light colorscheme
end
end
macOS
function updateColorscheme()
exit_code = os.execute("defaults read -g AppleInterfaceStyle")
if exit_code == 0 then
-- Set dark colorscheme
else
-- Set light colorscheme
end
end
Running the Functions on Launch
Place this Lua code after the previous functions' definitions.
vim.api.nvim_create_augroup('custom_startup', {})
vim.api.nvim_create_autocmd('VimEnter', {
desc = 'Create a socket for every nvim process',
group = 'custom_startup',
once = true,
callback = createSocket
})
vim.api.nvim_create_autocmd('UIEnter', {
desc = 'Set the appropriate theme on startup',
group = 'custom_startup',
once = true,
callback = updateColorscheme
})
Calling a Function from a Python Script
Now we can call the updateColorscheme
function through the socket using a Python script with the pynvim
library, which is available in most distribution's repositories. Otherwise, it can be installed directly with Python's pip
.
pip install pynvim
Here's the script for changing the colorscheme.
#!/usr/bin/env python3
import glob
from pynvim import attach
nvim_sockets = (attach('socket', path=p) for p in glob.glob('/tmp/nvim/nvim*.sock'))
for nvim in nvim_sockets:
nvim.exec_lua('updateColorscheme()')
All that remains is to automate running this script whenever the system theme changes, which we'll discuss at the end of this post.
Emacs
Making Emacs react to theme changes is very similar to the Neovim process described in the previous section: we'll create a function that queries the system theme, and we'll run it both on startup and every time it changes. The difference here is that we don't need to create sockets nor a Python script, as Emacs already gives us a way to run Elisp code with a shell command.
Creating a Function that Updates the Theme
Here's the function that gets the correct theme and applies it.2
Linux
Note: As mentioned in the Neovim section, this will work in desktop environments like GNOME and KDE Plasma. Other users will know what to do instead.
;; Set the theme variables
(setq dark-theme 'doom-tomorrow-night)
(setq light-theme 'doom-tomorrow-day)
(defun get-correct-theme ()
(if (= (call-process-shell-command "dbus-send --session --dest=org.freedesktop.portal.Desktop --print-reply /org/freedesktop/portal/desktop org.freedesktop.portal.Settings.Read string:'org.freedesktop.appearance' string:'color-scheme' | grep -q 'uint32 1'") 0)
dark-theme
light-theme))
;; Apply the correct theme on startup
(setq doom-theme (get-correct-theme))
;; Function that updates the theme
(defun update-theme ()
(load-theme (get-correct-theme)))
macOS
;; Set the theme variables
(setq dark-theme 'doom-tomorrow-night)
(setq light-theme 'doom-tomorrow-day)
(defun get-correct-theme ()
(if (= (call-process-shell-command "defaults read -g AppleInterfaceStyle") 0)
dark-theme
light-theme))
;; Apply the correct theme on startup
(setq doom-theme (get-correct-theme))
;; Function that updates the theme
(defun update-theme ()
(load-theme (get-correct-theme)))
Calling the Function from the Shell
The function can be run from outside Emacs via the following command.
emacsclient --no-wait --eval "(update-theme)"
The next section will discuss how to run this command automatically when the system theme changes.
How to Automatically Follow the System Theme
Creating a Script
The first step is to create a shell script to update both Neovim and Emacs as discussed in the previous sections. We'll put this script together with the Python script we wrote in the Neovim section inside ~/.scripts
.
~/.scripts ├── neovim_emacs_theme.sh └── update_neovim_theme.py
Here are the contents of neovim_emacs_theme.sh
.
#!/bin/sh
update_theme() {
emacsclient --no-wait --eval "(update-theme)" &
~/.scripts/update_neovim_theme.py &
} > /dev/null 2>&1
update_theme()
Finally, we need to make both scripts executable.
chmod +x neovim_emacs_theme.sh
chmod +x update_neovim_theme.py
Running the Script Automatically
How to run the script when the system theme changes will be different in every desktop and operating system. I will explain how to do it both in GNOME and in macOS, as that covers the two most common use cases.
On Linux (GNOME)
The
Night Theme Switcher extension lets us run a script when GNOME's theme changes. Just install the extension, open its settings window and navigate to the "Commands" tab, enable "Run commands" and write our script's path in both the "Sunrise" and "Sunset" text fields (i.e. ~/.scripts/neovim_emacs_theme.sh
).
On macOS
macOS doesn't let us hook a script to run when the theme changes, but we can achieve the same result by using a lightweight daemon to observe these theme changes and react to them by running our script. AbyssWatcher is one such daemon I wrote in Swift for this purpose, as there were no usable existing projects with this functionality at the time of writing.
Unfortunately, the setup process is a bit longer than just installing a GNOME extension, but the same final result can be achieved. In fact, the GIF at the start of this post was recorded on macOS using AbyssWatcher! Read the project's README.org
file for further instructions on setting it up.
For an example on how exactly to set the colorscheme, you can have a look at my dotfiles.
The code comes from my Doom Emacs config, but it is easy to change it for vanilla Emacs or other Emacs distributions.