Skip to content
Stickman803Public

About

A macOS menu bar app that kills the panel and keyboard backlight while the Mac stays awake, unlocked and screenshot-able. Private DisplayServices/CoreBrightness, an idle-gated poll, and the notes from reverse-engineering both.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Night

A tiny macOS menu bar app that turns the lights off without putting the Mac to sleep.

Three things, from one moon in the menu bar:

menu item what it does
Sleep Display pmset displaysleepnow. Screen off, session locked (if your lock grace period is "immediately"), SSH unaffected.
Keyboard Backlight Off Keys dark. Toggles back to the level it found.
Display Brightness Zero Panel dark, but the Mac stays awake and unlocked.
Go Dark Both of the above. Flips to Wake It Up once dark.
Open at Login On by default, registered on first launch only.

No dock icon, no app switcher entry, no preferences window. ~600 lines of Swift and Objective-C, no dependencies, builds with swiftc in about four seconds.

Going dark by any of those routes also switches auto-brightness off for whichever of the two it touched — see below for why it has to. Control comes back on Wake It Up, on the individual toggle, or on simply turning the brightness up yourself with F1/F2.

This is a finished side project, published because the reverse-engineering notes below are worth more than the app. It is not actively maintained.

Why "brightness zero" is not the same as "sleep display"

This is the item that makes the app worth having. DisplayServices is the framework the F1/F2 keys themselves go through, so setting 0 is exactly "held F1 until it stopped": the backlight goes off, but the display stays awake. The framebuffer is live, the session is not locked, and screencapture returns real pixels.

So an SSH session can still see and drive the screen overnight while the room stays dark — verified with a 3.7 MB screenshot taken at brightness 0.000. Sleeping the display instead locks the session and gets you nothing.

The way out if anything goes wrong is the brightness key. Night does not intercept F1/F2; the panel is simply at zero and one keypress brings it back. Quitting Night also restores everything on the way out, so quitting is never the thing that leaves you with a black panel.

Install

Requires macOS 13+ and the Xcode command line tools (xcode-select --install). Apple silicon and Intel both build; the private symbols were confirmed on Apple silicon.

git clone https://github.com/Stickman803/night.git
cd night
./build.sh                 # -> ~/Applications/Night.app, replacing a running copy
./build.sh /path/to/X.app  # somewhere else
open ~/Applications/Night.app

Or grab Night.app.zip from Releases — Apple silicon only, and ad-hoc signed rather than notarized, so the first launch needs a right-click → Open, or:

xattr -dr com.apple.quarantine ~/Applications/Night.app

Kbd.m is a separate cc pass — swiftc will not compile Objective-C — and arrives as a .o plus -import-objc-header. The ad-hoc codesign --identifier is not optional: without a stable identity, every rebuild looks like a new app to TCC and SMAppService, and the login registration is dropped without saying so.

The two things that cost the build

  1. The ambient sensor undoes you. Zeroing either backlight with auto-brightness still on just means the sensor walks it back up within a minute. Both paths turn auto off before zeroing, save the old flag, and put it back on restore.
  2. Restore order is auto first, level second. Backwards, re-enabling the sensor immediately overwrites the value just restored: a panel that was at 80% came back at 38% in a dark room, and a keyboard handed back a 0 stayed dark, because the sensor only tracks from wherever the level already is — it does not re-light on its own.

Pre-dark values are kept in UserDefaults, not just memory, so a crash or a reboot can still put things back.

Giving the sensor its job back when you reach for F2

Reaching for the brightness key is the habit, so it has to run the whole of Wake It Up — keyboard level back, auto-brightness back on both, menu and icon caught up — not just the half that concerns the panel. Otherwise auto-brightness stays switched off all day because the way out of the dark was a key rather than this menu.

There is no notification to hang that on — DisplayServicesBrightnessChanged does not exist on this build — so Night polls, and only while it is actually holding something down. Idle cost is nil: the timer does not exist the rest of the time.

