displayed is a small macOS display CLI for replacing the specific
BetterDisplay workflow of disconnecting the MacBook built-in display when a
target external monitor is present.
The current implementation uses displayplacer for visible display state and
for disabling detected displays. It uses a native private-API restore path for
bringing the built-in display back after it disappears from the normal
CoreGraphics/displayplacer online list.
For regular use, install through Cargo:
cargo install --path . --forceThis installs displayed under Cargo's bin directory, normally
~/.cargo/bin/displayed. It does not require sudo when Rust is installed for
the current user.
For local development, build the release binary and symlink it into a user-local PATH directory:
cargo build --release
ln -sfn "$PWD/target/release/displayed" ~/.local/bin/displayedThis keeps the installed command pointed at the repo's release build, so a later
cargo build --release updates the command immediately. On macOS, ~/.local/bin
is not always in PATH by default; use this option only if your shell already
includes it or you explicitly add it.
For multi-user/system-wide use:
cargo build --release
sudo cp target/release/displayed /usr/local/bin/displayedThis changes a shared system path and should not be the default for personal development.
After installation, run:
displayedRunning without arguments opens interactive mode.
Runtime state is stored in ~/Library/Application Support/displayed/, not in
the current working directory.
Remove the command according to how it was installed:
cargo uninstall displayed
rm ~/.local/bin/displayed
sudo rm /usr/local/bin/displayedcargo run --
cargo run -- list
cargo run -- snapshot --label before
cargo run -- disable-internal --when-serial s123456 --dry-run
cargo run -- enable-internal --id 37D8832A-2D66-02CA-B9F7-8F30A301B230 --dry-run
cargo run -- restore-internal --display-id 1 --dry-run
cargo run -- restore-display --display-id 2 --dry-run
cargo run -- redetect-displays --dry-run
cargo run -- safe-reset --dry-run
cargo run -- targets
cargo run -- guard --dry-run
cargo run -- install-guard --dry-run
cargo run -- uninstall-guard --dry-run
cargo run -- watch --dry-run
cargo run -- watch --target-serial s123456 --interval 60 --dry-runRemove --dry-run only after the generated displayplacer command looks right.
When the built-in display is already disconnected, watch should stay quiet
instead of treating the remaining external display as an error.
displayedInteractive mode shows the current detected displays, including whether the built-in display is currently visible. It uses a keyboard TUI:
up/downorj/k: select a detected display.spaceorenter: toggle the selected display. If macOS removes the disabled display from the live display list,displayedkeeps it as a rememberedoffrow and uses native restore when you toggle it back on.i: toggle the internal display. If the internal display is absent, this runs the native restore path.a: save/remove the selected external display as an auto target.s: safe reset.r: refresh.?: show an in-app help screen describing every key.q: quit.
Auto targets are stored under
~/Library/Application Support/displayed/targets using monitor identity data:
serial ID first, persistent ID and contextual display ID as fallbacks. Saved
auto targets are merged into the interactive list even when disconnected, so you
can select a disconnected target and press a to remove it. Display list numbers
are only for current UI selection and are not used as the watch identity.
While interactive mode is open, saved auto targets are always watched. If a
saved target is connected, displayed automatically disables the internal
display. If no saved target is connected and the internal display is not
enabled, displayed requests the native internal restore path so unplugging the
external monitor does not leave the Mac without an active display. The watch
path registers a CoreGraphics display reconfiguration callback and also performs
a periodic reconcile. Interactive mode also applies this no-enabled-display
safety restore even when no auto target is saved. When CoreGraphics reports a
non-internal display remove/disable event, interactive mode runs the safety
restore before trusting the latest displayplacer state because that state can
be stale during hot unplug. A remembered internal display disable event is not a
restore trigger, so auto-disabling the built-in display cannot loop against the
safety restore. Interactive mode reconciles every 5 seconds; non-interactive
watch uses the requested interval, defaulting to 60 seconds.
displayed guard is a one-shot safety check. It never disables the internal
display and never rearranges displays. It restores the internal display only when
the MacBook lid is open, no external display is present on the desktop, and the
built-in itself is absent. If the lid is closed, it stays idle so normal clamshell
use is not disturbed. If it cannot confirm any panel, it fails open by restoring
the internal display, so a confused or unreadable system still recovers a usable
screen.
The decision reads the authoritative CoreGraphics online display list through
a freshly spawned probe ("which panels are present on the desktop?"), not
displayplacer enabled flags and not CGDisplayIsActive. Each choice is
deliberate:
- Fresh probe, not the in-process snapshot. A long-lived process's cached CoreGraphics view can keep listing a panel that an unplug-during-sleep already dropped, a false positive that once vetoed every restore. A freshly spawned child queries WindowServer anew, which avoids that per-process cache. WindowServer itself can still retain a pre-sleep phantom until another hotplug; the lid-wake recovery described below handles that narrower case without changing periodic idle behavior.
- Online (present), not active (lit). An external in idle DPMS display-sleep
is still online but reports
CGDisplayIsActive = false; an active-based check would therefore decide "no external" and re-enable the built-in every 30s during idle, fighting the watcher and heating the Mac. Online is true for a connected panel whether lit or display-asleep, and false only when the panel is unplugged or intentionally disabled (the auto-off built-in leaves the list). - Not
displayplacer. The decision no longer depends ondisplayplacerbeing on the guard'sPATH, whose absence once made the guard restore every 30s and fight the watcher into a flicker.
For periodic reconciliation, "is a panel present?" is answered directly and freshly so intentional auto-off / idle display-sleep does not cause repeated restore attempts. Lid wake has a stricter one-shot active-panel check because a pre-sleep phantom can otherwise make an unusable layout look present.
The long-running watcher's restore path shares this signal: it will not restore the built-in while an external is present on the desktop, so a display-asleep external no longer makes the watcher spin failing enables against the intentionally-disabled built-in during idle. A genuine unplug still removes the external from the online list, so the built-in is restored as before.
displayed guardWhen interactive displayed is open, that same process is the only long-running
watcher. It already watches CoreGraphics display changes and now also registers
for macOS system power notifications. On SystemWillSleep, it records a pending
wake restore under ~/Library/Application Support/displayed/ and acknowledges
sleep immediately. It does not run a synchronous private display transaction in
the sleep callback because lid-close can block that transaction for about ten
seconds and still reject it.
After SystemHasPoweredOn, a real lid wake gets a short grace period for the
built-in panel to become active. If it remains inactive, displayed restores it
even when the previous saved external is still reported as present; that stale
report can survive deep sleep until another hotplug. Normal non-lid wake and the
periodic guard keep the online-presence policy above, so an external in ordinary
DPMS sleep does not cause repeated internal-display flicker. The pending marker is
cleared only after an active built-in is verified. A separate post-wake hold keeps
the auto watcher from immediately undoing that recovery based on the same dark
external phantom; the hold is released when the saved target is observed active or
absent.
displayedFor crash/stuck recovery when the interactive process is not running or is not responding, install the launchd one-shot guard:
displayed install-guardThis is optional and is never installed automatically. If displayed is always
kept running from a terminal, the terminal process already provides the primary
watcher and power notifications; install the guard only if the extra crash/stuck
fallback is wanted.
This writes one user LaunchAgent:
~/Library/LaunchAgents/com.stargt.displayed.guard.plist
It runs displayed guard --reason launchd --quiet at load and every 30 seconds.
It is not a kept-alive monitoring process; each run exits after the one-shot
check. This still gives recovery coverage if the interactive TUI or watch
exits, crashes, or gets stuck. If a pending wake marker remains after a crash,
the first lid-open guard run follows the same active-panel recovery path.
The plist bakes a PATH into EnvironmentVariables covering Homebrew
(/opt/homebrew/bin, /usr/local/bin), the install-time PATH, and standard
system directories. launchd otherwise runs agents with a minimal PATH that omits
Homebrew, so the guard could not find displayplacer and would misread display
state. Reinstall with install-guard after moving displayplacer or changing
shells so the baked-in PATH stays correct.
This is a plain user LaunchAgent, not a packaged macOS Login Item. It may not
appear in System Settings -> Login Items & Extensions. Manage it with the
install-guard and uninstall-guard commands, or by inspecting
~/Library/LaunchAgents/com.stargt.displayed.guard.plist directly.
Uninstall the launchd guard with:
displayed uninstall-guardThe launchd guard path is restore-only, so it preserves the intended auto-target behavior: saved external targets may still disable the internal display when they are definitely present, but a MacBook opened without a usable external display is forced back to an enabled internal display. This cannot cover pre-login/FileVault or OS/hardware failures before the user's LaunchAgents are allowed to run.
From interactive mode, select an external display and press a. Interactive
mode starts watching saved targets immediately. To run the same behavior without
the TUI, use:
displayed watchSaved targets can be inspected or cleared:
displayed targets
displayed targets --cleardisplayed safe-resetSafe reset only performs enabling/re-detection work:
SLSDetectDisplays()CGSConfigureDisplayEnabled(display_id:<remembered-display-id>, enabled:true)for every remembered contextual display IDdisplayplacer "id:<detected-disabled-display> enabled:true"for any detected disabled displays
It does not intentionally disable or rearrange displays.
The useful comparison is not just display brightness. We need the before/after
state of CoreGraphics, IOKit, displayplacer, and BetterDisplay preferences.
-
Start with BetterDisplay running and the external monitor connected.
-
Capture the pre-change state:
cargo run -- snapshot --label before-internal-disconnect
-
In BetterDisplay, apply the setting that disconnects only the built-in MacBook display.
-
Capture the post-change state:
cargo run -- snapshot --label after-internal-disconnect
-
If you re-enable the built-in display, capture that too:
cargo run -- snapshot --label after-internal-reconnect
-
Compare the files:
diff -u snapshots/<before>.txt snapshots/<after>.txt
Snapshot files can include monitor serial numbers, EDID data, and BetterDisplay
license metadata. snapshots/ is intentionally gitignored.
User-run captures from the visible terminal confirmed the BetterDisplay state transition:
- Before disconnecting the built-in display:
- Built-in
Color LCDwas present indisplayplacer listas a MacBook built in screen, enabled, and main atorigin:(0,0). - External
LG FHDwas present, enabled, and positioned atorigin:(-66,-1080).
- Built-in
- After BetterDisplay disconnected the built-in display:
- Built-in
Color LCDdisappeared from the CoreGraphics/displayplacer online list. - External
LG FHDremained enabled and became the main display atorigin:(0,0). - BetterDisplay preferences changed
appDisconnected@Display:2from false to true andconnected@Display:2from true to false.
- Built-in
This means disabling the built-in display can use the same surface that
displayplacer already exposes:
displayplacer "id:<built-in-persistent-id> enabled:false"For the captured setup, the external serial was s673744. The direct watch
form remains available:
cargo run -- watch --target-serial s673744 --interval 60 --dry-runThe preferred flow is to run displayed, select the external display, press
a, then use displayed watch or press w inside the TUI. That stores monitor
identity information instead of relying on the current display list index.
The BetterDisplay-free restore candidate is a native private-API sequence:
SLSDetectDisplays()
CGSConfigureDisplayEnabled(display_id:<built-in-display-id>, enabled:true)For the captured setup, the built-in display's display ID/contextual ID was 1:
cargo run -- restore-internal --display-id 1The same native restore primitive can restore any remembered display ID. For the
captured external display, the contextual display ID was 2:
cargo run -- restore-display --display-id 2If the built-in display is currently visible, running list first records its
contextual display ID in ~/Library/Application Support/displayed/state:
cargo run -- list
cargo run -- restore-internalredetect-displays runs only the SkyLight rediscovery call:
cargo run -- redetect-displaysThe older displayplacer restore attempt is still available via
enable-internal, but it can fail with Unable to find screen ... once the
built-in display is fully absent from the CoreGraphics/displayplacer online
list:
cargo run -- enable-internal --id 37D8832A-2D66-02CA-B9F7-8F30A301B230This native restore path is experimental. It is based on BetterDisplay's local
binary imports and disassembly: BetterDisplay imports SLSDetectDisplays,
CGSConfigureDisplayEnabled, and calls SLSDetectDisplays() with no arguments
inside its reconnect flow.
One caveat: Codex-run processes in this environment still saw an empty CoreGraphics display list, while user-run terminal commands saw the real display state. Do final validation from the interactive terminal that is visibly attached to the monitor.