Skip to content

fix: keep the expand/collapse arrow out of the hidden zone on macOS 26 - #381

Open
antib0t wants to merge 1 commit into
dwarvesf:developfrom
antib0t:bugfix/fix-arrow-hidden-by-own-collapse-macos26
Open

fix: keep the expand/collapse arrow out of the hidden zone on macOS 26#381
antib0t wants to merge 1 commit into
dwarvesf:developfrom
antib0t:bugfix/fix-arrow-hidden-by-own-collapse-macos26

Conversation

@antib0t

@antib0t antib0t commented Jul 28, 2026

Copy link
Copy Markdown

fix: keep the expand/collapse arrow out of the hidden zone on macOS 26

Refs #336

Issue description

On macOS 26 the app launches, the icons collapse, and the expand/collapse arrow is nowhere in the menu bar. There is no control left to click, so the bar cannot be expanded again. From the outside this reads as "Hidden Bar stopped working", or as the app hanging on launch: the process is alive and the collapse worked, but the app has hidden its own only UI.

Quitting and relaunching does not help. The state that causes it lives in the app's defaults and survives an uninstall, so reinstalling does not help either.

Reproduced on macOS 26.5.2 (25F84), Apple silicon, single display. Present in v1.10 and in current develop.

Root cause

The launch collapse runs before the status items are placed

macOS 26 positions status-item windows asynchronously. For roughly the first second after launch, NSStatusBarButton.window?.frame.origin is still (0, 0) for every item. StatusBarController collapses on a flat 1s delay and gates that collapse on isBtnSeparateValidPosition, which compares those origins:

return btnExpandCollapseX >= btnSeparateX   // LTR

With both origins at zero the comparison is 0 >= 0, so the guard passes on geometry that does not exist yet. The app collapses without ever checking where its own arrow is.

Measured on 26.5.2, same launch, one second apart:

pre-collapse: arrowX=0.0     sepX=0.0     validPos=true   <- collapse happens here
pre-collapse: arrowX=-2991.0 sepX=-2566.0 validPos=false  <- real geometry, too late

A stale saved order puts the arrow inside the hidden zone

macOS persists each item's slot as NSStatusItem Preferred Position <autosaveName> in the app's own defaults. A larger value places the item further left (verified on 26.5, see below). The app only works when the group reads, left to right: always-hidden, separator, arrow. The arrow has to stay to the right of the separator, because collapsing inflates the separator and pushes everything to its left off-screen.

A real user's defaults, carried over from an earlier install:

"NSStatusItem Preferred Position hiddenbar_expandcollapse" = 1000;
"NSStatusItem Preferred Position hiddenbar_separate" = 717;

1000 > 717, so the arrow sat to the left of the separator, inside the zone the separator hides. The collapse then swallowed the arrow along with the user's icons:

HideMechanism: requested=4112.0 windowWidth=4128.0 buttonWidth=4112.0 length=4112.0
DIAG: arrowX=-2991.0  (screen is 0...2056)

The order check exists precisely to prevent this. It never got the chance to run on real numbers, because of the race above.

One correction to #360 while I was in here: on 26.5.2 the hide mechanism itself is fine. The separator's requested 4112pt is honored exactly, and icons do move off-screen. Counting menu-bar windows via CGWindowListCopyWindowInfo at layer 25: 22 visible without the app, 12 with it collapsed. Length inflation on 26 works.

Direction of the preferred-position value

The repair depends on knowing which way the value points, so I measured it instead of assuming. Two status items, positions written before creation:

position 800 -> x=1357.0
position 300 -> x=1745.0
=> larger preferred position is further LEFT

An item with no saved value is placed by creation order, outside every item that owns a slot. That is why a partially saved set is normal rather than corrupt, and why the repair treats a missing value as outermost.

Steps to reproduce

The app's own defaults are what trigger this, so the reproduction seeds them directly. It is the state a real user arrived at through ordinary Cmd-dragging.

  1. Quit Hidden Bar.
  2. Seed a saved order that puts the arrow to the left of the separator:
    defaults write com.dwarvesv.minimalbar "NSStatusItem Preferred Position hiddenbar_expandcollapse" -float 1000
    defaults write com.dwarvesv.minimalbar "NSStatusItem Preferred Position hiddenbar_separate" -float 717
  3. Have enough third-party menu-bar icons that some sit to the left of the separator's slot.
  4. Launch Hidden Bar on macOS 26 and wait for the automatic collapse.