Half a second, not two. The screen comes back the instant the key is pressed — that is the key doing it, Night never intercepts it — but the keyboard relighting is Night's doing, and at two seconds you would watch the keys trail the screen. Under half a second the two read as one event.

What a tick actually costs

It runs all night, so it was worth measuring rather than assuming — and the first guess of "a handful of dlsym'd calls, costs nothing" was wrong by a factor of seven.

per tick at 2/sec over 8 hours
first cut 3.55 ms 0.71% of a core 205 s of CPU
caches, no double reads 0.48 ms 0.095% of a core 27 s of CPU
idle-gated 0.0005 ms 0.0001% of a core 0.75 s of CPU

The gate is the whole thing, and it comes from one observation: while Night is holding the lights down, nothing but a person at the machine can move them. The ambient sensor is off — that is the first thing going dark does — and nothing brightens without someone touching the machine. So if there has been no input, there is nothing to read.

CGEventSource.secondsSinceLastEventType(.combinedSessionState, kCGAnyInputEventType) answers that for free: it reads a counter the window server already keeps, and 200,000 calls averaged 0.0005 ms. On a sleeping night the tick is that one call and nothing else, so the expensive reads happen only in the couple of seconds around the Mac actually being woken — which is exactly when the half-second response matters and the cost does not.

⚠ CGEventType(rawValue: ~0) does construct, and it agrees with IOHIDSystem's HIDIdleTime (488.76 s against 489.18 s, the gap being the time between the two reads). But do not write ?? .null as its fallback: .null answers a different question and returned 193,575 seconds where any-input said 489 — a gate that never opens. The fallback here is .mouseMoved, which at worst under-reports idleness and opens the gate too often, which is the safe direction.

A backstop every 30 seconds looks anyway, regardless of input, covering the one case the gate cannot see: brightness changed with no HID event at all, a script over SSH being the realistic one. It costs about 0.4 s of CPU across a night.

Where the original 3.55 ms went, and what fixed it:

  • copyKeyboardBacklightIDs, 0.34 ms, on every single brightness read. It is a hardware inventory. Cached in Kbd.m; an empty result is not cached, so a late-arriving keyboard still turns up.
  • CGGetActiveDisplayList, 0.44 ms, likewise — half the cost of reading the panel at all. Cached in Display.swift and invalidated by CGDisplayRegisterReconfigurationCallback, so plugging a monitor in still does the right thing.
  • Everything was read twice, once to decide and once to redraw. Nothing redraws now unless something actually changed.

The menu items and SMAppService.status — a real lookup, not a cached flag — are computed on menu open, never on the tick. And the timer does not exist at all unless the lights are being held down, so none of this runs during ordinary use.

Why it polls at all — two dead ends, both checked

Neither of these works, and both look like they should:

  • No notification fires on a brightness change. Logged every NSDistributedNotificationCenter name across a change: nothing at all. DisplayServicesBrightnessChanged does not exist in this build either.
  • IOServiceAddInterestNotification on AppleARMBacklight registers and never fires. It returns kr=0, so it looks healthy. Worse, that service's IODisplayParameters — brightness, rawBrightness, BrightnessMilliNits — is a frozen boot-time snapshot: it read brightness=32768 rawBrightness=1488 before and after a real brightness change, and 32768/65536 is exactly 0.5 while the panel was at 0.80. Do not trust those values for anything.

The two halves are deliberately asymmetric:

  • Panel up ⇒ both released. It is the "I'm awake" signal. The keyboard goes with it because several MacBook function rows (MacBookPro18,1 among them) have no keyboard-backlight key at all — it lives in Control Center — so a keyboard left held would be stranded dark until you happened to open this menu.
  • Keyboard up ⇒ only the keyboard. A smaller statement, taken literally.

And a distinction worth keeping straight in the code: release keeps the level it finds, because you just chose it; restore writes the level back from memory. Releasing the keyboard in the coupled case would hand the sensor a 0 and leave the keys dark, so that path restores instead.

What "restored" does and does not promise

