# 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: ```sh 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`). ```sh git clone 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: ```sh 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: ```sh 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](LICENSE).