Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Installation

Prerequisites

  • Linux (x86_64 or aarch64) with a desktop environment (X11 or Wayland), or macOS (arm64 or x86_64) — see macOS (M1) below for the macOS-specific install path
  • Rust 1.88+ (MSRV) if installing from source
  • Build dependencies for the full daemon (Linux only — macOS needs nothing beyond Xcode Command Line Tools): sudo apt install libudev-dev libwayland-dev libmpv-dev pkg-config
  • If pkg-config cannot find libudev, set PKG_CONFIG_PATH=/usr/lib/pkgconfig

Render backend

The software render backend (render_black, render_screensaver ladder stages) is off by default. The full build below enables it. For a smaller build, omit render and its libwayland-dev / libmpv-dev dependencies.

  • Build: add --features rendercargo build --release --features render
  • Dependencies: libwayland-dev, libmpv-dev, and pkg-config
  • Without the feature, configs using render stages are rejected at startup with E_RENDER_UNAVAILABLE; the daemon, CLI, and non-render sensors/displays still build and run normally.

Homebrew

The Legion Works tap publishes one formula per binary for macOS and Linuxbrew. Formulas are versioned with each release.

brew install legion-works/tap/dormantd
brew install legion-works/tap/dormantctl
brew install legion-works/tap/dormant-tray

Arch Linux (AUR)

dormant-bin installs pre-built x86_64 release binaries and the systemd user units under /usr/lib/systemd/user/.

yay -S dormant-bin

From source

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

Binaries land in ~/.local/bin/ — make sure this is on your PATH.

Tray applet

On Linux, dormant-tray is a KDE StatusNotifierItem applet. On macOS, it is a native NSStatusItem menu-bar item. Both provide status, pause/resume, and blank/wake controls through the daemon's Unix socket.

The dormant tray applet: status tooltip and pause/blank/wake menu

install -Dm755 target/release/dormant-tray ~/.local/bin/dormant-tray

See Tray autostart below to run it on every login.

From release

The cargo-dist pipeline publishes shell installers and tarballs for every binary. These URLs always resolve to the newest release; substitute download/vX.Y.Z for a specific one. Install on Linux x86_64 or aarch64:

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/legion-works/dormant/releases/latest/download/dormantd-installer.sh | sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/legion-works/dormant/releases/latest/download/dormantctl-installer.sh | sh

dormant-tray-installer.sh is also available in the same directory. Checksums are published alongside every artifact; verify with:

sha256sum -c dormantd-x86_64-unknown-linux-gnu.tar.xz.sha256
sha256sum -c dormantctl-x86_64-unknown-linux-gnu.tar.xz.sha256

Systemd user unit

dormant runs as a user service — it does not need root. The unit file (dormant.service) is in the release tarball (under dormantd-<triple>/systemd/dormant.service) and in the source tree at crates/dormantd/systemd/dormant.service.

The AUR package installs the unit under /usr/lib/systemd/user/ already. Use the manual paths below for source builds and release tarballs.

Install it:

mkdir -p ~/.config/systemd/user

# From source:
cp crates/dormantd/systemd/dormant.service ~/.config/systemd/user/

# From a release tarball:
tar -xf dormantd-x86_64-unknown-linux-gnu.tar.xz \
    dormantd-x86_64-unknown-linux-gnu/systemd/dormant.service
mv dormantd-x86_64-unknown-linux-gnu/systemd/dormant.service \
    ~/.config/systemd/user/dormant.service

# Then enable and start:
systemctl --user daemon-reload
systemctl --user enable --now dormant

Check status:

systemctl --user status dormant
journalctl --user -u dormant -f

The unit runs as Type=notify, restarts on failure, and uses a 150-second engine-liveness watchdog. Reload with systemctl --user reload dormant. To stop:

systemctl --user stop dormant

When upgrading from a unit that used Type=simple, install the new dormantd binary before copying or reloading the new unit. Then run systemctl --user daemon-reload and systemctl --user restart dormant. See Watchdog + last-known-good rollback.

Configuration file location

dormant reads config from these paths, first match wins:

  1. --config CLI flag
  2. $DORMANT_CONFIG environment variable
  3. $XDG_CONFIG_HOME/dormant/config.toml (usually ~/.config/dormant/config.toml)

MQTT credentials, HA tokens, and Samsung TV tokens go in a separate file with restricted permissions:

$XDG_CONFIG_HOME/dormant/credentials.toml

Set permissions to 600 — dormant will refuse to load a credentials file readable by others:

chmod 600 ~/.config/dormant/credentials.toml

Tray autostart

Linux (systemd)

Run dormant-tray on every graphical session with the provided user unit — the same mechanism as the daemon. The unit file (dormant-tray.service) ships in the release tarball under dormant-tray-<triple>/systemd/dormant-tray.service and in the source tree at crates/dormant-tray/systemd/dormant-tray.service.

mkdir -p ~/.config/systemd/user

# From source:
cp crates/dormant-tray/systemd/dormant-tray.service ~/.config/systemd/user/

# From a release tarball:
tar -xf dormant-tray-x86_64-unknown-linux-gnu.tar.xz \
    dormant-tray-x86_64-unknown-linux-gnu/systemd/dormant-tray.service
mv dormant-tray-x86_64-unknown-linux-gnu/systemd/dormant-tray.service \
    ~/.config/systemd/user/dormant-tray.service

# Then enable and start:
systemctl --user daemon-reload
systemctl --user enable --now dormant-tray

