Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

displayed

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.

Install

Normal local install

For regular use, install through Cargo:

cargo install --path . --force

This installs displayed under Cargo's bin directory, normally ~/.cargo/bin/displayed. It does not require sudo when Rust is installed for the current user.

Development symlink

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/displayed

This 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.

System-wide install

For multi-user/system-wide use:

cargo build --release
sudo cp target/release/displayed /usr/local/bin/displayed

This changes a shared system path and should not be the default for personal development.

After installation, run:

displayed

Running without arguments opens interactive mode.

Runtime state is stored in ~/Library/Application Support/displayed/, not in the current working directory.

Uninstall

Remove the command according to how it was installed:

cargo uninstall displayed
rm ~/.local/bin/displayed
sudo rm /usr/local/bin/displayed

Commands

cargo 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-run

Remove --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.

Interactive Mode

displayed

Interactive mode shows the current detected displays, including whether the built-in display is currently visible. It uses a keyboard TUI:

  • up/down or j/k: select a detected display.
  • space or enter: toggle the selected display. If macOS removes the disabled display from the live display list, displayed keeps it as a remembered off row 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.

Internal Display Guard

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 on displayplacer being on the guard's PATH, 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 guard

When 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.

displayed

For crash/stuck recovery when the interactive process is not running or is not responding, install the launchd one-shot guard:

displayed install-guard

This 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-guard

The 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.

Auto Targets

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 watch

Saved targets can be inspected or cleared:

displayed targets
displayed targets --clear

Safe Reset

displayed safe-reset

Safe reset only performs enabling/re-detection work:

  1. SLSDetectDisplays()
  2. CGSConfigureDisplayEnabled(display_id:<remembered-display-id>, enabled:true) for every remembered contextual display ID
  3. displayplacer "id:<detected-disabled-display> enabled:true" for any detected disabled displays

It does not intentionally disable or rearrange displays.

Investigation Protocol

The useful comparison is not just display brightness. We need the before/after state of CoreGraphics, IOKit, displayplacer, and BetterDisplay preferences.

  1. Start with BetterDisplay running and the external monitor connected.

  2. Capture the pre-change state:

    cargo run -- snapshot --label before-internal-disconnect
  3. In BetterDisplay, apply the setting that disconnects only the built-in MacBook display.

  4. Capture the post-change state:

    cargo run -- snapshot --label after-internal-disconnect
  5. If you re-enable the built-in display, capture that too:

    cargo run -- snapshot --label after-internal-reconnect
  6. Compare the files:

    diff -u snapshots/<before>.txt snapshots/<after>.txt

Privacy

Snapshot files can include monitor serial numbers, EDID data, and BetterDisplay license metadata. snapshots/ is intentionally gitignored.

Current Finding

User-run captures from the visible terminal confirmed the BetterDisplay state transition:

  • Before disconnecting the built-in display:
    • Built-in Color LCD was present in displayplacer list as a MacBook built in screen, enabled, and main at origin:(0,0).
    • External LG FHD was present, enabled, and positioned at origin:(-66,-1080).
  • After BetterDisplay disconnected the built-in display:
    • Built-in Color LCD disappeared from the CoreGraphics/displayplacer online list.
    • External LG FHD remained enabled and became the main display at origin:(0,0).
    • BetterDisplay preferences changed appDisconnected@Display:2 from false to true and connected@Display:2 from true to false.

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-run

The 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.

Restore Built-In Display

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 1

The 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 2

If 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-internal

redetect-displays runs only the SkyLight rediscovery call:

cargo run -- redetect-displays

The 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-8F30A301B230

This 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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages