Light / dark, resolved

One theme.
Everywhere you work.

A local daemon resolves one effective theme from your OS, an offline sun schedule, or a manual override—then broadcasts it to your terminal, tray, and Neovim.

macOSLinuxWindowsNeovim
daemon — session
$ theme-agent status
mode        auto
effective   dark   (source: sun)
system      unavailable
next        07:42 sunrise → light

$ theme-agent subscribe --json
{"event":"theme_changed","effective":"light","previous":"dark","source":"system","mode":"auto","timestamp":"2026-06-15T22:08:29Z"}

Built on local signals

Push, not poll

macOS notifications, the XDG portal, and a Windows registry watch react when the OS changes.

Offline sun math

NOAA equations, DST-aware local time, and explicit support for polar day and polar night.

One daemon, many clients

CLI, tray, and Neovim stay in sync over per-user, local NDJSON IPC.

User-level service

launchd, systemd --user, or a logon task. It starts at login and never needs admin.

Five modes. One answer.

A fixed priority turns every input into light or dark: manual > system > sun > fallback.

auto

Tries the OS preference, then offline sun calculation, then fallback.

theme-agent set auto
manualsystemsunfallback
London · 51.5072, −0.1276
sunrise 07:42sunset 18:03switch elevation · configurable −18° … 18°elevation −0.833°

Sun mode · offline

Sunrise, computed.
Never fetched.

The daemon uses hand-rolled NOAA solar equations. jiff keeps the calculation timezone- and DST-correct at any longitude.

Polar day and polar night are first-class states. Coordinates come only from your config; no network or geolocation lookup is involved.

[location]
latitude = 51.5072
longitude = -0.1276
[sun]
elevation = -6   # civil twilight
Sleep cap 6hWake guard 60sElevation −18°…18°

Everything is a client.

The daemon owns the answer. Every surface subscribes to the same local state.

Editor

Neovim follows live changes

require("theme_agent").setup({
  -- colorschemes = { light = "dawnfox", dark = "nightfox" },
})
:ThemeAgentStatus:ThemeAgentSet:ThemeAgentSubscribe:ThemeAgentUnsubscribe:ThemeAgentReload

Desktop

Native tray, same modes

theme-agent

  • Light
  • Dark
  • System
  • Sun
  • Auto
  • Edit config…
  • Reload
  • Quit

Hooks

Transitions become actions

[hooks]
on_start = ["notify-send", "Theme", "{theme}"]

argv only — no shell, ever

IPC

Local events, pushed

{"event":"daemon_ready","effective":"dark","mode":"auto"}
{"event":"theme_changed","effective":"light","previous":"dark","source":"system","mode":"auto","timestamp":"2026-06-15T22:08:29Z"}
{"event":"config_changed","timestamp":"2026-06-15T22:08:30Z"}

Socket 0600. Per-user only. No TCP.

Get started.

Install

$ rustup update stable
$ cargo +stable install --locked --git https://github.com/Ajaymamtora/theme-agent theme-agent
Requires Rust 1.88+.

Register the service

$ theme-agent service install
User-level. Starts now and at login. Safe to re-run on every upgrade.

Headless

$ cargo +stable install --locked --no-default-features --git https://github.com/Ajaymamtora/theme-agent theme-agent
Skips the native tray and its GTK dependencies.

First run

$ theme-agent status
$ theme-agent set sun
$ theme-agent subscribe