Skip to content

Menu bar: hold-to-end works on macOS 27 and shows on a light bar - #93

Open
hasin023 wants to merge 5 commits into
krishhgg:mainfrom
hasin023:fix/menubar-light-mode
Open

hasin023 wants to merge 5 commits into
krishhgg:mainfrom
hasin023:fix/menubar-light-mode

Conversation

@hasin023

@hasin023 hasin023 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Refs #60. This overlaps with #76 and touches StatusItemController.swift too, so the two will conflict. I'm putting it up as another way to fix it.

On macOS 27 a click on the status item arrives as a mouseDown/mouseUp pair at the middle of the item, with the release already sent while the button is still down. So a press on the end ring hit the countdown, and a hold couldn't last.

A local event monitor now puts the click back at the real pointer (NSEvent.mouseLocation) and holds the release back until NSEvent.pressedMouseButtons says the button is up, sending drags in between so sliding off cancels. Press-and-hold stays as it was. Where the system already reports the right position and release, events pass through untouched. The event has to be replaced before dispatch: SwiftUI reads the location from the window's current event, not from the one passed to mouseDown.

I also switched the ring and pills to the label colour. They were fixed white and charcoal, so the ring was nearly invisible on a light menu bar.

Tests: event tests for hold, short tap, slide-off and the eye and countdown clicks (events sent at the item's centre with an instant release, like macOS 27), plus a light/dark ink check. I ran the UI tests locally and left the full suite to CI.

By hand on a MacBook Pro (M1), macOS 27.0.1, light menu bar: holding the ring ends the session. Not tried: macOS 26, a dark menu bar by hand, an external display. I added a line to the real-Mac checklist.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no blocking issues remain.

Summary

Fixes menu bar clicks and press-and-hold on macOS 27, and makes the ring and pills follow the label colour.

  • Menu-bar clicks reach the control under the pointer on macOS 27.
  • Menu-bar pills and the end ring follow the light or dark bar.

No new actionable issues found. AppKit tests and visual checks could not run in this Linux environment.

Reviews (7) · Last reviewed commit: "Menu bar: the click correction is macOS ..." · Reviewed by Greptile

macOS 27 delivers a status item click at the middle of the item, with the
release already sent while the button is still down. So a press on the end
ring landed on the countdown and a hold never lasted. Read the pointer and
button state ourselves, and draw the ring and pills in the label colour so
they show on a light menu bar.
Comment thread Sources/Insomnia/UI/StatusItemController.swift Outdated
Comment thread Sources/Insomnia/UI/StatusItemController.swift
…d closes

Only an event at the middle of the item's window is moved to the pointer, so
an accurate click that sat in the queue keeps its place. The hold now checks
that the button is still down when the ring closes, so a release just before
the deadline can't still end the session. Tests for both.
@robbopp

robbopp commented Oct 12, 2026

Copy link
Copy Markdown

Tested on macOS 27.0.1 (26A434): built main (db9085e) with this PR merged on top. Press-and-hold on the end ring now ends the session, and a quick click doesn't. Keyboard backlight is also restored after lid open on this build (it wasn't on v0.1.0).

@BurakErdemci

Copy link
Copy Markdown

Tested this on macOS 27.0.1, MacBook Air M4 (Xcode 27.0), built from ac5f2e5 with #71 merged on top (it only touches the lid-close path).

step result
hold the end ring ends the session ✅
single click on the end ring does not end it ✅
click the countdown extend pills open, after a 1–2 s delay ✅
with extend open, click Minutes the extend field opens ✅
click the eye, then Minutes while Hours is selected the pills collapse, Minutes is never selected ❌

The last one is the global mouse monitor in installMonitors (StatusItemController.swift:774): on macOS 27 the click on the item's own window reaches it as well, and it collapses before the pill sees the click. #76 filters that monitor for clicks inside the status item's window (shouldIgnoreGlobalClick); something like that on top of this PR would cover the pill case. The hold-to-end and light-bar parts here are better than the double-click fallback in #76.

On macOS 27 a click on the status item also reaches the global mouse
monitor, which collapsed the open pills before a pill could take the click.
Ignore global clicks that land on the item's own window.
@hasin023

Copy link
Copy Markdown
Contributor Author

@BurakErdemci thanks for testing. The pill case was the global click monitor: I pushed a fix in a2cec5d that ignores global clicks landing on the item's own window.

