Skip to content

Add the tray's macOS head - #991

Merged
SimonCropp merged 3 commits into
mainfrom
tray-mac
Oct 29, 2026
Merged

SimonCropp merged 3 commits into
mainfrom
tray-mac

Conversation

@SimonCropp

Copy link
Copy Markdown
Member

Part of phase 3 of the cross-platform tray plan: the tray on macOS. Not packed yet; how the heads ship is phase 4.

The head. src/DiffEngineTray.Mac is an Exe named DiffEngineTray, as the Windows and Linux heads are. It is the Linux head step for step, with a status bar item in place of the StatusNotifierItem: the core's tracker and inline queue, the same menu (Exit, "Run at startup", Debug view, Open logs, Raise issue), and warnings through macOS's notifications. "Run at startup" is a launch agent, through PlatformHeads' RunAtLogin.ForPlatform. Built in Release-NotWindows only.

The library (decision G). The status bar item is PlatformHeadsMac's StatusItem in a native library of its own, diffengine_tray, so a tray change never moves the viewer's ABI version. It is a second product of native/swift (Sources/Detray, with a CDetray bridge to the header), and PlatformHeads 0.8.0's mac-libraries has build-native collect it into src/DiffEngineTray.Mac/runtimes in the same run as the viewer's. The binaries came in through #990.

The ABI. native/include/detray.h, DETRAY_VERSION 1, which the head checks before anything else. A C struct cannot hold a list of itself, so the menu goes over as one array of DetrayItems, each with its parent's index (-1 for the top level), and the Swift side makes it a tree again. Strings are UTF-8, allocated and freed by the managed side around each call. The icons and glyphs are handed over once at start, as PNGs. detray_poll returns the chosen items in the order they were chosen. detray_describe writes the menu as the library rebuilt it, for the tests. The managed half (Detray, DetrayItem, MacTray) is in the core, as ProcessEx's native calls are, so the core's tests can reach it.

The main thread. AppKit is driven from the main thread only, so the head's Main is synchronous, and the loop is the core's new TrayLoop.RunPumped, on the thread Main starts on, with the library's pump as its wait (a tenth of a second at most, less once there is input). Nothing in Program.Inner awaits before the loop returns. TrayLoop.Run is unchanged, and still what the Linux head uses.

The app. An accessory, with no Dock icon and no menu bar of its own. App Nap is off (NapGuard), as the viewer has it, or the sockets and scan are answered late. A logout reaches an app with no Dock icon as a quit, which TerminateGuard refuses and reports, and the loop then ends the way Exit does, staging the queue on the way out. SIGTERM and SIGHUP are SessionEndSignals', as on Linux.

No status bar. With no library for the machine, one of another version, or no button from AppKit, the head runs as Linux does with no tray host: it holds the queue, tracks what test runs send it, and says once, in a notification, why there is no icon.

Shared now. The pictures move to the core as TrayPictures (the Linux head's TrayImages, now with the PNGs the Mac library takes), embedded in the core from the Windows head's files, and TrayLogin moves to the core.

PlatformHeads. All four packages and the native pins (native/CMakeLists.txt, build-native.yml) move to 0.8.0. The viewer's dylib was rebuilt against it in #990; its Linux binaries came out the same.

CI. The macOS jobs treat the tray's dylib as they treat the viewer's. The unix job builds it from source before testing, so the tests use the Swift as it is now. The native job checks both committed dylibs have both slices, and on osx-arm64 runs DiffEngineTray.Core.Tests against the committed one.

Tests:

  • MacTrayTests.EachItemFollowsTheItemWhoseSubmenuItIsIn: the flattened menu, in order, over a menu with a glyph, a toggle each way, a separator, a disabled heading, a submenu in a submenu and text beyond ASCII.
  • MacTrayTests.TheLibraryRebuildsTheMenuItWasHanded, macOS only: the library's version, then that menu handed over and read back with detray_describe, every field of every item. It needs no status bar, so it runs on CI. This PR's macOS jobs are the first time it runs.
  • TrayLoopTests.APumpedLoopStaysOnItsThread: on a thread of its own, the pump and a chosen item both run on the thread that started the loop, the menu is applied, and Exit ends it.
  • Windows: the full solution in Release passes (3,785 tests, 0 failed, 49 skipped).
  • Not seen on a Mac: the status bar item itself, its menu opening, a choice reaching the loop, or a logout. There is no Mac here, so those wait for one.

SimonCropp and others added 3 commits October 29, 2026 09:35
src/DiffEngineTray.Mac is the tray on macOS: the core's tracker and
inline queue, with a status bar item from a native library of its own,
diffengine_tray (decision G), so a tray change never moves the viewer's
ABI version. It is PlatformHeadsMac's StatusItem behind
native/include/detray.h, a second product of native/swift, which
build-native collects into the head's runtimes through PlatformHeads
0.8.0's mac-libraries.

The menu crosses the ABI as one array, each item with its parent by
index, and the Swift side makes it a tree again. The managed half,
Detray and MacTray, is in the core, as ProcessEx's native calls are, so
MacTrayTests hands the library a menu and reads back what it rebuilt on
the macOS job.

AppKit is driven from the main thread only, so the head runs
TrayLoop.RunPumped on the thread Main starts on, with the library's pump
as its wait. The app is an accessory with no Dock icon, App Nap is off,
and a logout is TerminateGuard's, ending the loop as Exit does.

The pictures and the login item move to the core for both heads off
Windows. Every PlatformHeads pin moves to 0.8.0.
Co-authored-by: SimonCropp <122666+SimonCropp@users.noreply.github.com>
The macOS jobs treat the tray's dylib as they treat the viewer's. The
unix job copies the freshly built libdiffengine_tray.dylib into the
head's runtimes before testing, so MacTrayTests hands its menu to the
Swift as it is now, not as it was at the last build-native rebuild. The
native job checks both committed dylibs for both slices, and on
osx-arm64 runs DiffEngineTray.Core.Tests against the committed one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant