2026-09-15 12:44:18 +10:00
2026-09-15 12:44:18 +10:00
2026-09-15 12:44:18 +10:00
2026-09-15 12:44:18 +10:00
2026-09-15 12:44:18 +10:00
2026-09-15 12:44:18 +10:00
2026-09-15 12:44:18 +10:00
2026-09-15 12:44:18 +10:00
2026-09-15 12:44:18 +10:00

Wakeful

A menu bar switch for macOS sleep. Does what caffeinate -dis does, without a terminal window pinned open for the rest of the day.

Lives in the menu bar, toggles with a global shortcut, starts at login, and can turn itself on when you plug in or connect an external display.


Why this exists

caffeinate is the right tool with the wrong interface. It's a foreground process, so it needs a terminal window that you can't close, that survives nothing, and that tells you nothing about its own state. Wakeful holds exactly the same kernel assertions — it just does it from a background app with a switch.

Verify that for yourself at any time:

pmset -g assertions

You'll see PreventUserIdleDisplaySleep, PreventUserIdleSystemSleep, and PreventSystemSleep attributed to Wakeful, with the reason string "Wakeful is keeping this Mac awake".


Design

Approach

Rather than shelling out to /usr/bin/caffeinate and babysitting a child process, Wakeful calls IOPMAssertionCreateWithName directly — which is all caffeinate itself does. This means:

  • No subprocess, no orphaned caffeinate in Activity Monitor if the app dies.
  • The kernel releases every assertion automatically on process exit, so a crash can never leave your Mac permanently awake.
  • Assertions are named and attributed properly in pmset -g assertions.

Assertion → caffeinate mapping

Menu item IOKit assertion caffeinate
Display sleep PreventUserIdleDisplaySleep -d
System idle sleep PreventUserIdleSystemSleep -i
Full system sleep (AC only) PreventSystemSleep -s
Disk spin-down PreventDiskIdle -m

Default is -d -i -s, i.e. caffeinate -dis. The Prevent submenu shows the equivalent command for whatever you've selected.

Module layout

Sources/Wakeful/
  main.swift            NSApplication bootstrap, .accessory activation policy
  AppDelegate.swift     NSStatusItem + the whole menu, rebuilt on every open
  WakeController.swift  State machine: on/off, source, expiry, smart rules
  SleepBlocker.swift    Owns live IOPMAssertionIDs; declarative apply(Set)
  PowerMonitor.swift    IOPS notifications (AC / charge) + screen-change notifications
  Preferences.swift     UserDefaults, all keys settable via `defaults write`
  HotKey.swift          Carbon RegisterEventHotKey global shortcut
  LaunchAtLogin.swift   SMAppService.mainApp registration

Two design decisions worth calling out:

The menu is rebuilt from scratch in menuNeedsUpdate(_:). Menu bar apps accumulate bugs from trying to keep dozens of checkmarks in sync with model state. Throwing the menu away and regenerating it from Preferences and WakeController on each open makes desync structurally impossible, and a menu opens rarely enough that the cost is irrelevant.

SleepBlocker.apply(_:) is declarative, not imperative. You hand it the set of assertions you want held and it reconciles the delta. No if held { release } branching at call sites, and repeated calls are free.

Smart activation semantics

The tricky part of conditional activation is not the conditions, it's not fighting the user. Wakeful tracks why it is on (.manual vs .automatic):

  • A rule becoming true turns Wakeful on with source .automatic.
  • A rule becoming false turns it off again only if source is .automatic. Your manual session is never cancelled by a rule.
  • Manually turning it off while a rule wants it on sets a suppression flag, so the app doesn't immediately re-enable itself. The flag clears as soon as the rule stops being satisfied — so unplugging and replugging re-arms it.

Global shortcut

Uses the Carbon RegisterEventHotKey API, deliberately not CGEventTap or NSEvent.addGlobalMonitorForEvents. The latter two would drag the user through an Accessibility / Input Monitoring permission prompt; RegisterEventHotKey needs no permission and no entitlement at all.

v1 ships three presets (⌃⌥⌘A, ⌃⌥⌘K, ⇧⌥⌘Z) rather than a recorder UI, and none is enabled by default — an app shouldn't claim a system-wide key combination without being asked.