One question: does the 1–2 s delay when you click the countdown also happen on main without this PR (or with #76)? I haven't seen it on my M1 on macOS 27.0.1, so I'd like to know if it comes from this change.

@hasin023

Copy link
Copy Markdown
Contributor Author

@robbopp thanks for testing as well. Good to know the hold works and a quick click doesn't on macOS 27.0.1 (26A434).

@BurakErdemci

Copy link
Copy Markdown

Retested on a clean checkout of a2cec5d (git status --short empty, nothing else applied), installed with ./scripts/install.sh. macOS 27.0.1 (26A434), Mac16,12, Apple M4, Xcode 27.0.

check result
eye, then Minutes while Hours is selected, then Days pills stay open, focus moves, a 2-minute session started ✅
hold the end ring session ended 0.64 s after mouse-down, before release ✅

On the 1–2 s delay: it isn't click latency. I logged mouse down/up and the status item's width (through accessibility, 50 Hz) over four sessions. The end ring appeared 4.6–4.8 s after the countdown did, every time, with or without a click on the countdown (one run had no click at all). That is the .starting → .running switch: showsRunningControls needs isRunning, and insomnia.log writes "session started until …" about 5 s after the start the session counts from (e.g. until 16:24:53Z (120s requested) logged at 16:22:58Z). A click during that window just meets the ring when it lands. This PR doesn't touch that path, so main and #76 behave the same. I didn't time which step of the start takes the 5 s.

Not run: single click on the ring, light menu bar.

Removes a build warning about reading the host view's frame off the main actor.
@hasin023

Copy link
Copy Markdown
Contributor Author

@greptileai review

SergeiNikolenko added a commit to SergeiNikolenko/Insomnia that referenced this pull request Oct 16, 2026
@krishhgg

Copy link
Copy Markdown
Owner

End-to-end test in a VM on macOS 27.0.1

Hold-to-end works with this PR on macOS 27.0.1, and the ring now shows on a light menu bar. Holding the ring for 1.5 s ended the session in 6 of 6 tries, on a light and on a dark menu bar. On main the same hold ended it in 0 of 6. The press landed on the countdown and opened the duration pills instead, as the PR describes. On a light menu bar, main's white ring almost disappears; this PR draws it dark.

Setup

A Tart VM on the maintainer's Mac mini, macOS 27.0.1 (26A434) with Xcode 27.0. Each version was built from source and installed with scripts/install.sh, then driven by a script inside the VM using Accessibility and posted mouse events.

Main (76e517d) against this PR's head (2ffa415). The scripts (t93b.sh for the light bar, t93c.sh for the dark one) set the wallpaper to Solid Colors Silver (light bar) or Mac Blue (dark bar). Each try started a session by clicking the eye and pressing Enter, and waited for the log's "session started until" line. Then hold.c moved the pointer onto the ring (12 pt from the status item's right edge), pressed the left button, held it 1.5 s with no movement and released it. A try counted if the log said session end (user).

Results

main this PR
Light bar, 3 holds 0 ended; each press opened the duration pills 3 ended (session end (user))
Dark bar, 3 holds 0 ended; each press opened the duration pills 3 ended
Ring on a light bar white, barely visible dark, visible
Ring on a dark bar white white, same as main

On a light bar during a session, main on top and this PR below:

light main

light this PR

On a dark bar, main on top and this PR below:

dark main

dark this PR

0.7 s into the hold, this PR (the ring pressed):

mid-hold this PR

After the hold on main (the pills opened):

after hold main

An earlier run that held 4 s after SleepDisabled turned 1, without waiting for the log line, gave 0 of 3 on main and 2 of 3 on this PR. Its first PR hold did not end the session, and the log doesn't say why. That run is in run-full-lab27.log.

Not covered

  • A real trackpad or mouse, a short tap, and sliding off the ring. These are posted events with no movement during the hold.
  • An external display, macOS 26 and real hardware.

The full logs are run-followup-lab27.log (light bar) and run-followup3-lab27.log (dark bar).

🤖 Generated with Claude Code

@krishhgg krishhgg closed this Oct 18, 2026
@krishhgg krishhgg reopened this Oct 18, 2026
@krishhgg
krishhgg marked this pull request as draft October 18, 2026 04:05
@krishhgg
krishhgg marked this pull request as ready for review October 18, 2026 04:05
@krishhgg

Copy link
Copy Markdown
Owner

Thanks for this. Two things to fix before it can merge. Both are edge cases in the new click handling, where a click or a hold can do the wrong thing.

1. The macOS 27 correction can also move a real macOS 26 click

StatusItemController.swift:934 treats any event at the exact middle of the window as the relayed macOS 27 click. A real click on macOS 26 can land there too. If that click waits in the queue while the pointer moves onto the end ring, atPointer moves it onto the ring. A short click then fails to open the pills, and a click that is held can end the session.

Fix: apply the correction only on the macOS 27 path that relays the click. Add a test with an accurate event at the middle of the window while the live pointer is somewhere else, and check that the event passes through unchanged.

2. Sliding off just before the ring closes can still end the session

In HoldToEndButton.swift:83, the drags that cancel a hold come from a separate 60 Hz timer. Say the pointer crosses the 12-point cancel distance after the timer's last poll. The completion task then sees holding == true and the button still down, so it fires before the next drag can cancel it.

Fix: record the pointer position when the hold begins, and check the live distance from it before fire(). Add a test where the completion runs before the next movement poll.

Push the fixes as new commits on this branch (no rebase or force-push). CI, Greptile and the Codex review then run again on the new head. My VM test above covered 2ffa415, so I'll rerun it on the new head.

🤖 Generated with Claude Code

@abed-daloopa

Copy link
Copy Markdown

Hey @hasin023, @krishhgg

Could we please get this merged soon? 🙏

Thanks in advance

…he deadline cancels the hold

On macOS 26 an event at the middle of the item is a real click, so the local monitor now exists on macOS 27 and later only. When the ring closes it also checks how far the pointer moved since the hold began, so a slide made after the last poll still cancels. Tests cover both, and the release checklist gets a macOS 26 line.
@hasin023

Copy link
Copy Markdown
Contributor Author

@krishhgg thanks. Both are fixed in e537b3a, as a new commit on this branch.

  1. The click monitor now exists on macOS 27 and later only. On macOS 26 an event at the middle of the item is a real click and is left where it happened. testOnMacOS26AClickAtTheMiddleIsNotMovedToWhereThePointerIs covers it.
  2. When the ring closes, it now also checks how far the pointer moved since the hold began (12 pt, as before). A slide made after the last poll cancels the hold. testSlidingOffBeforeTheRingClosesDoesNotEndTheSessionEvenIfNoDragArrived covers it, with no drag event delivered.

I also added a macOS 26 line to the Menu bar checklist in docs/releasing.md. Full suite on macOS 27.0.1 (M1): 1,246 tests, 0 failures, 13 skipped (tmux is not installed here). I did not try the fixes by hand on macOS 26.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants