Repository navigation
Menu bar: route status item clicks by the cursor on macOS 27 - #76
BurakErdemci wants to merge 2 commits into
Conversation
On macOS 27 a click on the status item reaches the hosting view only on release, as a mouseDown/mouseUp pair located at the item's center, so every click hit whatever sat in the middle: the countdown instead of the end control, the Hours pill instead of Minutes. The same click also reached the global monitor, which collapsed the pills. Replaying the pair at the real cursor does not help: SwiftUI fires no gesture for a replayed event. On macOS 27 and later the hosting view now routes each left click itself: the root view reports the frames of the eye, the pills, the countdown and the end control, and the click goes to the element under the cursor, read from NSEvent.mouseLocation. SwiftUI's own gestures do nothing there. Global clicks inside the item's window no longer collapse the pills, and since a hold cannot be measured, the end control ends a session on a click followed by a confirming click within two seconds. macOS 26 keeps its gestures and press-and-hold unchanged. Fixes krishhgg#60. Co-authored-by: Claude <[email protected]>
Co-authored-by: Claude <[email protected]>
|
Tested on a second macOS 27 Mac: MacBook Pro (Mac14,6), Apple M2 Max, macOS 27.0 (26A428), Xcode 27.1. Build: By hand, with the new log line:
Not exercised: VoiceOver, and macOS 26. |
paoValle
left a comment
There was a problem hiding this comment.
Source review only. This machine has no Xcode (macOS 27.0.1, Command Line Tools only), so I could not run swift test or the -warnings-as-errors release build. Everything below is read from the diff at af6bd6e and the files it touches, and the one test claim I could not verify by running is named as such.
What I checked, and what reads right. The routing reaches a real action for every target, and all five SwiftUI tap sites that could otherwise fire a second time are guarded by routesClicksManually — eye, pill, countdown, confirmation label, end control — each with an explicit accessibilityAction left in place, so VoiceOver keeps working. That double-fire is the failure mode I would have looked for here, and the guards cover it. osMajorVersion is injected the way DisplayPower does it, so the macOS 26 path is testable rather than assumed, and testMacOS26KeepsTheHoldToEndPath and testMacOS26HostKeepsSwiftUIMouseHandling pin it. The confirmation is not a bare two-click: endTapped re-arms when the deadline has passed even if the clearing task has not run yet, and testLateConfirmingClickRearmsBeforeTheTimerClearsTheFlag pins that specifically. testRouteClickOnTheEndControlArmsThenEndsTheSession asserts endConfirmArmed and manager.isActive after the first click, so it fails if the confirm is removed — it bites. I also traced the two mutations Greptile asked for earlier: dropping window.convertPoint(fromScreen:) from hostPoint changes the result by the window and host origins (the test window is at 400,300), and dropping the isFlipped flip breaks the round trip, so the new hosted test does catch both.
Two findings and one nit, most important first.
1. The confirmation label changes the status item's width, which docs/spec.md says this layout does not do
StatusRootView.swift:94-101 replaces the countdown with a wider text while the confirmation is armed. Measured on this Mac with AppKit's own metrics (NSFont 11 pt medium rounded for the label, 13 pt medium rounded with monospaced digits for the countdown):
"Click again to end" 11 medium rounded = 93.84
"59:59" 13 medium rounded+mDigit = 38.18 (+55.66)
"3:59:41" 13 medium rounded+mDigit = 50.67 (+43.18)
"30d 00:00" 13 medium rounded+mDigit = 66.35 (+27.49)
So arming grows the item by 27–56 pt and expiry shrinks it back: two NSStatusItem.length writes per end attempt, on a control a user can reach by accident. docs/spec.md:1159 enumerates the layout-changing state as "the phase, the pill slots, the error label", and the StatusRootView docstring repeats that enumeration; the confirmation is now a fourth. The page's own note at docs/spec.md:1153-1160 is that every length write makes Control Center re-lay out the menu bar and blocks the next render commit for 8–130 ms. The existing width budget test (Tests/InsomniaTests/UIStatusTests.swift:1149-1184) pins an exact widthTargetChangeCount per layout change but never arms the confirmation, so the two new writes are not in that sequence.
Cheapest check if the branch is installed: log widthTargetChangeCount across one arm/expire cycle. If it moves by two, sizing that slot to the wider of the countdown and the label for the whole running phase would remove both writes and keep the enumeration true — an invisible placeholder is already the pattern for this in PillView.swift:31-36. I cannot measure the item here (no build), so I am not claiming it does move; the text widths above are what I measured.
2. The README still captions and illustrates the hold
Step 3 was updated for the click-and-confirm, but the image directly above it in the same section was not. README.md:37's alt text still reads "hold the end control to finish early", and docs/assets/session-flow.svg (:3 and the caption at :70-71) still draws a mid-filled ring labelled "Hold to end". The install page therefore teaches a gesture that on macOS 27 does nothing, one paragraph after correcting it. The same text is in docs/spec.md:100, docs/spec.md:1137 and docs/mockups/menubar-ux.html:174; the spec is what a later contributor reads before touching this control, so it should name both behaviours the way the README now does.
3. Nit: hostPoint drops bounds.origin.y
StatusItemController.swift:928 — bounds.height - point.y is only right while bounds.origin is zero. bounds.maxY - point.y is the same line and origin-safe.
Not exercised by me: swift test, VoiceOver, and macOS 26 on hardware. One note, not a finding: #93 changes the same file and #74 adds a menu action on top of this, so the owner has to pick.
|
Closing in favour of #93. Its latest head ( |
Fixes #60.
Why
On macOS 27 a left click on the status item never reaches the hosting view the way it does on macOS 26. Measured on a MacBook Air M4, macOS 27.0.1, with temporary logging in
StatusHostingViewand the monitors:event.locationInWindowNSEvent.mouseLocationread in that mouseDown is the real cursor.Replaying the pair at the real cursor does not work: a replacement
NSEventpassed tosuper.mouseDown, the same with the host window's number, andwindow.sendEventwith the replacement (which comes back to the host at the right location) all left SwiftUI firing no tap at all.What
Everything new is gated on
osMajorVersion >= 27(injected, asDisplayPowerdoes). On macOS 26 taps, hold-to-end and the menu behave as before.StatusRootViewreports the frames of the eye, each pill, the countdown, the armed "Click again to end" label and the end control in a named coordinate space (hitFrameson the model, not observed).StatusHostingViewwith aleftClickRouterreadsNSEvent.mouseLocation, converts it to the host, and does not callsuperfor left clicks.routeClickpicks the smallest frame under the point and calls the existing action: eye ->iconTapped, pill ->focus, countdown ->customExtend, end or label ->endTapped. One log line per click:status item click at (x, y) -> target.accessibilityActions call the real actions so VoiceOver presses keep working.Tests
UIStatusTests:hitTarget(at:frames:),routeClickper phase with injected frames, the confirm flow (arm, end, expiry re-arms, deadline passed with the flag still set re-arms, session end and extend disarm), the global-filter decision on 26 and 27, a hosted check that frames survive arm/expire cycles, and that macOS 26 builds no router.swift build -c release -Xswiftc -warnings-as-errorspasses;swift test --filter UIStatusTests --skip testHotspotPasswordProblemCopyand--filter UIStartupTestspass.testHotspotPasswordProblemCopyfails here on untouched main too (the Keychain message comes back in Turkish, this Mac's locale).Validation
A debug build of this branch on the same Mac, clicked by hand, with the new log line:
-> icon, pills open and stay open-> pill(minutes),-> pill(days), focus moves-> end, "Click again to end"-> countdown, extend pills opensession extended by 300s-> end,-> end, session end (user), sleep restoredNot exercised live: a click on the armed label itself and VoiceOver. macOS 26 was not run on hardware; the 26 path is covered by the unchanged gestures and by tests with
osMajorVersion: 26.Written with Claude Code (code by Codex), checked by two independent automated audits, a fix round and a narrow re-audit before opening.

Confidence Score: 5/5
The PR appears safe to merge, with the previous test-coverage finding addressed and no new blocking issues found.Summary
The PR routes menu bar clicks using the cursor position on macOS 27 and later. Ending a session uses two clicks within 2 seconds. macOS 26 keeps its existing gestures.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Status item left click] --> B{macOS 27 or later?} B -->|No| C[Existing SwiftUI gestures] B -->|Yes| D[Read cursor screen position] D --> E[hostPoint converts to host coordinates] E --> F[routeClick selects the control] F --> G[Eye, pill or countdown action] F --> H[End control or confirmation label] H --> I{Confirmation deadline still valid?} I -->|No| J[Arm confirmation for 2 seconds] I -->|Yes| K[End the session]Reviews (2) · Last reviewed commit: "Menu bar: test the screen-to-host click ..." · Reviewed by Greptile