Skip to content

Restore menu bar hiding, Layout, and the Ice Bar on macOS 27 - #997

Open
WuColin-1 wants to merge 119 commits into
jordanbaird:mainfrom
WuColin-1:macos-27
Open

WuColin-1 wants to merge 119 commits into
jordanbaird:mainfrom
WuColin-1:macos-27

Conversation

@WuColin-1

Copy link
Copy Markdown

Summary

Makes Ice usable on macOS 27 again: hiding, the Menu Bar Layout editor, and the Ice Bar. Only public APIs are used for hiding. Everything new is gated on #available(macOS 27.0, *).

macOS 27 moved every status item into MenuBarAgent. There are no per-item windows any more, and an oversized status item (Ice's 10,000-point divider) is discarded instead of pushing items off screen. That's why current builds show "Loading menu bar items" and hide nothing (#954, #965, #970, #991, #992).

What's in this branch

This builds on the maintainer's macos-26 branch, not main. Against main the diff includes all 77 macos-26 commits, so the macOS 27 work is easiest to review as macos-26...macos-27 (33 commits of our own, plus #980 and #950).

  • Add experimental macOS 27 menu bar compatibility #980 (PWB97): accessibility-based item enumeration, and hiding via an Ice-owned spacer that pushes items into the native overflow.
  • fix(xpc): make MenuBarItemService work for ad-hoc-signed builds on macOS 26 #950 (pdurlej): lets MenuBarItemService work with ad-hoc-signed builds.
  • Our changes:
    • Removed QA logs and probe tools that posted real input or used the private assessment-mode API.
    • Safer native drags:
      • Ice only drags its boundary within 3 s of a user toggle.
      • It waits until no modifier or mouse button is held and nothing was typed in the last 0.5 s.
      • Keyboard input is suppressed while the synthetic Command key is down.
      • MenuBarAgent-hosted items can't be dragged from Layout; those drags crashed MenuBarAgent in Add experimental macOS 27 menu bar compatibility #980's testing.
    • Safety check: if widening a spacer pushes Ice's own button off the bar, Ice withdraws the spacers.
    • Boundary alignment on 27.0 (26A428): hit testing returns MenuBarAgent's unidentified host button. It's now accepted only when its frame matches the boundary's frame.
    • Spacer sizing: based on Ice's position relative to the notch. Concealment changes are coalesced (one per 300 ms), so rapid clicks don't stack overflow animations.
    • Ice Bar on macOS 27:
      • Hidden items stay concealed, and only the bar opens and closes.
      • Clicking an item briefly reveals the hidden items, clicks the item where MenuBarAgent draws it, and conceals them again after its menu closes.
      • The bar opens without the blocking cache refresh, which always hit its 1 s timeout.
      • It's placed under Ice's button and closes on outside clicks through a passive monitor.
      • There's a new background opacity slider.
    • Glyph capture:
      • The menu bar strip was captured at 1x on Retina displays, because CGDisplayPixelsWide returns points; it's now captured at full resolution.
      • Glyphs are extracted from the bar background, trimmed, and centered.
      • MenuBarAgent's overflow button is excluded from items. Items whose frames overlap it or each other are treated as not drawn, since they're in the system overflow; they show their app icon instead of a garbage crop.
    • Troubleshooting: a DebugDumpMacOS27Glyphs default (off unless set) writes captures and frames to the Caches folder.
    • Unrelated fix: restored a smart-rehide delay that Add experimental macOS 27 menu bar compatibility #980 changed without an availability check for macOS 14–26.
    • Docs: MACOS27.md covers the approach, limitations, and how to build an ad-hoc copy.

Testing

On a notched MacBook, macOS 27.0 (26A428), Xcode 27.0, Release build signed ad hoc:

  • Hiding and showing with Ice's button, including rapid clicks.
  • Layout drags moving items in the real menu bar.
  • Opening the Ice Bar and the sharpness of its glyphs.
  • The standalone decision tests (NativeMenuBarBoundaryTests, NativeDragVisibilityStateTests, MacOS27DynamicItemStateTests, MenuBarGlyphImageTests) pass.

Not yet tested:

  • The last functional commit (system-overflow detection).
  • The opacity slider.
  • Clicking an item from the Ice Bar after the overflow change.
  • Multiple displays and non-notched displays.
  • Regressions on macOS 14–26.

Known limitations

  • Other apps' status item images aren't available through any API, so Ice Bar glyphs are crops of the menu bar. Items that macOS keeps in its own overflow can't be captured and show their app icon.
  • Clicking an Ice Bar item reflows the menu bar once while it's revealed.
  • Search, show on hover/click/scroll, auto-rehide, spacing, and app-menu hiding remain disabled on macOS 27.
  • Ad-hoc builds must disable hardened runtime (ENABLE_HARDENED_RUNTIME=NO), or library validation rejects the embedded Sparkle framework.

🤖 Generated with Claude Code

Misc additional changes and cleanup
`RunLoopLocalEventMonitor` seems to prevent certain buttons from receiving events in macOS 26 Developer Beta 1. This might be a bug in the beta, or `RunLoopLocalEventMonitor` itself. For now, let's just move to a better mouse check implementation that doesn't use continuous event monitoring.

Note the `FIXME` (line 1057). The previous implementation had this problem too, but it was never documented.
A big part of this is hopefully a temporary measure. We need an accurate identifier for every item, and the old way just isn't cutting it right now. Items are all owned by the Control Center in macOS 26, and try as I might, I couldn't find a great way to get the _actual_ host apps for the items.

Must dig deeper into the mines...

Oh yeah, there's also a bunch of random stuff here too that I don't really want to explain. Just know that it probably all fixed something.
This is what I get for trusting Xcode to update my build settings
- Refactor screen capture
- Rework menu bar item getters
- Update immovable/non-hideable info lists
- Remove OSLog wrapper
- Minor migration rework
- Remove old entitlements file
Should also fix a crash when accessing `ControlItem.windowNumber`.
- Rework app lifecycle
- Rework how windows are initialized
- Update documentation comments
- Refactoring and cleanup
This should fix some performance issues that occur during mouse tracking operations (e.g. highlighting a button on hover).
We also store the status item and layout constraint in a separate storage class. Not sure I like this, but the idea is to convey the tightly coupled relationship between the constraint and status item, and to ensure that they are initialized (and deinitialized) at the same time.
Relying on the default behavior seemed to work fine, but is now broken in the macOS 26 Developer Beta. Probably better to handle it explicitly, even if it is just a beta bug.
WuColin-1 and others added 30 commits September 16, 2026 22:15
The dated QA logs under docs/ and the probe tools under Tests/ posted real
input, moved real menu bar items, or activated the private assessment-mode
assertion. Keep only the pure decision/state tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Only drag Ice's hiding boundary within 3 s of an explicit user toggle
  (Ice button, menu, or hotkey); automatic cache refreshes and launch
  keep items expanded instead of taking over the pointer.
- Wait for the user to stop typing, pointing, and holding modifiers or
  buttons before a native Command-drag, and give up after 5 s.
- Suppress local keyboard and mouse events while the synthetic Command
  key is down, so keystrokes can't become Command shortcuts.
- Don't let Layout drag MenuBarAgent-hosted system items, which crashed
  MenuBarAgent's input loop in testing.
- Restore the 250 ms smart-rehide delay on macOS 14-26, which the
  macOS 27 work changed without an availability check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The always-hidden spacer's position is a guessed preferred position and is
never aligned. If it lands to the right of Ice, widening it pushes Ice's
button into the native overflow with no way to click it. After a spacer is
widened, confirm through Ice's own accessibility frames that the button is
still on the bar and right of every widened spacer; otherwise withdraw the
spacers and show all sections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Upstream PR jordanbaird#903 introduced MenuBarItemService.xpc with this line:

    listener = try XPCListener(service: name,
                               requirement: .isFromSameTeam()) { ... }

`.isFromSameTeam()` requires the listener and every peer to be signed
with the same Apple Team Identifier. That works for builds Jordan
produces from his Developer ID Application certificate. It does NOT
work for any ad-hoc-signed build (TeamIdentifier = empty), because
empty-vs-empty is never treated as a match — the listener rejects
every check-in attempt with "Bogus check-in attempt. Ignoring." and
"Dropping check-in message due to code signing requirement".

The visible symptom is the Menu Bar Layout settings pane spinning
forever on "Loading menu bar items…" — XPC never returns, so Ice
never gets the cached item snapshots. This is the same class of bug
reported in upstream issues jordanbaird#744 (46 reactions) and jordanbaird#891 (30 react-
ions), and it bites every community fork that ships without an Apple
Developer Program account — which is roughly every community fork.

This commit reads the running process's actual Team Identifier via
SecCodeCopySigningInformation; if it's nil (ad-hoc / unsigned), we
fall through to the no-requirement activation path. If it's set
(properly Developer-ID-signed build), behaviour is unchanged — the
strict same-team requirement still applies.

For our fire fork:
  - 0.11.13-fire.0..fire.1 (ad-hoc, this branch's defaults) → fixed.
  - 0.11.13-fire.2+ once signed with our Developer ID → unchanged,
    still uses .isFromSameTeam() because we'll have a team ID.

Also bumps MARKETING_VERSION 0.11.13-fire.1 → 0.11.13-fire.2 and
CURRENT_PROJECT_VERSION 1123 → 1124 so Sparkle in installed fire.1
recognizes this as a newer build and offers the update.
Companion to the listener-side fix in the previous commit. The
XPCSession client (Ice/MenuBar/MenuBarItems/MenuBarItemServiceConnection.swift)
also defaults to .isFromSameTeam() peer requirement, which silently
rejects ad-hoc-signed builds the same way the listener did.

The refactor introduces MenuBarItemService.ownTeamIdentifier() in the
shared layer so both sides query exactly the same predicate. Developer-
ID-signed builds keep strict same-team enforcement; ad-hoc community
builds skip both requirements (Listener accepts, Client sends).
Describe what works and what doesn't on macOS 27, how hiding works, the
hardening added on top of the macOS 27 compatibility work, and how to
build an ad-hoc-signed copy with Xcode 27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Library validation rejects the embedded Sparkle framework, which keeps
Sparkle's Team ID, so an ad-hoc build with hardened runtime crashes at
launch with "Library missing".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Requiring the pointer to be still for 250 ms made Layout drags time out
while the user kept pointing after a drop. Local input is suppressed
during the drag, so only held modifiers, pressed buttons and recent
typing need to block it. Log which condition blocked on timeout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On macOS 27.0 (26A428), system-wide AX hit testing at the narrow hosted
boundary returns no element, so every Layout move refused to align the
boundary. When the hit test finds no identified element, accept the drag
only if Ice's own fresh frame still matches and no other item's frame
contains the drag start. A hit on a different identified item still
refuses the drag.

Also log macOS 27 visibility decisions (once per change) and the reasons
alignment fails, so hide problems can be diagnosed from the log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spacer length assumed Ice sat at the right edge of the region right of
the notch. With many visible items, Ice sits well left of that edge, or
even left of the notch, so a 719-point spacer didn't fit, was discarded,
and nothing hid.

Compute the length from Ice's live frame, the notch, and the end of the
application menus: fill the space left of Ice, or, when Ice is right of
the notch, exceed the remaining gap there so the spacer moves left of the
notch and fills that side.

Also permit local mouse events during native drags (only keyboard input
is suppressed, matching the state the drags were validated with) and log
where a native drag left the item when its order can't be verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On macOS 27.0, hit testing at Ice's boundary returns MenuBarAgent's
unidentified host button instead of Ice's own element, so every Layout
move refused to align the boundary. Accept an unidentified hit element
only when its frame matches the boundary's frame, Ice's own frame still
matches, and no other item contains the drag start. Log each condition
when refusing, and log when a spacer is withdrawn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every click started a new overflow change while MenuBarAgent was still
animating the previous one, so rapid toggles left hidden items flashing
and the bar out of step with Ice's icon. Apply at most one concealment
change every 400 ms, always with the latest requested state; the icon
still updates on every click.

While already concealing, keep the spacer at the length that hid the
items instead of recomputing it every five seconds. Ice's button moves
once items overflow, and resizing to match made the whole bar reflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getApplicationMenuFrame() finds no menu bar on macOS 27, so the spacer
was sized as though the app menus ended at the screen's left edge. The
oversized spacer didn't fit beside the menus, and MenuBarAgent moved Ice's
button and the visible items across the notch on every toggle, making
positions jump and the animation slow.

Read the frontmost application's menu frames from its accessibility menu
bar, record Ice's position before concealing, and recompute the length
when another app becomes frontmost. Shorten the toggle coalescing
interval to 300 ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On macOS 27, status items can take the space of the application menus,
so a spacer that ended at the app menus let hidden items slide left
instead of overflowing.

This reverts commit 22a942d.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clicking Ice's icon with the Ice Bar enabled now shows the hidden items in
a bar below the icon, as on earlier versions of macOS.

- Keep hidden items concealed in the menu bar while the Ice Bar is in use.
  Only the Ice Bar opens and closes on click, so the menu bar no longer
  reflows on every toggle.
- Concealed items aren't drawn on macOS 27, so capture their images just
  before concealing them and whenever they're revealed, and fall back to
  the owning app's icon when no image exists.
- Clicking an item in the Ice Bar reveals the hidden items, clicks the item
  where MenuBarAgent draws it, and conceals the items again once the menu
  or window it opened closes.
- Place the Ice Bar below Ice's button using its accessibility frame, close
  it on clicks in other apps with a passive monitor, and stop closing it
  because the removed hidden-section control item has no frame.
- Don't republish the boundary handle on every cache refresh after an
  automatic hide couldn't align it; wait for a user action instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ce Bar

CGDisplayPixelsWide reports points for scaled display modes, so the menu
bar strip was captured at 1x (1710 px for a 3420 px display) and glyphs
looked soft and heavy when drawn at 2x. Use the display mode's pixel width.

Other apps' status item images aren't available through any API, so the
Ice Bar still shows crops of the menu bar. Remove the bar's background
from them, as Layout does: monochrome glyphs become templates that take
the Ice Bar's foreground color, and colored glyphs keep their colors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening the Ice Bar waited for a cache refresh that walks every running
app's accessibility tree on macOS 27 and hit its one-second timeout on
every open. Concealed items can't be read or captured then anyway, so show
the cached items at once and rely on the periodic refresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Apps report status item frames with different vertical offsets, so crops
taken from those frames drew some glyphs (such as Claude's) higher than
others. Trim each extracted glyph to its visible pixels and draw it
centered in a slot of uniform height with even horizontal padding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Settings > General > Ice Bar gets a background opacity slider. It fades
  the Ice Bar's background, tint, border and shadow; items stay opaque.
- When the DebugDumpMacOS27Glyphs default is set, write macOS 27 menu bar
  captures, per-item crops and processed Ice Bar glyphs, with their frames,
  to ~/Library/Caches/com.jordanbaird.Ice/GlyphDebug for troubleshooting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With a crowded menu bar, MenuBarAgent keeps some items in its own overflow
even when Ice reveals everything. Those items aren't drawn anywhere and
report stale frames stacked on the overflow («) button, so Ice cropped
the button and wallpaper as their Ice Bar images, and clicked the button
while believing it clicked the item. The overflow button itself was also
listed as a hidden item.

- Exclude MenuBarAgent's overflow button from menu bar items, remembering
  its frame, and mark items whose frames overlap it or another item as
  not on screen.
- Only crop images for items that are on screen; the Ice Bar shows the app
  icon for the rest.
- When a clicked Ice Bar item stays in the system overflow after revealing,
  open the system overflow instead, where the item can be clicked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An app's own AXExtrasMenuBar keeps reporting the frame its status item had
before macOS moved it. Items that left the system overflow therefore still
looked stacked on its button, so Ice treated them as not drawn and never
captured or clicked them correctly.

MenuBarAgent draws the bar, and its accessibility tree carries the positions
it actually used: one window per display, one container per item, each nesting
the owning app's element. Take frames from there, and mark an item as not
drawn when its container overlaps the overflow button or another container.

Also press a concealed item through AXPress or AXShowMenu, which works while
macOS isn't drawing it, so clicking an item in the Ice Bar no longer has to
reveal the hidden items first. Revealing remains the fallback for items that
answer neither action.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The menu bar is translucent, so a crop's background is a piece of wallpaper
that changes across the crop and can have an edge cutting through it. A single
background color left that wallpaper in the picture as a pale rectangle, and
its residual also made plain white glyphs look colored, which kept the
wallpaper instead of turning the glyph into a template.

Estimate the background at the top and bottom of every column and blend
between them, decide color from the glyph's own chroma, and drop anything too
faint or not connected to a solid part of the glyph.

Concealed items are never drawn, and a full menu bar can leave macOS no room
to draw them even when Ice reveals everything, so a picture can be impossible
to take for a long time. Save each picture under
~/Library/Caches/com.jordanbaird.Ice/IceBarImages, keyed so it survives the
runtime identities that change every launch, and add a photo pass that runs
once per launch: it gives the spacer's width back in steps, photographs
whatever macOS draws, and restores it. IceBarNoPhotoApps excludes apps whose
item changes too often to photograph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On macOS 26 and later the bar uses the system glass effect, with two settings
that work independently: Darkness picks the shade of the layer over the glass,
from white to black, and Transparency picks how much of the glass it covers,
from bare glass to solid.

Show a saved picture when the live cache has none, and never fall back to a
raw crop, which would put a rectangle of wallpaper around the glyph; the app's
icon is used instead.

A click on Ice's own button reaches MenuBarAgent, so the Ice Bar's outside
click monitor closed the bar and the button's action opened it again. Leave
clicks on that button to the action.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Multi-line conditions were aligned under their keyword, which the repository's
indentation_width rule rejects. Write them the way the rest of the codebase
does, with each condition on its own line and the brace below.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The remaining aligned continuations, in the Layout bar, the overlay panel, the
control item and the item manager, are written the same way: one condition per
line, indented once, with the brace below.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Style the Darkness and Transparency sliders like System Settings: a plain
continuous track with a symbol at each end, no percentage, and no step.

Dragging one used to catch. Each intermediate value republished the settings
object, which rebuilds the whole General pane, including the icon menus and
the launch-at-login toggle, and saved a preference. Track the drag in the
slider's own state and commit when it ends, and throttle the saves.

Two timers also blocked Ice's interface while the settings window was open:
the menu bar color sample took a screenshot on the main thread every five
seconds, and a complete accessibility walk of every running app ran just as
often. Sample the color off the main thread, and skip the walk while the user
is in a settings pane that doesn't show the items.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Icon spacing sets how much room sits between the items in the Ice Bar, from
none to 24 points, and is saved like the other appearance settings.

On macOS 26 and later the settings form had no top padding at all, so the
first row of every pane sat against the window's header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The asset catalog lives under Ice/Resources, so the icon at the top of the
README resolved to nothing on the repository page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

4 participants