A tiny macOS menu bar app that controls the brightness of an external monitor over DDC/CI, and the built-in display natively. Written in Swift and AppKit, with no SwiftUI, no dependencies, and no Xcode required to build.
| Light | Dark |
|---|---|
![]() |
![]() |
macOS will not let the brightness keys touch an external monitor. Existing tools
solve that, but carry more than they need to. Measured on the same machine with
vmmap --summary (physical footprint, idle):
| Footprint | Idle CPU | |
|---|---|---|
| Lumen | 11.4 MB | 0.0% |
| MonitorControl 4.3.3 | 24.2 MB | — |
The difference is not the language — only ~3 MB of that is app heap; the rest is the AppKit floor, which any language pays. It comes from what is not linked: no SwiftUI, no auto-updater, and no embedded Swift runtime (the OS copy is used instead of shipping back-deployment dylibs, which would otherwise become private dirty pages).
- Brightness, contrast and volume for external displays over DDC/CI
- Dims below the panel's hardware floor. DDC luminance 0 is the minimum backlight, not black, so the bottom of the brightness slider scales the gamma ramp instead of the backlight
- Brightness for the built-in panel via
DisplayServices - Every connected display in one popover, each with its own transport
- F1 / F2 adjust the display under the cursor
- Handles both brightness key forms: Apple media keys and plain F1/F2
- Click, drag, or scroll on any slider
- Menu bar glyph tracks brightness — Lumen consumes the key event, so Apple's HUD never appears and the glyph is the feedback
- Open at Login, via
SMAppService - Rebuilds in about two seconds
- Apple Silicon Mac, macOS 13 or later
- Command Line Tools (
xcode-select --install) — full Xcode is not needed - The monitor connected over DisplayPort or USB-C. HDMI cannot carry DDC/CI on Apple Silicon.
./scripts/make-signing-identity.sh # once, see "Signing" below
./build.sh
open Lumen.appThe brightness keys need two separate grants. Both are one-time.
- Accessibility — System Settings → Privacy & Security → Accessibility
- Input Monitoring — System Settings → Privacy & Security → Input Monitoring
Accessibility alone is not enough. Without Input Monitoring, macOS silently strips the keyboard bits from the event tap mask and reports no error, so plain F1/F2 never arrive. Lumen detects this and says so in the popover.
Check the live state any time:
cat /tmp/lumen-state.txttrusted=true
tapActive=true
grantedMask=0x4c00 # bits 10, 11, 14 = keyDown + keyUp + systemDefined
keyboardEvents=true
A grantedMask of 0x4000 means Input Monitoring is missing.
Grant the app bundle, not the executable. TCC keys these grants to the bundle
identity: launching Lumen.app yields 0x4c00, while running
Lumen.app/Contents/MacOS/Lumen directly yields 0x4000.
build.sh signs with a local self-signed certificate, and this is not cosmetic.
An ad-hoc signature's designated requirement is the binary's cdhash:
ad-hoc: cdhash H"73c96956..." # new every build
certificate: identifier "local.lumen" and certificate leaf H"c6846..." # stable
With an ad-hoc signature, every rebuild looks like a different app to macOS and
silently revokes both grants. scripts/make-signing-identity.sh creates a
certificate-based identity so the requirement is stable and permissions survive
rebuilds. The certificate does not need to be in the trust store — it only needs
to be in an unlocked keychain that is on the search list.
If no identity is found, build.sh falls back to ad-hoc and warns.
| Tool | What it proves |
|---|---|
./selftest |
DDC read/write and both display paths, with a visible pulse |
./tapdump |
The event tap parses brightness keys |
./whichdisplay |
Which display a cursor position resolves to |
./keyspy |
Logs every key and media event to /tmp/lumen-keyspy.log |
./dimtest |
Reads the gamma ramp back to prove software dimming engages |
LUMEN_DEBUG=1 traces tap events to /tmp/lumen.log. For a bundle launch, set it
session-wide first, because open does not pass the environment through:
launchctl setenv LUMEN_DEBUG 1 # launchctl unsetenv LUMEN_DEBUG to clearRender the popover to PNGs without touching the menu bar:
LUMEN_PREVIEW=1 ./Lumen.app/Contents/MacOS/Lumen
# writes /tmp/lumen-ui-light.png and /tmp/lumen-ui-dark.pngExternal displays. Apple Silicon replaced IOFramebuffer with
IOMobileFramebuffer, which exposes no I²C bus, so the classic
IOFBCopyI2CInterfaceForBus route is dead. DDC/CI instead goes through four
private IOKit symbols that appear in no public header:
IOAVServiceCreate IOAVServiceCreateWithService
IOAVServiceReadI2C IOAVServiceWriteI2C
They are plain C functions, resolved with dlsym rather than linked, so a macOS
release that drops them yields a "DDC unavailable" message instead of a launch
crash.
Built-in display. DisplayServicesGetBrightness / SetBrightness, so it
changes the real backlight rather than dimming with a gamma overlay.
Keys. A CGEventTap on .cgSessionEventTap watching keyDown, keyUp and
systemDefined. Apple's internal keyboard sends brightness as NX_SYSDEFINED
media keys (subtype 8, key codes 2 and 3); many external keyboards send plain F1
and F2 instead. Modified presses (⌘F1, fn+F1) pass through untouched. The tap
re-enables itself after tapDisabledByTimeout, which is otherwise a silent death.
Below the hardware floor. A monitor's DDC luminance 0 is its minimum
backlight, which on many panels is still bright. The brightness slider is one
continuous 0-100 scale split in two: above 15% it maps to DDC luminance, and
below 15% it pins DDC at 0 and scales the display's gamma ramp down to 0.15 via
CGSetDisplayTransferByFormula. Measured on a DELL P3223QE:
| Slider | gammaMax |
|---|---|
| 100% / 50% / 15% | 1.000 |
| 10% | 0.717 |
| 0% | 0.150 |
Gamma is chosen over a black overlay window because it does not intercept clicks and does not appear in screenshots. It outlives the process that set it, so Lumen restores it on quit and also clears it at launch, which makes a crash self-healing. Software dimming applies to external displays only — the built-in panel's native minimum is already dark.
Reads are advisory. DDC writes are reliable; reads are not. Lumen validates
the get-VCP reply header ([2]==0x02, [3]==0x00, [4]==vcp) and rejects anything
malformed, then falls back to the persisted value. Before that check, misaligned
frames looked plausible — one panel returned 96, 110 and 100 for an unchanged
value, and a bogus 110/128 maximum. Never build UI that trusts a DDC read.
Slider drags coalesce onto one serial queue, latest-value-wins, so a drag cannot queue minutes of stale traffic at ~20 ms of bus time per write.
- One external display.
IOAVServiceCreatebinds the default external monitor; a second would need a per-display IORegistry walk. - Some panels never report brightness. A DELL P3223QE accepts writes but returns no valid brightness or contrast read at any settle time up to 50 ms (volume reads fine). Sliders then start from the last value you set.
- Gamma dimming reduces colour depth. Below 15% the ramp is compressed, so gradients can band. That is the trade for going darker than the panel allows.
- No on-screen HUD. Lumen consumes the key event to stop macOS double-handling it, so the menu bar glyph is the only feedback.
- Private APIs mean this is not App Store eligible, and Apple can break it.
Verified on an M3 Pro with a DELL P3223QE over USB-C: menu bar app, popover, DDC writes, built-in brightness, display hot-plug, and Apple media keys.
The plain-F1/F2 path is implemented and the event tap is confirmed to receive keyboard events, but it has not yet been confirmed against external keyboard hardware. Synthetic key injection is not a valid test here — posted events are rewritten in transit.
- m1ddc (MIT) — the reference for the
Apple Silicon DDC/CI protocol and
IOAVServicediscovery - MonitorControl — prior art
- The journey to controlling external monitors on M1 Macs
MIT — see LICENSE.