Restoring auto-brightness means handing the level back to the ambient sensor, which is then free to move it — that is what auto-brightness is, and it was doing exactly that before Night touched anything. The remembered value is a starting point, not a lock. If auto-brightness was off before going dark, it is restored off and the level stays put.

Verified

End to end through the real menu, and re-run unchanged after the tick was optimised:

  • Go Dark clicked in the menu bar → display=0.0 auto=false kbd=0.0 auto=false.
  • Panel brightened by hand → inside the first half-second sample, display=0.80 auto=true kbd=0.146 auto=true: the chosen level intact rather than overwritten, keyboard relit from its remembered 0.17, saved state cleared so the poll stopped, menu back to "Go Dark" with both checkmarks clear.
  • Keyboard raised on its own, with only the keyboard held → auto back on, level left at the 0.40 chosen, panel untouched.
  • Night relaunched while already dark → keeps holding, rather than undoing the night.

Re-run after the idle gate, which needed a different rig — a brightness change made from a shell generates no HID event, so it is exactly what the gate is built to ignore:

  • Raised with no input at all → correctly ignored, still holding after 3 s (auto=false).
  • One synthetic mouse move, nothing else changed → released inside the first 0.5 s sample: auto=true on both, keyboard relit to 0.147, saved state cleared.
  • Backstop → raised with no input, released at exactly t+30 s.

⚠ Do not drive this app's own menu with synthetic clicks to test it. A click meant for Go Dark landed on Sleep Display, which locked the screen — and a locked screen reads as display=0.0 with DisplayServicesSetBrightness silently doing nothing, which looks exactly like the bug you are hunting. Plant the state in defaults and drive the hardware directly instead; it is deterministic and it cannot lock anything.

The private API, and how it was found

Nothing here is public. Both frameworks are in the dyld shared cache, so nm on the path returns nothing — the symbols were found with dlsym and class_copyMethodList at runtime (macOS 27 build 26A428, Apple silicon). They have been stable for years, and a missing symbol degrades to a no-op rather than a crash.

Display — DisplayServices.framework, resolved by dlsym in src/Display.swift:

DisplayServicesGetBrightness                   (CGDirectDisplayID, float *) -> int
DisplayServicesSetBrightness                   (CGDirectDisplayID, float)   -> int
DisplayServicesAmbientLightCompensationEnabled (CGDirectDisplayID, bool *)  -> int
DisplayServicesEnableAmbientLightCompensation  (CGDirectDisplayID, bool)    -> int

Keyboard — CoreBrightness.framework, class KeyboardBrightnessClient, in src/Kbd.m. It is Objective-C and not Swift because the selectors take a raw unsigned long long keyboard id alongside a float, and there is no way to spell that objc_msgSend cast in Swift. Get the ids from copyKeyboardBacklightIDs (an NSArray<NSNumber *>, one entry for the built-in keyboard) and pass unsignedLongLongValue — passing the NSNumber itself silently returns -1, which reads like "no backlight" and is not.

copyKeyboardBacklightIDs            @16@0:8
brightnessForKeyboard:              f24@0:8Q16
setBrightness:forKeyboard:          B28@0:8f16Q20
enableAutoBrightness:forKeyboard:   B28@0:8B16Q20
isAutoBrightnessEnabledForKeyboard: B24@0:8Q16
isKeyboardBuiltIn:                  B24@0:8Q16

Layout

build.sh            one swiftc pass, one cc pass, ad-hoc codesign
src/main.swift      the three actions, the menu bar, the idle-gated poll
src/Display.swift   DisplayServices via dlsym
src/Kbd.h/.m        KeyboardBrightnessClient via objc_msgSend
src/Info.plist      LSUIElement, which is what keeps it out of the dock

License

MIT. See LICENSE.

About

A macOS menu bar app that kills the panel and keyboard backlight while the Mac stays awake, unlocked and screenshot-able. Private DisplayServices/CoreBrightness, an idle-gated poll, and the notes from reverse-engineering both.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages