Your terminal tools do not follow the system theme. The OS flips to dark at sunset and every config file you own stays where it was, until you go and change them.
heimdallr watches the system light/dark theme and runs a command when it changes. What the command does is up to you.
heimdallr watch --on-change 'vanadis apply --variant "$1"'That runs once now with the current mode, and again every time the OS flips. Ask it the mode instead, and it answers in one word:
$ heimdallr
dark- Four platforms. macOS, Windows and Linux come from
dark-light. WSL reads the Windows theme throughreg.exe, which nothing else does. - It will not guess. A host with no system theme exits non-zero and says so, instead of
reporting
lightand sending you off to apply a theme nobody asked for. - One word on stdout.
$(heimdallr)is a baredarkorlight. Everything else goes to stderr. - It catches up.
watchfires once at start rather than only on the next change, so a machine that was asleep or off across the transition is not left behind.
$ cargo install heimdallrOr take a prebuilt binary from the latest release, which carries macOS, Linux and Windows archives and an installer script for each.
From a clone:
$ cargo build --releaseheimdallr does not keep itself alive. It has no daemon mode, no PID file and no config file, because systemd and launchd already do that and do it better. Copy one of these.
~/.config/systemd/user/heimdallr.service:
[Unit]
Description=Follow the system light/dark theme
[Service]
ExecStart=%h/.cargo/bin/heimdallr watch --on-change 'vanadis apply --variant "$1"'
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target$ systemctl --user daemon-reload
$ systemctl --user enable --now heimdallr.serviceRestart=on-failure matters. watch refuses to start when the mode cannot be read, which is
the right answer for a one-shot invocation and the wrong one for a unit that came up before the
thing it reads. Coming back in ten seconds costs nothing.
To see what it is doing, filter by the identifier rather than the unit — the command's own
output does not come back under -u:
$ journalctl --user -t heimdallr -f~/Library/LaunchAgents/com.github.torabit.heimdallr.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.github.torabit.heimdallr</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/heimdallr</string>
<string>watch</string>
<string>--on-change</string>
<string>vanadis apply --variant "$1"</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/heimdallr.log</string>
<key>StandardErrorPath</key>
<string>/tmp/heimdallr.log</string>
</dict>
</plist>$ launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.github.torabit.heimdallr.plistWSL is why this exists. Every cross-platform detector reaches for the XDG Desktop Portal, and
under WSL the portal knows nothing about the Windows theme. heimdallr reads the registry
through reg.exe instead.
It finds reg.exe through /proc/mounts when PATH does not have it, so nothing needs
adjusting in the unit file above. A systemd --user unit does not inherit the Windows
directories WSL appends to a login shell's PATH, and [interop] appendWindowsPath = false in
/etc/wsl.conf takes them away from the login shell too.
WSL is also the one platform that polls, because reg.exe has nothing to block on. --interval
sets how often and defaults to 30 seconds:
heimdallr watch --interval 60 --on-change 'vanadis apply --variant "$1"'One read costs about 29 ms, so the default is around 0.1% of one core. The price is latency: a
switch lands up to one interval late, and for light and dark that is a fine trade. It is also
why no Windows-side helper is worth building. --interval is ignored everywhere else, where the
system says when the theme changed.
It holds no state. The command owns which theme is applied. A second record of that would
drift, and being called twice with the same mode is harmless, so watch fires once at start
rather than waiting for a change it may already have missed while the machine was off.
It does not manage its own process. No daemon, no PID file, no log rotation, no config file. See the unit files above.
It does not follow the theme over ssh, and that is deliberate. A headless host has no system theme, so there is nothing to detect there. A remote session's colours come from the terminal, which is local. Writing config files on the remote is the wrong shape: those files are machine-global while a theme is per-session, so two clients in different modes cannot both be right.
The answer is to let remote tools defer to the terminal's own ANSI palette:
export BAT_THEME=ansi
export FZF_DEFAULT_OPTS='--color=16'That follows the local terminal automatically, per session, and needs nothing installed on the server.
darkmanruns scripts on theme changes, on Linux only.dark-notifydoes the same, on macOS only.- Auto Dark Mode and PowerToys Light Switch set the Windows theme on a schedule rather than react to it.
Nothing covers WSL.
MIT or Apache-2.0, at your option.
