Automatically synchronize Neovim with the current macOS light or dark appearance.
The plugin watches ~/Library/Preferences/.GlobalPreferences.plist via vim.uv.new_fs_event()
and periodically polls the current appearance as a fallback. It syncs once during setup, then
reacts to both file events and the poll timer to ensure timely detection.
- macOS
- Neovim 0.10+
{
"pzehrel/macos-appearance.nvim",
event = "UIEnter",
config = function()
require("macos-appearance").setup {
callback = function(appearance)
-- your theme-switching logic here
end,
}
end,
}callback receives "dark" or "light" whenever the system appearance changes
(and once during startup). It can be a plain function or an adapter table
{ apply = fun(appearance), reset? = fun() }.
For advanced use, skip callback and listen to User MacosAppearanceChanged
with data = { appearance = "dark" | "light" }.
During setup() the plugin:
- Detects the current macOS appearance.
- Calls
callback(if set) and firesUser MacosAppearanceChanged. - Starts a file watcher on the plist and a periodic poll timer.
- Registers cleanup on
VimLeavePre.
The plugin never writes to chadrc.lua or any other config file.
File events are debounced at 100 ms (configurable).
Note
macOS writes appearance changes to the plist through cfprefsd, which may
defer the actual disk write for several seconds. The file watcher alone
only catches the disk event. To bridge the gap the plugin runs a
lightweight poll (default every 2 s) that queries the current appearance
via defaults read. This ensures the theme switches promptly even when
cfprefsd delays the write. Set poll_interval_ms = 0 to disable
polling and rely solely on file events.
| Option | Default | Description |
|---|---|---|
debounce_ms |
100 |
Debounce window for file events |
retry_ms |
250 |
Retry delay when the plist is unwatchable |
poll_interval_ms |
2000 |
Polling interval (0 = disable) |
debug |
false |
Enable INFO-level diagnostic messages |
notify |
true |
Show informational messages |
path |
~/Library/Preferences/.GlobalPreferences.plist |
Override the watched file |
callback |
nil |
Called on appearance change |
local ma = require "macos-appearance"
ma.get() -- "dark" | "light"
ma.sync() -- detect now and fire callback / event
ma.start() -- start the file watcher
ma.stop() -- stop and release handlessetup() calls sync() then start(). Repeated calls to setup() are safe.
A built-in adapter for NvChad Base46 is included. Configure theme_toggle in chadrc.lua
(the first theme is light, the second dark):
-- chadrc.lua
M.base46 = {
theme = "tokyodark",
theme_toggle = { "flexoki-light", "tokyodark" },
}Then pass the adapter as the callback:
{
"pzehrel/macos-appearance.nvim",
event = "UIEnter",
config = function()
require("macos-appearance").setup {
callback = require("macos-appearance.adapters.nvchad"),
}
end,
}UIEnter ensures Base46 is initialized before the first sync.
The adapter exposes apply(appearance) and reset() for direct use:
local nvchad = require("macos-appearance.adapters.nvchad")
nvchad.apply("dark") -- switch to dark theme
nvchad.reset() -- clear internal state before re-setupmake check # format, lint, testTests run in headless Neovim without touching the real preferences plist.
MIT