Build

Requires Xcode or the Command Line Tools (xcode-select --install).

git clone <your-repo-url> wakeful
cd wakeful
make install

make install builds in release mode, assembles Wakeful.app, ad-hoc code-signs it, copies it to /Applications, and launches it.

Other targets:

Command Effect
make app Build and sign into ./dist/Wakeful.app without installing
make app-universal Same, from a universal arm64 + x86_64 binary
make run Run the raw binary in the terminal (for debugging)
make uninstall Quit, remove from /Applications, delete preferences
make clean Remove build artifacts

To sign with a real Developer ID instead of ad-hoc:

make install SIGN_ID="Developer ID Application: Your Name (TEAMID)"

You can also just open Package.swift to work on it in Xcode, but note that Preferences and Launch at Login only behave correctly from the assembled bundle.


Usage

Click the cup icon in the menu bar.

  • Turn On / Turn Off — indefinite session. Same as the global shortcut.
  • Keep Awake For — 15m / 30m / 1h / 2h / 5h timed session, with a live countdown in the menu bar. Add 30 minutes to extend one in flight.
  • Prevent — pick which assertions to hold. At least one is always held.
  • Smart Activation — auto-enable while on AC, while an external display is connected, or while on battery above 20/50/80% (the battery rule only applies off AC, otherwise a charged laptop would satisfy it forever). The submenu footer shows current conditions so you can see why a rule fired.
  • Global Shortcut — off by default; pick a preset to enable it. If the combination is already claimed by another app or a System Settings shortcut, the menu says "unavailable" rather than silently doing nothing. Note that no global shortcut fires while macOS Secure Input is active — i.e. while a password field has focus.
  • Launch at Login — install to /Applications first; the registration records the bundle path, so moving the app afterwards breaks it.
  • Turn On at Launch — combine with Launch at Login for always-awake.

Hidden preference

The toggle and the global shortcut start an indefinite session by default. To make them start a timed one instead:

defaults write com.github.wakeful.Wakeful defaultDurationMinutes -int 60

Caveats

  • Ad-hoc signing. make app signs with -, which is fine on the machine that built it. Anyone downloading a prebuilt binary would need a notarized build or a right-click → Open. Building from source sidesteps this entirely, which is why there are no releases.
  • Power assertions cannot stop lid-close sleep. This is the one thing people expect and don't get. Assertions never override user-initiated sleep, so PreventSystemSleep keeps the Mac out of full sleep while idle on AC but does nothing when you shut the lid. Clamshell sleep can only be defeated with sudo pmset disablesleep 1, which has no public API and is deliberately out of scope here.
  • PreventSystemSleep is AC-only. macOS ignores it on battery entirely.
  • Login item approval. macOS may put the registration in a requiresApproval state the first time; the menu says so, and it's cleared in System Settings ▸ General ▸ Login Items & Extensions. SMAppService requires a code-signed bundle; ad-hoc signing is expected to satisfy that locally, but if Launch at Login errors out on your machine, a real Developer ID is the fix.
  • Smart activation is coarse. "On AC" doesn't distinguish a charger from a dock. If you want "awake while on this specific display", that's a v1.1 problem.

Roadmap

Deliberately out of scope for v1, in rough priority order:

  1. Hotkey recorder UI instead of presets.
  2. Per-app rules — stay awake while Zoom / a specific process is running. This is what people actually reach for after timed sessions.
  3. A real Settings window (NSWindow), once the menu stops being enough.
  4. Optional notification when a timed session expires.
  5. Battery-drain guard: auto-release below a floor even during a manual session.
  6. App icon + notarized release artifacts, if this is ever shared widely.

License

MIT. See LICENSE.

S
Description
A menu bar switch for macOS sleep. Does what caffeinate -dis does, without a terminal window pinned open for the rest of the day. Lives in the menu bar, toggles with a global shortcut, starts at login, and can turn itself on when you plug in or connect an external display.
https://gitea.hansenits.com.au/HansenITSolutions/Wakeful
Readme MIT
86 KiB
v1.1.0
Latest
2026-09-23 08:29:47 +00:00
Languages
Swift 92%
Makefile 4.9%
Shell 3.1%