Blank OLED screens when the room empties. Wake them the moment you return.
OLED panels burn in when they hold a static image. OS idle timers are a blunt fix — they blank while you're reading and linger after you leave. Presence sensors know the difference: they can tell an empty room from a still one.
dormant is a Rust daemon that reads those sensors and blanks your displays only when the room is actually empty, then wakes them the instant someone walks back in. It runs your PC monitors and your TVs, over local buses and over the network, on rules you write per display.
- Blank displays when a room is empty instead of on a fixed idle timer — driven by MQTT, Home Assistant WebSocket, or USB mmWave radar sensors
- Blank without killing audio — DDC/CI, Samsung Tizen, and gamma-table paths leave the OS output intact
- Soft KVM — two machines, one monitor; pull the panel by hotkey, CLI, tray, or web.
- Escalate through a render ladder: black Wayland overlay → muted mpv screensaver → power-off, on configurable dwell timers
- Track brightness-weighted panel on-hours per display, stored locally — no telemetry
- Wear-even screensavers — order media and bias bounded pixel shifts toward cooler estimated panel regions
- Active wear sampling — opt in to content-weighted OLED wear tracking from the KDE Wayland compositor.
- Configure and control through a loopback web dashboard with a validated config editor that preserves comments
- Config section rail — sticky per-tab section navigation with scroll-spy in the web UI
- Web UI v3 — full config-schema parity in the web editor (coordination, keymaps, input filters, hooks), a new shared-display switching view with live pull/push feedback, event history across reloads, doctor checks grouped by subject, panel-wear detail split by seeded/measured time, and a security-posture card with honest boundary notes.
- Drive the daemon from a desktop tray with global hotkeys (Linux KDE · macOS menu bar)
- Probe every sensor and display against live hardware with
dormantctl doctor - Recover from a bad config boot: automatic last-known-good rollback, a systemd watchdog, and
dormantctl emergency-wake - MQTT state publishing — opt-in retained state + Home Assistant discovery for sensors, zones, and displays
Protecting OLEDs without the usual trade-offs — no black bars burned into the panel, no audio cutting out when the TV screen goes dark, no three-second wait to see your desktop again. Every blank mode makes a different bargain:
| Mode | OLED protection | Audio survives | Wake |
|---|---|---|---|
screen_off_audio_on |
Full | Yes | Fast |
power_off |
Full | No | ~1s standby |
brightness_zero |
Partial — pixels stay lit | Yes | Instant |
Three display paths leave the OS output alone, so audio keeps playing:
ddccipower_offwrites VCP0xD6straight to the monitor's internal controller. The panel blanks; the OS output — and its audio sink — stay up.samsung-tizenscreen_off_audio_onsendsKEY_PICTURE_OFFover WebSocket. The TV cuts its backlight; the HDMI source keeps running and the audio plays on.samsung-tizenbrightness_zerodrives the TV's backlight to zero over Samsung IP Control G2 on port 1516 — a near-black dim rather than a true off, but the only mode that leaves the source's audio uninterrupted on a 2024 Samsung OLED.
Every DPMS path (kwin-dpms included) tears the output down and takes the audio with it, which is why it's a documented fallback, not a default.
Three binaries: dormantd (the daemon), dormantctl (the CLI), and dormant-tray (a desktop tray applet).
| Source | |
|---|---|
| MQTT — Zigbee2MQTT, ESPHome, any broker (auth supported) | Ready |
| Home Assistant WebSocket | Ready |
| USB-serial LD2410 mmWave radar | Ready |
Zones fuse multiple sensors with any / all / quorum / weighted logic. A sensor that goes quiet — broker down, USB unplugged, a stale reading — resolves as present, never absent. dormant will not blank a room it can't see.
dormantctl doctor checks the configured sensors and displays. --report-issue
and --draft-feature turn the full offline probe run into a ready-to-file
GitHub issue draft, with known config and credential values redacted.
| Controller | |
|---|---|
ddcci — DDC/CI power-off and brightness (audio-safe, Linux and macOS) |
Ready |
samsung-tizen — Samsung Tizen TVs over WebSocket |
Ready |
kwin-dpms — KDE Wayland DPMS (audio-unsafe fallback, Linux only) |
Ready |
ha-passthrough — any Home Assistant service call |
Ready |
command — arbitrary blank/wake shell commands |
Ready |
macos-gamma-black — Quartz gamma-table black (audio-safe, macOS only) |
Ready |
macos-display-sleep — pmset display sleep (whole-machine fallback, macOS only) |
Ready |
Each display gets an ordered controller chain with automatic fallback and bounded wake retry. A wake that fails on one controller escalates to the next. Repeated failures surface through desktop notifications, the tray, and the web dashboard.
A display referenced by no rule is manual-only: the daemon builds it, dormantctl status / the web UI / the tray show it, and it responds to hand-issued blank / wake commands — but no zone or rule ever drives it. That lets a TV join dormant without a keep-awake dummy zone.
dormant tracks brightness-weighted on-hours per display and shows them in the web dashboard as a panel-exposure card. An advisory appears after a long stretch without a rest window. Tracking does not change blank/wake timing. Ledgers stay under the daemon's local state directory; there is no telemetry. See Panel-wear tracking for the limits and the wear.* keys.
Build with --features web-ui for a loopback web dashboard: a live view of the sensor → zone → rule → display pipeline, force blank/wake, pause/resume, panel-wear tracking, failure state, and a config editor. The editor can create and delete sensors, zones, displays, and rules; its Samsung pairing wizard stores the granted token in credentials.toml. Config writes preserve comments and pass daemon-identical validation before they reach disk.
Build with --features render for blanking that never touches DPMS: a fullscreen black Wayland overlay, an escalation ladder (screensaver → black → power-off on your own dwell timers), and a muted streaming screensaver driven by mpv. The screensaver applies a 2 px pixel shift every 2 minutes by default; the uniform black overlay never shifts.
dormant-tray puts per-display status and blank/wake/pause controls in the KDE StatusNotifierItem tray on Linux and a native NSStatusItem menu-bar item on macOS. See the installation guide for systemd and launchd autostart.
Install through the channel that matches your platform:
Homebrew (macOS or Linuxbrew):
brew install legion-works/tap/dormantd
brew install legion-works/tap/dormantctl
brew install legion-works/tap/dormant-trayArch Linux (AUR):
yay -S dormant-binThe AUR package includes the pre-built binaries and systemd user units.
Installer scripts (Linux x86_64 / aarch64, macOS arm64 / x86_64):
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/legion-works/dormant/releases/download/v0.4.0/dormantd-installer.sh | sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/legion-works/dormant/releases/download/v0.4.0/dormantctl-installer.sh | shThe dormant-tray-installer.sh script is in the same release directory.
Checksums are published alongside every artifact; verify with
sha256sum -c <artifact>.sha256.
Release tarballs include the matching systemd user units on Linux. Manual installation and source builds are covered in the installation guide.
On macOS, install the per-user LaunchAgents for the daemon and tray, then
start them — see Installation: LaunchAgent
(macOS):
dormantctl launchd install
launchctl bootstrap gui/$UID "$HOME/Library/LaunchAgents/com.legionworks.dormant.plist"
launchctl bootstrap gui/$UID "$HOME/Library/LaunchAgents/com.legionworks.dormant-tray.plist"git clone https://github.com/legion-works/dormant.git
cd dormant
sudo apt install libudev-dev libwayland-dev libmpv-dev pkg-config
cargo build --release --features web-ui,render
install -Dm755 target/release/dormantd ~/.local/bin/dormantd
install -Dm755 target/release/dormantctl ~/.local/bin/dormantctl
install -Dm755 target/release/dormant-tray ~/.local/bin/dormant-trayWrite ~/.config/dormant/config.toml:
config_version = 1
[sensors.desk]
type = "mqtt"
broker_url = "mqtt://localhost:1883"
topic = "zigbee2mqtt/desk-presence"
[zones.office]
mode = "any"
members = ["desk"]
[displays.monitor]
controllers = ["ddcci"]
blank_mode = "power_off"
[rules.office]
zone = "office"
displays = ["monitor"]
grace_period = "60s"Then:
dormantctl validate # check the config
dormantd # start the daemon
dormantctl status # watch the pipeline
dormantctl doctor # diagnose sensors and displays against your hardware
dormantctl doctor exercise monitor # verify a real blank → wake control pathRun it as a user service:
mkdir -p ~/.config/systemd/user
cp crates/dormantd/systemd/dormant.service ~/.config/systemd/user/
systemctl --user enable --now dormantThe rendered book for the latest release lives at legion-works.github.io/dormant. Configuration reference, sensor and controller guides, and the doctor command are also in docs/src/introduction.md. Hardware-specific findings — which DDC codes work, how Samsung standby behaves — live in docs/research/.
Running in production on the author's hardware — an AOC AGON OLED monitor and a Samsung S90D — driven by real Zigbee and mmWave presence sensors. CI covers the full workspace on Linux, macOS, and Windows; macOS (M1) is a genuinely supported target as of this milestone — DDC/CI, Quartz-gamma black, and pmset display-sleep controllers, a CoreGraphics idle source, launchd LaunchAgents, and a native menu-bar tray item — while Windows remains portability-only (no native display control). The daemon caps Tokio at two workers, calls malloc_trim after screensaver teardown, and sets MALLOC_ARENA_MAX=2 in the systemd unit (Linux). The shipped watchdog restarts a wedged engine on Linux via systemd's WatchdogSec; macOS has no equivalent wedged-daemon detection (see Installation: LaunchAgent (macOS)). Last-known-good rollback can recover a bad boot config on both platforms — it is supervisor-agnostic.
It's a young project with one maintainer, aimed at homelabs and single-operator setups; interfaces can still shift before 1.0, and the web dashboard binds to loopback with no authentication by design.
MIT OR Apache-2.0, at your option.