Before this PR the icons collapse and the arrow is gone, with no way to expand the bar again from the menu bar. After this PR the arrow stays visible at the inner edge, the same icons collapse, and clicking the arrow expands them.

To restore your own arrangement afterwards, delete both keys and let macOS re-place the items.

Fix details

Three changes in StatusBarController.swift.

Unplaced geometry now counts as unknown rather than valid. A new isMenuBarGeometryReady requires both button windows to report a non-zero origin, and both order checks gate on it. A check that cannot see the layout answers no.

The launch collapse waits for real geometry. collapseWhenGeometryReady() replaces the flat asyncAfter(.now() + 1), retrying every 0.25s up to 20 attempts. If geometry never arrives the collapse still runs, and the order check, now honest, refuses it.

The saved order is validated and repaired before the items exist. macOS reads the slot when the item is created, so StatusItemPositions.repairIfNeeded() runs as the first statement of init(), ahead of the first statusItem() call. Nothing is rewritten while the order is sound.

The repair anchors on the separator and never moves it. An earlier version of this patch reassigned all the slots in sorted order. It put the arrow back on screen and quietly broke hiding, because the separator's slot is what defines the hidden zone, and moving it un-hides everything parked there. So the separator keeps its slot and only the misplaced items move, one slot's width to their own side:

stale order (arrow=1000.0 separator=717.0 alwaysHidden=nil)
  -> repaired to (arrow=667.0 separator=717.0 alwaysHidden=nil)

Finally, a bounded rescue. If a collapse ever leaves the arrow off every screen anyway, rescueArrowIfSwallowed() expands the bar, pulls the arrow inside the separator, and rebuilds the two status items so the arrow returns in the same session instead of after a restart. It is capped at two attempts, so a repair that does not take cannot turn into collapse/rescue ping-pong.

Two smaller things came along with it. The HideMechanism: diagnostic now logs the arrow's frame and whether it intersects any screen, which is the signal this class of bug turns on. The .prefsChanged observer moved out of getContextMenu() into init(), since the rescue path rebuilds the UI and would otherwise register a duplicate.

Testing

macOS 26.5.2 (25F84), Xcode 26.6, Release build, single 2056pt display. Menu-bar icon counts come from CGWindowListCopyWindowInfo at layer 25. Arrow visibility comes from the button window's frame checked against NSScreen.screens.

Scenario Result
Stale order arrow=1000, sep=717 repaired to arrow=667, arrow on screen, 9 icons hidden
Sound order no repair, no rewrite
Arrow slot saved, separator slot absent no repair (this is normal, not corruption)
Reversed variant arrow=900, sep=400 repaired, arrow on screen
Three consecutive relaunches 0 repairs, arrowOnScreen=true, 13 visible each time
Fresh defaults, nothing saved no repair, arrow on screen

The repair is idempotent. It fires once on a broken state and stays quiet afterwards.

Three things I could not cover, for lack of hardware: multi-display, RTL, and macOS 27. The RTL paths mirror the existing LTR comparisons and are written but untested. NSApplication.shared.userInterfaceLayoutDirection is read directly instead of Constant.isUsingLTRLanguage, because the latter is only assigned in applicationDidFinishLaunching, which runs after the status items are built.

macOS 26 places status-item windows asynchronously: for roughly the first
second after launch every button window still reports origin (0,0). The launch
collapse ran on a flat 1s delay and decided whether the arrow sat to the right
of the separator by comparing those origins, so the check reduced to 0 >= 0 and
was trivially true. The bar collapsed before the order could be verified.

Combined with a stale "NSStatusItem Preferred Position" that placed the arrow
to the left of the separator, that collapse pushed the app's own control
off-screen. The bar could no longer be reopened by clicking, because the arrow
had been hidden inside the zone it just collapsed.

- Treat unplaced geometry as unknown, never as a valid order.
- Wait for real geometry before the launch collapse instead of a fixed delay.
- Validate the saved item order before the status items are created and repair
  it around the separator. The separator keeps its slot, so the hidden zone
  stays where the user put it and only the misplaced items move.
- Add a bounded rescue that expands and rebuilds the items if a collapse ever
  leaves the arrow off every screen.

Refs dwarvesf#336
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.

1 participant