The unit uses ExecStart=%h/.local/bin/dormant-tray, so systemd expands the path from your home directory at launch — no reliance on PATH. It starts after dormant.service and restarts on failure. A plain XDG .desktop autostart does not work here: the systemd autostart generator resolves a relative Exec= against a minimal boot PATH that excludes ~/.local/bin, so no unit gets generated.

macOS (launchd)

dormantctl launchd install writes both checked-in LaunchAgent plists to ~/Library/LaunchAgents/: com.legionworks.dormant.plist for the daemon and com.legionworks.dormant-tray.plist for the tray. It does not bootstrap or start either job. Start them immediately, without waiting for the next login:

dormantctl launchd install
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.legionworks.dormant.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.legionworks.dormant-tray.plist

The tray agent runs only in an Aqua session. It starts at login, restarts after an unsuccessful exit, and throttles restarts to one per 10 seconds. Its stdout and stderr logs are ~/Library/Logs/dormant/dormant-tray.log and ~/Library/Logs/dormant/dormant-tray.err.log.

To remove the managed plist files, first boot out any loaded jobs, then run:

launchctl bootout gui/$(id -u)/com.legionworks.dormant
launchctl bootout gui/$(id -u)/com.legionworks.dormant-tray
dormantctl launchd uninstall

dormantctl launchd uninstall removes the two files only. It does not boot out loaded jobs, so launchd retains their last-loaded definitions until they are explicitly booted out.

macOS (M1)

dormant runs natively on macOS (arm64 and x86_64), no root required. This milestone (M1) ships:

  • DDC/CI display control (dormantctl doctor ddcci)
  • The macos-gamma-black and macos-display-sleep blank controllers
  • A CoreGraphics-based idle source
  • Read-only diagnostics: dormantctl doctor macos-idle, macos-display-sleep, macos-power

dormant-tray runs as a native NSStatusItem menu-bar item on macOS. See Tray autostart: macOS (launchd) to install and start its LaunchAgent with the daemon.

From release (macOS)

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/legion-works/dormant/releases/latest/download/dormantd-installer.sh | sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/legion-works/dormant/releases/latest/download/dormantctl-installer.sh | sh

Binaries land in ~/.local/bin/, same as Linux. The installer also prints the next step — it deliberately does not run dormantctl launchd install or launchctl bootstrap for you (that would start a background daemon before you have configured it).

LaunchAgent (macOS)

dormant ships per-user LaunchAgents — the macOS analog of the systemd user units above — as checked-in daemon and tray plists: crates/dormantd/share/com.legionworks.dormant.plist and crates/dormant-tray/share/com.legionworks.dormant-tray.plist. dormantctl launchd install embeds byte-identical copies at build time.

Install it (idempotent, non-root — writes only under your home directory):

dormantctl launchd install

This atomically copies both plists to their canonical paths under ~/Library/LaunchAgents/, mode 0644. It does not start either agent — bootstrap them explicitly:

launchctl bootstrap gui/$UID "$HOME/Library/LaunchAgents/com.legionworks.dormant.plist"
launchctl bootstrap gui/$UID "$HOME/Library/LaunchAgents/com.legionworks.dormant-tray.plist"

Check status and force an immediate (re)start:

launchctl print gui/$UID/com.legionworks.dormant
launchctl kickstart -k gui/$UID/com.legionworks.dormant

Reload config the same way as Linux — signal, not restart:

launchctl kill HUP gui/$UID/com.legionworks.dormant

Daemon logs land at ~/Library/Logs/dormant/dormantd.log (stdout) and dormantd.err.log (stderr). Tray logs are ~/Library/Logs/dormant/dormant-tray.log (stdout) and dormant-tray.err.log (stderr). Each LaunchAgent's ProgramArguments creates that directory and redirects into it before exec-ing the binary, so the shell never lingers as a supervisor.

To stop and remove:

launchctl bootout gui/$(id -u)/com.legionworks.dormant
launchctl bootout gui/$(id -u)/com.legionworks.dormant-tray
dormantctl launchd uninstall

launchd uninstall only removes the two canonical files. It does not boot out a still-loaded label, so run bootout first or launchd will keep the last-loaded definition in memory after the on-disk files are gone.

Lifecycle semantics (see the plist's own comments for the full rationale):

  • RunAtLoad=true + KeepAlive.SuccessfulExit=false: dormantd starts immediately on bootstrap/login and is relaunched whenever it exits, except a clean exit 0.
  • ThrottleInterval=10: launchd will not restart dormantd more than once every 10 seconds — the only restart-rate control here, this is a crash-loop rate limiter, not a liveness check.
  • No watchdog parity. Unlike the systemd unit's WatchdogSec=150 engine-liveness ping, launchd has no built-in mechanism to detect a dormantd that is running but wedged (hung without exiting). A wedged daemon on macOS is invisible to launchd and will not be restarted; dormantctl doctor exercise <display> and the emergency-wake path (see Troubleshooting) are the operator recourse there, independent of the supervisor.
  • Composition with boot-rollback. The crash-loop / last-known-good rollback logic is entirely supervisor-agnostic — it counts dormantd process starts sharing a config fingerprint within a sliding window from its own state files, not from systemd or launchd. So on macOS, launchd's 10-second ThrottleInterval paces the restarts the same way systemd's RestartSec=2 does on Linux, and the crash-loop counter behind it still trips and rolls back to the last-known-good config after repeated failed starts — the supervisor differs, the rollback guarantee does not.