Das Raster ist weg. Widgets stehen nebeneinander, alle gleich groß, in der Reihenfolge aus den Einstellungen. Das Panel sitzt am oberen Rand — nach unten zu wachsen bedeckt den Bildschirm, nach rechts und links legt es sich in den ohnehin leeren Streifen neben der Notch. Eine zweite Reihe entsteht erst, wenn der Bildschirm keine weitere Kachel mehr hergibt. Die Größenauswahl fällt damit weg, und das ist kein Verlust: ein 2×1 neben einem 2×2 lässt oben rechts eine Lücke, die niemand füllen kann und die aussieht wie ein Fehler. Genau daher kam auch die Überlappung. Ein Test prüft jetzt für ein bis zwölf Kacheln, dass sich keine zwei schneiden und alle vollständig im Panel liegen — außerhalb heißt oben: hinter der Notch. Kacheln werden zusätzlich beschnitten, damit überquellender Inhalt nicht über den Nachbarn zeichnet. Der Kalender wählt jetzt zwischen Monatsraster und Terminliste. Das war vorher an die Kachelgröße gekoppelt und ist in Wahrheit eine Frage dessen, was man sehen will. In der Menüleiste wird die Breite gemessen statt geschätzt. Vorher stand je Darstellungsart eine feste Zahl im Code, großzügig gewählt — mit dem Ergebnis, dass neben jedem Wert Platz für ein weiteres Symbol blieb, in dem nichts stand. Gemessen wird die breiteste vorkommende Zeichenfolge, nicht die gerade angezeigte, sonst springt die Leiste bei jedem Messwert. Die Balkendarstellung bekommt ihre Breite aus der Kernzahl, die erst nach der ersten Messung feststeht. Popover schließen jetzt beim Klick daneben. `behavior = .transient` genügt nicht: es greift, solange die eigene App aktiv ist, aber Onyx läuft als .accessory und wird durch einen Klick auf ein Statuselement nicht aktiviert. Ein Klick in ein fremdes Fenster erreichte das Popover gar nicht. Ein globaler Beobachter sieht solche Klicks, ein lokaler die in der eigenen App, Escape schließt ebenfalls — und es ist immer nur eines offen. Mixer und Lüfter haben eigene Menüleistenelemente. Beides regelt man mitten in etwas anderem; der Weg über ein Einstellungsfenster war zu weit. Die Lüfter sind damit auch als Panel-Kachel echt statt Platzhalter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Onyx
Notch-Erweiterung für macOS. Beim Überfahren der Notch fährt ein Panel mit eigenen Widgets aus — Kalender, Wetter, Medien, Hardware, Netzwerk, Audio-Mixer, Ablage und AirDrop.
Zielgerät: MacBook Pro mit Apple Silicon, macOS 26+.
Bauen
xcodegen generate # erzeugt Onyx.xcodeproj aus project.yml
open Onyx.xcodeproj # dann ⌘R
Voraussetzungen: Xcode 26, XcodeGen,
Signierung mit Team PP34X97WS3.
Tests laufen ohne Xcode:
cd Packages/OnyxKit && swift test
Ein notarisiertes DMG baut Scripts/release.sh. Es braucht zwei Dinge, die
einmalig einzurichten sind — ein „Developer ID Application“-Zertifikat und ein
notarytool-Zugangsprofil; beides steht im Kopf des Skripts. Das Skript prüft
beides vorweg und bricht ab, statt nach zehn Minuten Bauen zu scheitern.
Aufbau
Die Projektdefinition liegt als project.yml vor, nicht als handgepflegte
.xcodeproj — so ist im Diff lesbar, was sich an den Build-Einstellungen ändert.
Onyx/ App-Target (nicht sandboxed, Hardened Runtime)
Packages/OnyxKit/ Alle Module als lokales Swift-Package
OnyxCore Einzelinstanz-Sicherung
OnyxDesign Farben, Maße, Bewegung — einzige Quelle
OnyxNotch Panel-Fenster, Geometrie, Zustandsmaschine
OnyxWidgetKit Widget-Protokoll, Layout-Engine, Persistenz
OnyxMenuBar Modul-Protokoll für Menüleisten-Elemente
CalendarProvider EventKit (und später Calendarr)
WeatherProvider WeatherKit + CoreLocation
MediaProvider MediaRemote-Adapter, AppleScript, Medientasten
Vendor/ Fremdcode im Quellzustand (siehe unten)
Spikes/ Wegwerf-Werkzeuge zur API-Verifikation
docs/spikes/ Was dabei herauskam — auch die Sackgassen
Entscheidungen, die man sonst für Fehler hält
Keine App-Sandbox. Sie ist mit dem privilegierten Helper, mit IOKit und mit dem MediaRemote-Adapter unvereinbar. Der App-Group-Container für Calendarr bleibt trotzdem erreichbar — auf macOS ist er von der Sandbox unabhängig. Nachgemessen in docs/spikes/D-appgroup.md.
TCC-Entitlements trotz fehlender Sandbox. Bei aktivierter Hardened Runtime
verlangt macOS com.apple.security.personal-information.* auch ohne Sandbox.
Fehlt eine davon, erscheint der Berechtigungsdialog gar nicht erst — ohne
Fehlermeldung. Das hat einen halben Abend gekostet:
docs/spikes/E-tcc-hardened-runtime.md.
ENABLE_DEBUG_DYLIB = NO. Xcode lagert im Debug-Build den Programmcode in
eine eigene dylib aus. TCC ordnet Berechtigungsanfragen dann dem verbleibenden
Rumpf zu, der weder Info.plist noch Entitlements trägt.
Eigenes Einstellungsfenster. SwiftUIs Settings-Szene öffnet über einen
privaten Selektor, der bei Apps ohne Menüleiste unzuverlässig ankommt.
Der MediaRemote-Adapter wird nicht gelinkt. Er wird von /usr/bin/perl
geladen, einem berechtigten Systemprogramm — im eigenen Prozess greift Apples
Entitlement-Prüfung und es käme nichts an.
Signiert wird im Release anders als im Debug. Der Helfer und der Adapter bekommen nur im Release einen sicheren Zeitstempel, und der Adapter wird dort mit der echten Identität statt ad-hoc signiert. Beides verlangt die Notarisierung, und beides fällt sonst erst nach dem Hochladen auf. Im Debug wären es nur Netzanfragen bei jedem Bauen.
Die Abdeckung eines Calendarr-Schnappschusses steht nicht in der Datei. Sie
ist zugesagtes Verhalten des Schreibers (49 Tage nach vorn) und wird von
CalendarrSnapshotReader daraus abgeleitet. Sie aus dem frühesten Termin zu
erschließen wäre naheliegend und falsch: mehrtägige Termine ragen ins Fenster
hinein und werden mitgeschrieben, obwohl ihr Anfang nicht abgedeckt ist. Ein
Test hält das fest.
Der Audiomixer hat keinen Schalter je Programm mehr. Am Regler zu ziehen ist die Entscheidung, dieses Programm zu regeln. Ein Schalter davor wäre eine Hürde, die nichts schützt — der große Ausschalter stellt ohnehin alles zurück.
Fremdcode
Vendor/mediaremote-adapter — ungive/mediaremote-adapter,
BSD-3-Clause. Als Quellcode eingebettet und beim Bauen kompiliert, nicht als
fertige Binary: so ist nachvollziehbar, was läuft. Stand siehe
ONYX-VERSION.txt.
© 2026 Scarriffleservices