Releases: dotcomjack/nocturne
Release list
Nocturne 1.2.0
Hover to show
The cover can now hand the bar back on demand. Turn on Hover to show and the strip drops away while the pointer is on the menu bar, then comes back when the pointer leaves. Off by default.
It exists so you can read the time by going to look for it, which is a deliberate act, rather than by changing a mode and then remembering to change it back. The point of the app was always to stop the clock catching your eye when you were not asking. It was never to stop you asking.
Offered in Gone and Hide everything only. Blind hides the time by writing Control Center's own IsAnalog preference, and undoing that costs a Control Center restart, which is a KeepAlive job on a 1s throttle. Half a second and a menu bar blink in each direction is not a hover.
It still asks for no permissions
That was the part worth getting right, because the whole locator is built around never triggering a Screen Recording prompt.
The pointer is read with a global mouse monitor. macOS gates key events behind accessibility and leaves mouse events alone, which is easy to assume and worth measuring. Measured on macOS 26.3.1 from an ad-hoc signed .app launched with open, so with no inherited terminal grant, reporting AXIsProcessTrusted() false:
| Global monitor handle | created |
AXIsProcessTrusted() |
false |
mouseMoved events delivered |
131 |
Polling NSEvent.mouseLocation on a short timer was the alternative and is strictly worse: it wakes the CPU 20 times a second, forever, to notice a pointer that is usually not moving.
Three details that are less obvious than they look
The hover region is the whole menu bar, not the covered rect. In Gone the patch is a 44pt dial in the corner. Requiring the pointer to land on it exactly would read as broken. Moving to the bar at all is the gesture.
The strip goes transparent rather than being ordered out. A 2s tracker re-shows any window that is not visible, so ordering the strip out would put it back within two seconds while the pointer was still resting on the bar.
On more than one display, only the bar you are pointing at uncovers. Strips are matched to bars by intersection rather than by index, because both lists are built from a dictionary keyed by screen and that iteration order is not stable. Verified in a single pass: laptop strip at alpha 0.0, external strip at alpha 1.0.
There is also one more edge that only shows up in testing. CGRect.contains excludes its own max edge, and a menu bar band's top edge is the top of the screen, which is exactly where the pointer sits when you slam it upward to reach the bar. Without a one point expansion, the single most likely pointer position reads as not hovering.
Install
brew install --cask dotcomjack/tap/nocturne, or the DMG below. Universal, signed with a Developer ID and notarized, so it opens on a double click.
Nocturne 1.1.1
The shimmer moved the icon
The sweep frames were rebuilt from the symbol name at pointSize: 15, while the resting glyph is created with no configuration at all. Every glyph came out larger in the frames:
| glyph | resting | sweep frame |
|---|---|---|
clock |
15x15 | 18x17 |
clock.badge.xmark |
18x16 | 21x19 |
moon.stars |
15x16 | 17x18 |
moon.fill |
15x15 | 18x17 |
A status item with variableLength sizes itself to whatever image it is showing, so every sweep resized the item and nudged its neighbours. That is the movement and the phantom padding.
Frames are now drawn from the resting image itself, so they cannot differ from it in size. Verified live: sampling every menu bar item for 18 seconds at the fastest cadence, every item held one width throughout.
(Pinning the item length was tried first and rejected. Measuring the button width during refreshIcon runs before layout has settled, and the icon vanished from the menu bar entirely. Matching the source image is the smaller and safer fix.)
The sweep is icy dark blue
The band is now #3F87B3 rather than a dip in brightness.
That has a consequence worth knowing: the frames can no longer be template images, because a template is repainted in a single colour and the blue would simply vanish. So the frames carry the glyph colour as well, cached per appearance and rebuilt when light and dark flip. The resting glyph stays a template, so it keeps adapting for free whenever nothing is sweeping.
Checked against both bar appearances. #3F87B3 sits at mid luminance, so it reads against a white glyph and a black one.
Nocturne 1.1.0
The icon can shimmer
A highlight sweeps down the menu bar icon over 700ms. Settings has a cadence picker: Off, Often (10s), Occasionally (30s, the default) or Rarely (2 minutes). Picking one previews it straight away.
The default is deliberately unhurried and Off is a first-class option, because this is an app for not being interrupted by your menu bar.
Two things that made it work properly:
The frames are template images, with the sweep in the alpha channel rather than the colour. AppKit keeps tinting a template for whatever the menu bar currently needs, so one set of frames reads correctly on a light bar, a dark bar, and over a wallpaper that moves underneath. Baking a colour in would have meant regenerating everything on every appearance change.
The band dims rather than brightens. A template's alpha caps at 1, so brightening a band means dimming everything else, and the whole icon visibly dropped the instant a sweep started and snapped back when it ended. Dipping instead makes the first and last frames identical to the resting glyph, so the sweep begins and ends on nothing.
Frames are cached per symbol, and the schedule timer carries 25% tolerance so decoration cannot wake a sleeping Mac.
The panel got a pass
- Sections and mode changes animate at 160ms instead of popping.
- Mode cards share a minimum height, so one description wrapping to two lines no longer leaves the stack ragged.
- The window sizes itself to its content and clamps to the screen, instead of a fixed height that left dead space on a large display and overflowed a small one.
- Light-mode contrast fix. Section labels measured 4.21:1 against macOS
windowBackgroundColor, under the 4.5:1 WCAG AA needs for small text. The brand bronze is specified against a lighter paper background, so small tracked labels now use a slightly deeper variant measuring 4.93:1. Dark mode already passed at 7.05:1 and is unchanged.
Nocturne 1.0.8
A verification pass over all 19 earlier fixes found that two of them did not actually hold. Those are the interesting ones.
The quit-safety confirmation could never fail
1.0.1 added a confirmation loop so quitting cannot exit before the clock restore has committed, since that would strand you on an analog clock with no app left to undo it.
The loop read the value back and compared it. But CFPreferencesCopyValue is served from the writing process's own cache, so the comparison was true on the first iteration whether or not the daemon had committed anything. Measured across 8 runs: exited on iteration 1 every time, in 0.22ms to 0.37ms. The retry and the timeout were decorative; a fixed 120ms sleep was doing all the work.
It now flushes with CFPreferencesAppSynchronize, whose return value is a real signal worth retrying on. The guarantee it protects was re-verified: 0 of 12 SIGTERM quits left the clock analog.
Raising the window height in 1.0.6 was a regression
1.0.6 raised the Settings panel to 880pt so nothing sat below the fold. That makes a 912pt window, and AppKit does not clamp a non-resizable window to the screen:
| style | requested | result |
|---|---|---|
[.titled, .closable] |
1232pt | 1232pt, hangs 149pt off screen |
+ .resizable |
1232pt | clamped to fit |
On any Mac under roughly 912pt of usable height, which includes 1440x900, the bottom of the panel was permanently unreachable, and what sits there is the login-items warning and the Restore button. The window is now resizable and sizes itself to the screen.
Two smaller ones
The beacon guard failed open: when the status item frame was unavailable it skipped the whole check and placed the full-bar strips with no glyph, which is exactly the lockout the guard exists to prevent.
restoreOriginalClock had quietly stopped keeping its own one-restart promise, because routing mode through the property in 1.0.6 put an apply() ahead of the batched writes.
Nocturne 1.0.7
Five fixes from a third review pass. The headline one is a guard that never did anything.
The safety guard in Hide everything was doing nothing
1.0.4 added a check so the moon glyph is only drawn where Nocturne's icon really is, because that glyph is the advertised way out of a blank menu bar and painting a fake one is worse than painting none.
The check compared the glyph frame against the strip we had placed. In Hide everything that strip spans the full screen width, so any frame in the menu bar's y-band intersected it, including the exact x=57 phantom it was written to reject. It has never once fired.
It now asks the window list whether an item is actually drawn there. Measured:
| old guard | new check | |
|---|---|---|
| real menu bar item | pass | pass |
| phantom at x=57 | pass | reject |
The same guard could make the bar flash forever
It also ran after the windows were placed, so with an off-bar glyph it created the strip and tore it down on the same pass, every 2 seconds, indefinitely. The decision now happens before anything is drawn.
Settings could show the wrong switch position, then act inverted
@AppStorage caches a key in memory once assigned through the wrapper and stops reading through to UserDefaults, so the code that pulled live system values in was invisible to any switch you had already touched. The panel showed the old position, and your next click wrote a value the system already had, so the menu bar did not change and you had to click twice.
That refresh only ran once per login anyway
The Settings window is a singleton that is only ordered out when closed, so its view never leaves and onAppear fires once per process. For an app that launches at login, that is once. Reopening Settings showed the values from the first open, and the "macOS is blocking this" row stayed up even after you had approved Nocturne under Login Items. It now refreshes whenever the window becomes key.
Dark to light left the glyph invisible
The flat fill colour is baked into a layer and the glyph is tinted once, so with Appearance set to Auto, sunset left a white glyph on a light strip. Appearance changes are now observed and the overlay rebuilds.
Also: flipping several Clock Options in a row now costs one Control Center restart instead of one each.
Nocturne 1.0.6
Six fixes from a second adversarial review pass. All measured, not guessed.
Hide everything uncovered the menu bar for up to 2 seconds
activate() early-returned when the overlay was already running, and teardown() never clears isActive, so the callback written to re-place the strip after a Control Center restart did nothing. The 2 second tracker tears the strip down whenever it ticks during the ~500ms restart window, which it lands in roughly one change in four, and only the next tick put it back.
In Hide everything that means the entire menu bar was readable for up to 2 seconds, which is the mode's only job. activate() now re-syncs instead of ignoring the call.
Quitting from the menu blanked the menu bar for ~1.8 seconds
The menu handler restored the clock, then NSApp.terminate ran applicationWillTerminate, which restored it again. Two force-kills of Control Center about 120ms apart, and the second lands on the freshly respawned process and trips launchd's 1 second ThrottleInterval.
Measured: ~1.8s of empty menu bar instead of ~0.5s. The menu no longer restores; the terminate handler already covers every path, and the signal handlers cover pkill.
"macOS is blocking this" could never appear
Introduced in 1.0.5. Snapping the switch back re-entered onChange, which recomputed the warning flag to false, so the row and its shortcut to Login Items never rendered. Worse, the second pass called launchAtLogin = false again, which unregisters a request that was merely pending approval. The corrective pass now bails first.
Settings hid its own undo control
Content overflowed the fixed 430x720 window. In Gone mode the entire General section, including "Restore clock to how it was", sat below an invisible fold with macOS scrollbars hidden at rest. Window is now 880pt.
Two documentation corrections
The restore comment and README claimed a pre-existing analog clock is preserved. It never was: the snapshot forces isAnalog to false at capture time, deliberately, so that clearing Nocturne's preferences cannot strand you on analog forever. Both now say so, and the README states the caveat plainly.
Two em dashes in the published v1.0.2 release notes were also fixed. All six releases now scan clean.
Nocturne 1.0.5
Four fixes from an adversarial review pass. All four were measured, not guessed.
Changing one Clock Option silently reverted the others
Any switch pushed all four keys from Nocturne's own cached copies, and Nocturne read the system values exactly once, at first run. So anything you changed in System Settings afterwards got overwritten the next time you touched an unrelated switch.
enable Seconds in System Settings -> ShowSeconds = 1
flip "Day of the week" in Nocturne -> ShowSeconds = 0 (silently reverted)
Each switch now writes only its own key, and Settings re-reads the live values every time it opens, so the panel can no longer show a stale picture either.
The Hide everything strip was 1pt too tall
Its height came from frame.maxY - visibleFrame.maxY, which reads 34 on a notched display where the bar is really 33. The strip overhung by two device pixels across the full width and, being darker than the bar, rendered as a hairline rule underneath it.
Height is now measured from the menu bar items themselves, which are exactly as tall as the bar. Verified: strip window is 33.0, matching the Window Server's own menu bar window.
The beacon could be a decoy
AppKit keeps reporting a plausible on-bar frame for a status item macOS has dropped for menu bar overflow, or that a menu bar manager has parked off screen. Of 26 test status items, 20 were dropped and every one still claimed isVisible with a frame matching no real slot.
Since that glyph is the advertised way out of a blank menu bar, painting one that does nothing when clicked is worse than painting none. It now has to land inside a strip we actually placed, and the strip comes down if it cannot.
Launch at login failed silently
It snapped back with no explanation when macOS refused registration. It now says so and offers a button straight to Login Items.
Nocturne 1.0.4
Hide everything now keeps Nocturne's own icon visible.
Blanking the whole menu bar also blanked the one control that turns it back on, so there was no obvious way to re-enable visibility.
Cutting a hole in the strip around the icon was the first attempt, and it looked worse: the hole exposes the real bar, which is lighter than the strip, so the icon sat in a visibly paler rectangle.
Instead the strip stays whole and the glyph is redrawn one window level above it, with ignoresMouseEvents so clicks pass straight through to the real status item. The bar stays uniform, the way out stays lit, and the menu opens normally.
Verified end to end: beacon lands byte-identical to the real status item rect (x=1392 w=31 h=33), a synthetic click at that point opens Nocturne's menu at layer=101 above the overlay, and the menu is fully readable with the correct mode checkmarked.
There is a second way back regardless: double-clicking Nocturne in Applications opens Settings as an ordinary window, even with the bar blank.
Nocturne 1.0.3
After pressing "Restore clock to how it was", the mode you were on could not be turned back on.
restoreOriginalClock() wrote mode straight to UserDefaults, which leaves @AppStorage's in-memory copy stale. Disk said off while self.mode still read blind, so clicking Blind in the menu hit the didSet guard mode != oldValue, saw no change, and never applied anything. The click looked like it did nothing.
Reproduced:
1. pick Blind -> mode=blind, IsAnalog=false
2. press Restore -> disk mode=off, in-memory mode=blind (stale)
3. click Blind -> nothing happens, clock stays readable
4. click a DIFFERENT mode -> works fine
It now assigns through the property, which keeps the in-memory value honest and also bumps the generation counter that cancels an in-flight overlay callback. Pressing Restore mid-restart previously left the overlay up with the mode already off.
Nocturne 1.0.2
Gone mode covered the wrong menu bar item for up to 2 seconds after switching modes.
waitForControlCenter waited for the clock to be present, not for its geometry to be settled. Both halves of that turn out to be wrong right after a Control Center restart:
- The dying process's windows are still listed at
t=0.004s, so the non-empty check short-circuited instantly on pre-restart geometry. They only actually vanish aroundt=0.03s. - Control Center returns around
t=0.6sand animates its items in, the clock growing36 → 38 → 40 → 42 → 44ptover roughly a third of a second.
Placing the strip against either put it over a neighbouring icon until the 2 second tracker corrected it. Measured misses of 11pt, 32pt, and a 33pt width error, which is a different item entirely.
It now requires the same rect twice in a row before believing it. Measured across 4 restarts:
| Picked | |
|---|---|
| Old (first non-empty) | w22, w22, w36, w26 , wrong 4 of 4 |
| New (stable twice) | w44, w44, w44, w44 , correct 4 of 4 |
Settled truth was w44 in every trial.
Affects Gone and Hide everything only. Blind is unaffected.