diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml index baade9bc..758023c7 100644 --- a/.github/workflows/build-native.yml +++ b/.github/workflows/build-native.yml @@ -36,7 +36,7 @@ concurrency: jobs: native: - uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.13.0 + uses: SimonCropp/PlatformHeads/.github/workflows/build-native.yml@0.14.0 permissions: contents: write pull-requests: write diff --git a/claude.md b/claude.md index 522e4d7c..23d5587d 100644 --- a/claude.md +++ b/claude.md @@ -616,12 +616,12 @@ the comment there about not caching "nothing staged" asks for. **Native shim (`native/`), used by the Mac and Linux heads only:** - The build and packaging of the heads is PlatformHeads' (`D:\Code\PlatformHeads`, github.com/SimonCropp/PlatformHeads), which BuildMonitor shares: it was worked out here and - copied there, and fixes made here did not reach the copy. Seven pins name a tag of it: the + copied there, and fixes made here did not reach the copy. Eight pins name a tag of it: the `uses:` of `.github/workflows/build-native.yml`, the `GIT_TAG` in `native/CMakeLists.txt`, `PlatformHeads`, `PlatformHeads.Core`, `PlatformHeads.Linux` (the - Linux tray's) and `PlatformHeads.WindowsForms` in `Directory.Packages.props`, which move - together, and the `PlatformHeads` package - in `native/swift/Package.swift`, all at 0.13.0. So is the native runtime under both heads' drawing: the C++ + Linux tray's), `PlatformHeads.Mac` (the macOS tray's) and `PlatformHeads.WindowsForms` in + `Directory.Packages.props`, which move together, and the `PlatformHeads` package + in `native/swift/Package.swift`, all at 0.14.0. So is the native runtime under both heads' drawing: the C++ one `deview.cpp` calls as `ph::` (the window, GLFW's input as it happened, which frames are built and drawn, the renderer, the fonts and fontconfig, and the label helpers) and the Swift `PlatformHeadsMac` the AppKit head imports (`HeadWindow`, `EventPump`, `TerminateGuard`, @@ -745,21 +745,22 @@ the comment there about not caching "nothing staged" asks for. native library of its own, `diffengine_tray` (decision G), so a tray change never moves the viewer's ABI version. It is a second product of `native/swift`, built by the same `build-native` run, which collects it into the head's `runtimes` through PlatformHeads' `mac-libraries`, and - committed as the viewer's is. The ABI is `native/include/detray.h`, `DETRAY_VERSION` 2: the - menu goes over as one array of `DetrayItem`s, each with its parent by index, since a C struct - cannot hold a list of itself, and the Swift side (`Sources/Detray`) makes it a tree again. Its - managed half is in the core (`Detray`, `MacTray`), as ProcessEx's native calls are, so the - core's tests run it on the macOS job: `MacTrayTests` hands it a menu and reads back what it - rebuilt (`detray_describe`), with no status bar. The `unix` job builds it from source first, as - it does the viewer's, and the `native` job loads the committed one and checks it is universal. - AppKit is driven from the main thread only, so - the loop is `TrayLoop.RunPumped`, on the thread `Main` starts on, with the library's pump as - its wait; nothing in `Program.Inner` awaits before the loop returns. The app is an accessory, - with no Dock icon, App Nap is off (`NapGuard`), and a quit is `TerminateGuard`'s. A logout, - restart or shutdown runs `TraySessionEnd.For` from inside the pump (`detray_on_session_end`, - `MacTray.SessionEnding`, set before `detray_init` makes the guard) and is let through, since - refusing it cancelled the logout; any other quit is refused and reported (`detray_quitting`), - for the loop to end as Exit does. "Run at startup" is a + committed as the viewer's is. Its ABI and its binding are PlatformHeads': the library's target, + `Sources/Detray`, is one file that depends on `PlatformHeadsMacTray`, which SwiftPM links in, + so the library exports `platformheads_tray.h`, and the head calls it through `PlatformHeads.Mac` + (`MacTray` names the library to `StatusItemTray` and `StatusItemApp`). They were this repository's + own, `detray.h` with its binding in the core, until PlatformHeads 0.14.0 took them, so BuildMonitor's + tray could share them. `MacTrayTests`, in the core's tests, loads the committed library through + `PlatformHeads.Mac`, checks its version and reads back a menu it was handed, with no status bar. + The `unix` job builds it from source first, as it does the viewer's, and the `native` job loads + the committed one and checks it is universal. AppKit is driven from the main thread only, so + the loop is `TrayLoop.RunPumped`, on the thread `Main` starts on, with `StatusItemApp.Pump` as + its wait; nothing in `Program.Inner` awaits before the loop returns. `StatusItemApp.Start` makes + the app an accessory, with no Dock icon, App Nap off (`NapGuard`), and a quit `TerminateGuard`'s. + A logout, restart or shutdown runs `TraySessionEnd.For` from inside the pump + (`StatusItemApp.SessionEnding`) and is let through, since refusing it cancelled the logout; any + other quit is refused and reported (`StatusItemApp.Quitting`), for the loop to end as Exit does. + The `tray-install` job sends both kinds of quit. "Run at startup" is a launch agent. Packed as `DiffEngineTray.osx-x64` and `.osx-arm64` - The tray ships as .NET 10 RID specific tool packages (decision F), by `src/DiffEngineTray.Core/TrayHead.targets`, which every head imports after naming its RIDs in diff --git a/native/CMakeLists.txt b/native/CMakeLists.txt index 3497304f..0015ad46 100644 --- a/native/CMakeLists.txt +++ b/native/CMakeLists.txt @@ -15,7 +15,7 @@ include(FetchContent) FetchContent_Declare( platformheads GIT_REPOSITORY https://github.com/SimonCropp/PlatformHeads.git - GIT_TAG 0.13.0 + GIT_TAG 0.14.0 GIT_SHALLOW ON) FetchContent_MakeAvailable(platformheads) include(${platformheads_SOURCE_DIR}/cmake/PlatformHeads.cmake) diff --git a/native/include/detray.h b/native/include/detray.h deleted file mode 100644 index a3ebf1b9..00000000 --- a/native/include/detray.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * The macOS tray's ABI: DiffEngineTray's status bar item, which is PlatformHeadsMac's StatusItem. - * A library of its own beside the viewer's (diffengine_viewer), built from the same Swift package, - * so a change to either never moves the other's version. - * - * Followed by native/swift/Sources/Detray and src/DiffEngineTray.Core/Detray.cs. Bump - * DETRAY_VERSION whenever a struct changes or a field changes meaning: the managed side refuses a - * library whose version is not the one it was built for. - * - * Every call that touches AppKit is the managed side's main thread's to make, as AppKit requires. - */ -#ifndef DETRAY_H -#define DETRAY_H - -#include - -#define DETRAY_API __attribute__((visibility("default"))) - -#ifdef __cplusplus -extern "C" { -#endif - -/* - * 1: the status item and its menu. - * 2: detray_on_session_end, and a logout, restart or shutdown no longer reported by - * detray_quitting. A library from before cancels the logout that asked it to quit. - */ -#define DETRAY_VERSION 2 - -/* What detray_on_session_end calls. */ -typedef void (*DetraySessionEnd)(void); - -/* PlatformHeads' TrayMenuFlags and PlatformHeadsMac's StatusMenuFlags, which write and read them. */ -#define DETRAY_ENABLED 1 -#define DETRAY_SEPARATOR 2 -/* A toggle, drawn with a check when DETRAY_CHECKED is set as well. */ -#define DETRAY_CHECKABLE 4 -#define DETRAY_CHECKED 8 - -/* - * One menu item. The menu is one array, each submenu's items after the item that opens it, and - * parent is the index of that item in the same array, or -1 for an item of the menu itself. The - * strings are UTF-8, and read only during the call they are passed to. icon names a glyph handed - * over by detray_set_menu_icon, or is empty. - */ -typedef struct DetrayItem { - const char* id; - const char* label; - const char* icon; - int32_t flags; - int32_t parent; -} DetrayItem; - -#ifndef DETRAY_TYPES_ONLY - -DETRAY_API int32_t detray_version(void); - -/* The application, as an accessory with no Dock icon, and the item in the status bar. 1 on success. */ -DETRAY_API int32_t detray_init(void); - -/* An icon's picture, by the managed TrayIcon's value. A PNG, drawn at the menu bar's height. */ -DETRAY_API void detray_set_icon(int32_t icon, const uint8_t* png, int32_t length); - -DETRAY_API void detray_set_menu_icon(const char* name, const uint8_t* png, int32_t length); - -/* What the item shows: which icon, its tooltip, and the menu built when it is next opened. */ -DETRAY_API void detray_apply(int32_t icon, const char* tooltip, const DetrayItem* items, int32_t count); - -/* - * Dispatches AppKit's events for up to a tenth of a second, and returns at once when one has left - * an item chosen or a quit asked for. The menu's own tracking loop runs inside it. - */ -DETRAY_API void detray_pump(void); - -/* - * The id of an item chosen since the last poll, written to buffer, its whole length in bytes - * returned, or 0 for none. Chosen items are handed over one a poll, in the order they were chosen. - */ -DETRAY_API int32_t detray_poll(char* buffer, int32_t capacity); - -/* - * 1 once AppKit has been asked to quit other than by the session ending: with no Dock icon, by - * something that sent the app a quit event, such as AppleScript's quit. Refused, for the managed - * side to end its loop the way Exit does. - */ -DETRAY_API int32_t detray_quitting(void); - -/* - * Has ended called at a logout, restart or shutdown, from inside the detray_pump that dispatched - * AppKit's quit, on the main thread. AppKit ends the process as it returns, so it is the one - * chance to stage the queue: refused, the quit would cancel the logout. Read by detray_init, so - * set before it; with none, a session's end is refused and reported by detray_quitting. - */ -DETRAY_API void detray_on_session_end(DetraySessionEnd ended); - -/* - * The menu as the last apply left it, a line an item, as text, written to buffer with its whole - * length returned. For tests, which can make no status item: neither this nor detray_apply needs - * detray_init. - */ -DETRAY_API int32_t detray_describe(char* buffer, int32_t capacity); - -DETRAY_API void detray_shutdown(void); - -#endif - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/native/swift/Package.swift b/native/swift/Package.swift index 3a3cc753..7b18f338 100644 --- a/native/swift/Package.swift +++ b/native/swift/Package.swift @@ -8,14 +8,14 @@ let package = Package( platforms: [.macOS(.v12)], products: [ .library(name: "diffengine_viewer", type: .dynamic, targets: ["Deview"]), - // The tray's status bar item, a library of its own so a change to either never moves the - // other's ABI version. build-native's mac-libraries collects it into the tray's head. + // The tray's status bar item, PlatformHeads' tray ABI in a library of its own, so the + // viewer's never carries it. build-native's mac-libraries collects it into the tray's head. .library(name: "diffengine_tray", type: .dynamic, targets: ["Detray"]) ], dependencies: [ - // What every AppKit head does underneath its drawing, shared with BuildMonitor. The sixth - // pin of PlatformHeads, moved with the other five. - .package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.13.0") + // What every AppKit head does underneath its drawing, and the tray's ABI, shared with + // BuildMonitor. A pin of PlatformHeads, moved with the other seven. + .package(url: "https://github.com/SimonCropp/PlatformHeads", exact: "0.14.0") ], targets: [ // Exists only to import the ABI. Swift does not guarantee struct layout, so the structs @@ -33,15 +33,9 @@ let package = Package( .linkedFramework("CoreGraphics"), .linkedFramework("ImageIO") ]), - // The tray's ABI, for the reason CDeview exists. - .target(name: "CDetray"), .target( name: "Detray", dependencies: [ - "CDetray", - .product(name: "PlatformHeadsMac", package: "PlatformHeads") - ], - linkerSettings: [ - .linkedFramework("AppKit") + .product(name: "PlatformHeadsMacTray", package: "PlatformHeads") ]) ]) diff --git a/native/swift/Sources/CDetray/bridge.c b/native/swift/Sources/CDetray/bridge.c deleted file mode 100644 index adf66c7b..00000000 --- a/native/swift/Sources/CDetray/bridge.c +++ /dev/null @@ -1,3 +0,0 @@ -/* SwiftPM needs a C target to have at least one source file. There is nothing to compile: this - * target exists only to publish detray_bridge.h to Swift. */ -#include "include/detray_bridge.h" diff --git a/native/swift/Sources/CDetray/include/detray_bridge.h b/native/swift/Sources/CDetray/include/detray_bridge.h deleted file mode 100644 index 6f947d46..00000000 --- a/native/swift/Sources/CDetray/include/detray_bridge.h +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Brings the tray's ABI structs and constants into Swift without their prototypes: the library - * provides those symbols itself, with @_cdecl, as the viewer's does. Reached by a relative include - * rather than copied, for the reason deview_bridge.h gives. - */ -#ifndef DETRAY_BRIDGE_H -#define DETRAY_BRIDGE_H - -#define DETRAY_TYPES_ONLY -#include "../../../../include/detray.h" - -#endif diff --git a/native/swift/Sources/Detray/Exports.swift b/native/swift/Sources/Detray/Exports.swift deleted file mode 100644 index d1e70c08..00000000 --- a/native/swift/Sources/Detray/Exports.swift +++ /dev/null @@ -1,93 +0,0 @@ -import AppKit -import CDetray -import Foundation -import PlatformHeadsMac - -/// The entry points of native/include/detray.h, over PlatformHeadsMac's StatusItem. -/// -/// The header is imported for its struct and constants only, with DETRAY_TYPES_ONLY, so these are -/// the definitions of those symbols rather than a second declaration of them. Each one that -/// touches AppKit runs inside its own autorelease pool, for the reason the viewer's Exports.swift -/// gives: nothing here calls `NSApplication.run`, which would drain one an event. - -@_cdecl("detray_version") -public func detrayVersion() -> Int32 { - Int32(DETRAY_VERSION) -} - -@_cdecl("detray_init") -public func detrayInit() -> Int32 { - autoreleasepool { - Tray.shared.start() ? 1 : 0 - } -} - -@_cdecl("detray_set_icon") -public func detraySetIcon(_ icon: Int32, _ png: UnsafePointer?, _ length: Int32) { - guard let png, length > 0 else { - return - } - - autoreleasepool { - Tray.shared.item.setIcon(icon, png: Data(bytes: png, count: Int(length))) - } -} - -@_cdecl("detray_set_menu_icon") -public func detraySetMenuIcon(_ name: UnsafePointer?, _ png: UnsafePointer?, _ length: Int32) { - guard let name, let png, length > 0 else { - return - } - - autoreleasepool { - Tray.shared.item.setMenuIcon(String(cString: name), png: Data(bytes: png, count: Int(length))) - } -} - -@_cdecl("detray_apply") -public func detrayApply(_ icon: Int32, _ tooltip: UnsafePointer?, _ items: UnsafePointer?, _ count: Int32) { - autoreleasepool { - Tray.shared.item.apply( - icon: icon, - tooltip: tooltip.map { String(cString: $0) } ?? "", - items: MenuItems.read(items, count)) - } -} - -@_cdecl("detray_pump") -public func detrayPump() { - autoreleasepool { - Tray.shared.pump() - } -} - -@_cdecl("detray_poll") -public func detrayPoll(_ buffer: UnsafeMutablePointer?, _ capacity: Int32) -> Int32 { - guard let chosen = Tray.shared.takeChosen() else { - return 0 - } - - return Utf8Buffer.write(chosen, buffer, capacity) -} - -@_cdecl("detray_quitting") -public func detrayQuitting() -> Int32 { - Tray.shared.quitting ? 1 : 0 -} - -@_cdecl("detray_on_session_end") -public func detrayOnSessionEnd(_ ended: DetraySessionEnd?) { - Tray.shared.sessionEnded = ended -} - -@_cdecl("detray_describe") -public func detrayDescribe(_ buffer: UnsafeMutablePointer?, _ capacity: Int32) -> Int32 { - Utf8Buffer.write(StatusMenuItem.describe(Tray.shared.item.items), buffer, capacity) -} - -@_cdecl("detray_shutdown") -public func detrayShutdown() { - autoreleasepool { - Tray.shared.stop() - } -} diff --git a/native/swift/Sources/Detray/Library.swift b/native/swift/Sources/Detray/Library.swift new file mode 100644 index 00000000..31a3578a --- /dev/null +++ b/native/swift/Sources/Detray/Library.swift @@ -0,0 +1,5 @@ +// The tray's library is PlatformHeads' PlatformHeadsMacTray and nothing else: its C ABI, +// platformheads_tray.h, which SwiftPM links into this product and so exports from it, and which +// PlatformHeads.Mac's StatusItemTray and StatusItemApp call. This target exists only to be the +// product's own, since a product can name no target of another package. +import PlatformHeadsMacTray diff --git a/native/swift/Sources/Detray/MenuItems.swift b/native/swift/Sources/Detray/MenuItems.swift deleted file mode 100644 index 2065fabc..00000000 --- a/native/swift/Sources/Detray/MenuItems.swift +++ /dev/null @@ -1,28 +0,0 @@ -import CDetray -import PlatformHeadsMac - -/// The menu as `detray_apply` hands it over, one array with each item's parent by index, made into -/// the tree `StatusItem` builds its menu from by PlatformHeadsMac's `StatusMenuItem.tree`. The -/// `DETRAY_*` flags are its `StatusMenuFlags`, at the same values. -enum MenuItems { - static func read(_ items: UnsafePointer?, _ count: Int32) -> [StatusMenuItem] { - guard let items, count > 0 else { - return [] - } - - return StatusMenuItem.tree( - Array(UnsafeBufferPointer(start: items, count: Int(count))), - parent: { $0.parent }) { entry, children in - StatusMenuItem( - id: text(entry.id), - label: text(entry.label), - icon: text(entry.icon), - flags: StatusMenuFlags(rawValue: entry.flags), - children: children) - } - } - - private static func text(_ pointer: UnsafePointer?) -> String { - pointer.map { String(cString: $0) } ?? "" - } -} diff --git a/native/swift/Sources/Detray/Tray.swift b/native/swift/Sources/Detray/Tray.swift deleted file mode 100644 index 9a2511bd..00000000 --- a/native/swift/Sources/Detray/Tray.swift +++ /dev/null @@ -1,70 +0,0 @@ -import AppKit -import CDetray -import PlatformHeadsMac - -/// The status item, and what has come of it since the managed side last asked: the items chosen, -/// in the order they were, and whether AppKit has been asked to quit. -final class Tray { - static let shared = Tray() - - let item = StatusItem() - private let napGuard = NapGuard() - private var chosen: [String] = [] - private(set) var quitting = false - - /// What `detray_on_session_end` was handed: the managed side's session end, which stages the - /// queue it owns and removes the version marker. - var sessionEnded: DetraySessionEnd? - - /// A quit sent to the app, which has no Dock icon to quit from, turned into the managed side's - /// own quit, which stages the queue on its way out. A logout, restart or shutdown runs - /// `sessionEnded` there and then instead, and AppKit ends the process once it returns. Made in - /// `start`, from whatever `sessionEnded` is by then: with none, a session's end is refused as a - /// quit is, which cancels the logout but loses nothing. Held here: AppKit keeps its delegate - /// weakly. - private var terminateGuard: TerminateGuard? - - private init() { - item.chose = { [unowned self] entry in - self.chosen.append(entry.id) - } - } - - /// An accessory, with no Dock icon and no menu bar of its own, which a tray has no use for: - /// not `HeadWindow.launch`, which makes a regular app so its window can take focus. - /// `finishLaunching` is the part of `run` that has to happen before events are pumped by hand. - /// App Nap is kept off, as the viewer keeps it: napped, a process's timers are coalesced, and - /// the managed side's sockets and scan beside this answered late. - func start() -> Bool { - let application = NSApplication.shared - application.setActivationPolicy(.accessory) - let terminate = TerminateGuard( - { [unowned self] in - self.quitting = true - }, - sessionEnding: sessionEnded.map { ended -> () -> Void in { ended() } }) - terminateGuard = terminate - application.delegate = terminate - application.finishLaunching() - napGuard.begin(reason: "Tracking what test runs leave pending") - return item.start() - } - - /// A tenth of a second at most, which is what the managed loop turns on, and less when an - /// item has been chosen or a quit asked for. - func pump() { - EventPump.pump(unseen: true) { self.hasInput } - } - - private var hasInput: Bool { - !chosen.isEmpty || quitting - } - - func takeChosen() -> String? { - chosen.isEmpty ? nil : chosen.removeFirst() - } - - func stop() { - napGuard.end() - } -} diff --git a/native/swift/Sources/Detray/Utf8Buffer.swift b/native/swift/Sources/Detray/Utf8Buffer.swift deleted file mode 100644 index 25a16068..00000000 --- a/native/swift/Sources/Detray/Utf8Buffer.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -/// A string handed back to the managed side: written to its buffer as UTF-8, as much as fits, and -/// its whole length returned, so a buffer too small for it is known to be. -enum Utf8Buffer { - static func write(_ text: String, _ buffer: UnsafeMutablePointer?, _ capacity: Int32) -> Int32 { - let bytes = Array(text.utf8) - let count = min(bytes.count, Int(capacity)) - if let buffer, count > 0 { - bytes.withUnsafeBytes { source in - _ = memcpy(buffer, source.baseAddress!, count) - } - } - - return Int32(bytes.count) - } -} diff --git a/src/DiffEngineTray.Core.Tests/DiffEngineTray.Core.Tests.csproj b/src/DiffEngineTray.Core.Tests/DiffEngineTray.Core.Tests.csproj index 994eca84..427d464c 100644 --- a/src/DiffEngineTray.Core.Tests/DiffEngineTray.Core.Tests.csproj +++ b/src/DiffEngineTray.Core.Tests/DiffEngineTray.Core.Tests.csproj @@ -19,7 +19,9 @@ - + + diff --git a/src/DiffEngineTray.Core.Tests/MacTrayTests.cs b/src/DiffEngineTray.Core.Tests/MacTrayTests.cs index 0f360d53..e7d61b71 100644 --- a/src/DiffEngineTray.Core.Tests/MacTrayTests.cs +++ b/src/DiffEngineTray.Core.Tests/MacTrayTests.cs @@ -1,94 +1,43 @@ /// -/// The macOS tray's menu across the C ABI. A C struct cannot hold a list of itself, so the menu -/// goes over as one array with each item's parent by index, and the library makes it a tree again. -/// Both halves are PlatformHeads', which tests the shape on its own; this is the ABI in between. +/// The macOS tray's committed library. Its ABI and its binding are PlatformHeads', which tests the +/// two against a library of its own; this is the one the Mac head ships, which has the ABI only +/// because SwiftPM links PlatformHeadsMacTray into it. /// public class MacTrayTests { /// - /// The library's own reading of what it was handed, every field of every item, against the - /// menu it was handed. Needs no status bar, so it runs on the macOS CI job. + /// The library exports the tray's ABI at the version PlatformHeads.Mac was built for, and hands + /// a menu back as it was handed it. Needs no status bar, so it runs on the macOS CI job. /// [Test] [RunOn(TUnit.Core.Enums.OS.MacOs)] public async Task TheLibraryRebuildsTheMenuItWasHanded() { - Detray.Resolve(); - await Assert.That(Detray.Version()).IsEqualTo(Detray.ExpectedVersion); - - MacTray.Send(TrayIcon.Active, "DiffEngineTray", Menu); - - await Assert.That(MacTray.Describe()).IsEqualTo(Describe(Menu, 0)); + StatusItemLibrary.Use("diffengine_tray"); + await Assert.That(StatusItemLibrary.Version()).IsEqualTo(StatusItemLibrary.ExpectedVersion); + + await Assert.That(StatusItemLibrary.Describe(Menu)).IsEqualTo( + """ + Exit [id: exit] [glyph: exit] + Run at startup [id: startup] [checked] + ---- + Sample.Test (txt) [id: move:a] + Accept move [id: move:a:accept] [glyph: accept] + + """); } - /// - /// What detray_describe writes for a menu, written here from the managed items. - /// - static string Describe(IReadOnlyList items, int depth) - { - var builder = new StringBuilder(); - foreach (var item in items) - { - builder.Append(' ', depth * 2); - if (item.Separator) - { - builder.Append("----\n"); - continue; - } - - builder.Append($"{item.Label} [id: {item.Id}]"); - if (item.IconName is { Length: > 0 } icon) - { - builder.Append($" [glyph: {icon}]"); - } - - if (!item.Enabled) - { - builder.Append(" [disabled]"); - } - - if (item.Checked is { } on) - { - builder.Append(on ? " [checked]" : " [unchecked]"); - } - - builder.Append('\n'); - builder.Append(Describe(item.Children ?? [], depth + 1)); - } - - return builder.ToString(); - } - - /// - /// A menu with each thing the ABI carries: a glyph, a toggle each way, a separator, a disabled - /// heading, a submenu in a submenu, and text beyond ASCII. - /// static readonly IReadOnlyList Menu = [ new("exit", "Exit", IconName: "exit"), new("startup", "Run at startup", Checked: true), new("separator:1", "", Separator: true), - new("group:Solution", "Solution", Enabled: false, IconName: "solution"), new( "move:a", "Sample.Test (txt)", Children: [ - new("move:a:accept", "Accept move"), - new( - "move:a:more", - "More", - Children: - [ - new("move:a:more:deeper", "Ünïcödé, 日本語", Checked: false) - ]) - ]), - new( - "move:b", - "Other (png)", - Children: - [ - new("move:b:accept", "Accept move", IconName: "accept") + new("move:a:accept", "Accept move", IconName: "accept") ]) ]; } diff --git a/src/DiffEngineTray.Core/Detray.cs b/src/DiffEngineTray.Core/Detray.cs deleted file mode 100644 index 1a696b66..00000000 --- a/src/DiffEngineTray.Core/Detray.cs +++ /dev/null @@ -1,55 +0,0 @@ -/// -/// The macOS tray's native library, native/include/detray.h, which the Mac head carries under -/// runtimes/{rid}/native. In the core, as ProcessEx's kernel32 and libproc calls are: it compiles -/// anywhere and is called only on macOS, and the core's tests can then run it there. -/// -static class Detray -{ - public const string Library = "diffengine_tray"; - - /// - /// Must match DETRAY_VERSION in native/include/detray.h, so a stale library is reported rather - /// than read as garbage. - /// - public const int ExpectedVersion = 2; - - /// - /// Has PlatformHeads' resolver find under runtimes/{rid}/native for these - /// imports. Once a process, however many threads ask. - /// - public static void Resolve() => - NativeResolver.Register(typeof(Detray).Assembly, Library); - - [DllImport(Library, EntryPoint = "detray_version")] - public static extern int Version(); - - [DllImport(Library, EntryPoint = "detray_init")] - public static extern int Init(); - - [DllImport(Library, EntryPoint = "detray_set_icon")] - public static extern void SetIcon(int icon, byte[] png, int length); - - [DllImport(Library, EntryPoint = "detray_set_menu_icon")] - public static extern void SetMenuIcon([MarshalAs(UnmanagedType.LPUTF8Str)] string name, byte[] png, int length); - - [DllImport(Library, EntryPoint = "detray_apply")] - public static extern void Apply(int icon, [MarshalAs(UnmanagedType.LPUTF8Str)] string tooltip, DetrayItem[] items, int count); - - [DllImport(Library, EntryPoint = "detray_pump")] - public static extern void Pump(); - - [DllImport(Library, EntryPoint = "detray_poll")] - public static extern int Poll(byte[] buffer, int capacity); - - [DllImport(Library, EntryPoint = "detray_quitting")] - public static extern int Quitting(); - - [DllImport(Library, EntryPoint = "detray_on_session_end")] - public static extern unsafe void OnSessionEnd(delegate* unmanaged ended); - - [DllImport(Library, EntryPoint = "detray_describe")] - public static extern int Describe(byte[] buffer, int capacity); - - [DllImport(Library, EntryPoint = "detray_shutdown")] - public static extern void Shutdown(); -} diff --git a/src/DiffEngineTray.Core/DetrayItem.cs b/src/DiffEngineTray.Core/DetrayItem.cs deleted file mode 100644 index fe8b36a6..00000000 --- a/src/DiffEngineTray.Core/DetrayItem.cs +++ /dev/null @@ -1,13 +0,0 @@ -/// -/// DetrayItem in native/include/detray.h: three UTF-8 strings the caller allocates and frees around -/// the call, the flags, and the index of the item whose submenu this one is in, or -1. -/// -[StructLayout(LayoutKind.Sequential)] -struct DetrayItem -{ - public IntPtr Id; - public IntPtr Label; - public IntPtr Icon; - public int Flags; - public int Parent; -} diff --git a/src/DiffEngineTray.Core/DiffEngineTray.Core.csproj b/src/DiffEngineTray.Core/DiffEngineTray.Core.csproj index 471b8303..0bc4378a 100644 --- a/src/DiffEngineTray.Core/DiffEngineTray.Core.csproj +++ b/src/DiffEngineTray.Core/DiffEngineTray.Core.csproj @@ -9,8 +9,6 @@ --> net10.0 - - true false false diff --git a/src/DiffEngineTray.Core/MacTray.cs b/src/DiffEngineTray.Core/MacTray.cs deleted file mode 100644 index b2f1cf0f..00000000 --- a/src/DiffEngineTray.Core/MacTray.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Text; - -/// -/// The macOS tray: PlatformHeadsMac's StatusItem in the tray's own native library -/// (), since the managed side has no AppKit. Every call is the thread Main -/// started on's to make, as AppKit requires, so the Mac head's loop is -/// , with as its wait. -/// -sealed class MacTray : ITray -{ - // An id is a path or a snapshot's key with a word either side, which this holds with room over - readonly byte[] chosen = new byte[64 * 1024]; - - /// - /// What a logout, restart or shutdown runs: , which stages the - /// queue and removes the version marker. Called from inside , on the thread - /// the loop runs on, and AppKit ends the process as it returns, so nothing after the pump runs. - /// Static, since the library calls back through a plain function pointer, which carries no - /// instance to find it on, and settable after , since what it stages is - /// made after the tray. - /// - public static Action? SessionEnding { get; set; } - - MacTray() - { - } - - /// - /// Nothing may be thrown back into the library, which is inside AppKit's quit. - /// - [UnmanagedCallersOnly] - static void SessionEnded() - { - try - { - SessionEnding?.Invoke(); - } - catch (Exception exception) - { - Log.Error(exception, "Failed while the session was ending"); - } - } - - /// - /// The item in the status bar, with every icon and glyph handed over. Null with an - /// when there is no library for this machine, one of another version, - /// or no status bar to put the item in. - /// - public static unsafe MacTray? Open(out string? error) - { - try - { - Detray.Resolve(); - var version = Detray.Version(); - if (version != Detray.ExpectedVersion) - { - error = $"The tray's native library is version {version}, and this tray was built for {Detray.ExpectedVersion}."; - return null; - } - - // Before Init, which reads it: with none, a logout is refused and cancelled - Detray.OnSessionEnd(&SessionEnded); - if (Detray.Init() == 0) - { - error = "AppKit gave the status item no button to draw in."; - return null; - } - } - catch (Exception exception) - when (exception is DllNotFoundException or EntryPointNotFoundException) - { - error = exception.Message; - return null; - } - - foreach (var icon in Enum.GetValues()) - { - var png = TrayPictures.Png(icon); - Detray.SetIcon((int) icon, png, png.Length); - } - - foreach (var (name, png) in TrayPictures.Glyphs) - { - Detray.SetMenuIcon(name, png, png.Length); - } - - error = null; - return new(); - } - - public void Apply(TrayModel model) => - Send(model.Icon, model.Tooltip, model.Items); - - /// - /// What hands over, which needs no status item, so a test can hand over a - /// menu and read back what the library made of it. The menu goes as PlatformHeads' - /// writes it, which PlatformHeadsMac's - /// StatusMenuItem.tree reads back, with as - /// detray.h's DETRAY_* bits, which have the same values. - /// - internal static void Send(TrayIcon icon, string tooltip, IReadOnlyList items) - { - var flat = TrayMenuItem.Flatten(items); - var native = new DetrayItem[flat.Count]; - var strings = new List(); - try - { - for (var i = 0; i < flat.Count; i++) - { - var (item, parent) = flat[i]; - native[i] = new() - { - Id = Utf8(item.Id, strings), - Label = Utf8(item.Label, strings), - Icon = Utf8(item.IconName ?? "", strings), - Flags = (int) item.Flags, - Parent = parent - }; - } - - Detray.Apply((int) icon, tooltip, native, native.Length); - } - finally - { - foreach (var pointer in strings) - { - Marshal.FreeCoTaskMem(pointer); - } - } - } - - static IntPtr Utf8(string text, List strings) - { - var pointer = Marshal.StringToCoTaskMemUTF8(text); - strings.Add(pointer); - return pointer; - } - - public TrayInput Poll() - { - var length = Detray.Poll(chosen, chosen.Length); - if (length == 0) - { - return new(); - } - - return new(ClickedItem: Encoding.UTF8.GetString(chosen, 0, Math.Min(length, chosen.Length))); - } - - /// - /// AppKit's events dispatched, for a tenth of a second at most: the loop's wait, and the only - /// way the menu opens at all. - /// - public static void Pump() => - Detray.Pump(); - - /// - /// AppKit was asked to quit other than by the session ending. With no Dock icon, that is a quit - /// event sent to the app, such as AppleScript's, which the library refused and turned into - /// this, for the loop to end the way Exit does, staging the queue on its way out. A session's - /// end is . - /// - public static bool Quitting => - Detray.Quitting() != 0; - - /// - /// The menu as the library rebuilt it from the last , as text. - /// - internal static string Describe() - { - var buffer = new byte[64 * 1024]; - var length = Detray.Describe(buffer, buffer.Length); - return Encoding.UTF8.GetString(buffer, 0, Math.Min(length, buffer.Length)); - } - - public void Notify(TrayNotification notification) => - DesktopNotification.Show("DiffEngineTray", notification); - - public void Dispose() => - Detray.Shutdown(); -} diff --git a/src/DiffEngineTray.Mac/DiffEngineTray.Mac.csproj b/src/DiffEngineTray.Mac/DiffEngineTray.Mac.csproj index a541886b..2f46b4aa 100644 --- a/src/DiffEngineTray.Mac/DiffEngineTray.Mac.csproj +++ b/src/DiffEngineTray.Mac/DiffEngineTray.Mac.csproj @@ -2,7 +2,7 @@ @@ -27,6 +27,7 @@ + diff --git a/src/DiffEngineTray.Mac/MacTray.cs b/src/DiffEngineTray.Mac/MacTray.cs new file mode 100644 index 00000000..02bcf397 --- /dev/null +++ b/src/DiffEngineTray.Mac/MacTray.cs @@ -0,0 +1,36 @@ +/// +/// The tray's status bar item: PlatformHeads.Mac's in the +/// tray's own native library, inside the accessory application makes, +/// since this tray has no window and so no application of its own. +/// +static class MacTray +{ + /// + /// The product of native/swift that links PlatformHeads' tray ABI, which this head carries under + /// runtimes/{rid}/native. + /// + public const string Library = "diffengine_tray"; + + /// + /// The application started and the item in the status bar. Null with an + /// when there is no library for this machine, one of another version, or no status bar to put + /// the item in. + /// + public static StatusItemTray? Open(out string? error) + { + StatusItemApp.SessionEndFailed = _ => Log.Error(_, "Failed while the session was ending"); + error = StatusItemApp.Start(Library, "Tracking what test runs leave pending"); + if (error is not null) + { + return null; + } + + return StatusItemTray.Open( + new( + Library, + "DiffEngineTray", + (icon, _) => TrayPictures.Png(icon), + TrayPictures.Glyph), + out error); + } +} diff --git a/src/DiffEngineTray.Mac/Program.cs b/src/DiffEngineTray.Mac/Program.cs index 958dd2dc..098ec031 100644 --- a/src/DiffEngineTray.Mac/Program.cs +++ b/src/DiffEngineTray.Mac/Program.cs @@ -87,12 +87,12 @@ static async Task Inner() owned.Start(); } - // A logout, restart or shutdown is heard inside the pump (MacTray.SessionEnding), and AppKit + // A logout, restart or shutdown is heard inside the pump (StatusItemApp.SessionEnding), and AppKit // ends the process as it returns, since refusing the quit would cancel the logout. The // signals are a session's processes being ended otherwise, which .NET answers by ending // the process with nothing below run var sessionEnding = TraySessionEnd.For(owned, TrayVersionFile.Delete); - MacTray.SessionEnding = sessionEnding; + StatusItemApp.SessionEnding = sessionEnding; using var sessionEnd = new SessionEndSignals( sessionEnding, _ => Log.Error(_, "Failed while the session was ending")); @@ -126,8 +126,8 @@ static async Task Inner() tray, () => { - MacTray.Pump(); - if (MacTray.Quitting) + StatusItemApp.Pump(); + if (StatusItemApp.Quitting) { tokenSource.Cancel(); } @@ -141,6 +141,10 @@ static async Task Inner() finally { TrayVersionFile.Delete(); + if (tray is not null) + { + StatusItemApp.Stop(); + } } await tokenSource.CancelAsync(); diff --git a/src/DiffEngineTray.Mac/runtimes/osx-arm64/native/libdiffengine_tray.dylib b/src/DiffEngineTray.Mac/runtimes/osx-arm64/native/libdiffengine_tray.dylib index 4d77e3cb..bb59a109 100644 Binary files a/src/DiffEngineTray.Mac/runtimes/osx-arm64/native/libdiffengine_tray.dylib and b/src/DiffEngineTray.Mac/runtimes/osx-arm64/native/libdiffengine_tray.dylib differ diff --git a/src/DiffEngineTray.Mac/runtimes/osx-x64/native/libdiffengine_tray.dylib b/src/DiffEngineTray.Mac/runtimes/osx-x64/native/libdiffengine_tray.dylib index 4d77e3cb..bb59a109 100644 Binary files a/src/DiffEngineTray.Mac/runtimes/osx-x64/native/libdiffengine_tray.dylib and b/src/DiffEngineTray.Mac/runtimes/osx-x64/native/libdiffengine_tray.dylib differ diff --git a/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib b/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib index 9ab3fc12..0477959c 100644 Binary files a/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib and b/src/DiffEngineViewer.Mac/runtimes/osx-arm64/native/libdiffengine_viewer.dylib differ diff --git a/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib b/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib index 9ab3fc12..0477959c 100644 Binary files a/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib and b/src/DiffEngineViewer.Mac/runtimes/osx-x64/native/libdiffengine_viewer.dylib differ diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 919c07e5..cf68ba68 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -19,10 +19,11 @@ --> - - - - + + + + + diff --git a/src/Packaging.Tests/Package.DiffEngineTray.osx-arm64.verified.txt b/src/Packaging.Tests/Package.DiffEngineTray.osx-arm64.verified.txt index c3a665dc..8c61605a 100644 --- a/src/Packaging.Tests/Package.DiffEngineTray.osx-arm64.verified.txt +++ b/src/Packaging.Tests/Package.DiffEngineTray.osx-arm64.verified.txt @@ -13,6 +13,7 @@ tools/net10.0/osx-arm64/DiffEngineTray.runtimeconfig.json tools/net10.0/osx-arm64/DotnetToolSettings.xml tools/net10.0/osx-arm64/EmptyFiles.dll tools/net10.0/osx-arm64/PlatformHeads.Core.dll +tools/net10.0/osx-arm64/PlatformHeads.Mac.dll tools/net10.0/osx-arm64/Serilog.Sinks.File.dll tools/net10.0/osx-arm64/Serilog.dll tools/net10.0/osx-arm64/runtimes/osx-arm64/native/libdiffengine_tray.dylib diff --git a/src/Packaging.Tests/Package.DiffEngineTray.osx-x64.verified.txt b/src/Packaging.Tests/Package.DiffEngineTray.osx-x64.verified.txt index 1e13d808..77b6c849 100644 --- a/src/Packaging.Tests/Package.DiffEngineTray.osx-x64.verified.txt +++ b/src/Packaging.Tests/Package.DiffEngineTray.osx-x64.verified.txt @@ -13,6 +13,7 @@ tools/net10.0/osx-x64/DiffEngineTray.runtimeconfig.json tools/net10.0/osx-x64/DotnetToolSettings.xml tools/net10.0/osx-x64/EmptyFiles.dll tools/net10.0/osx-x64/PlatformHeads.Core.dll +tools/net10.0/osx-x64/PlatformHeads.Mac.dll tools/net10.0/osx-x64/Serilog.Sinks.File.dll tools/net10.0/osx-x64/Serilog.dll tools/net10.0/osx-x64/runtimes/osx-x64/native/libdiffengine_tray.dylib