Commit Graph

21 Commits

Author SHA1 Message Date
Guido Schmit
594d7c422a Phase 5: Hardware- und Netzwerkmonitor, Panel und Menüleiste
Sechs neue Widgets (CPU, GPU, Speicher, Akku, Sensoren, Netzwerk) und dieselben
sechs als einzeln aktivierbare Menüleisten-Module mit fünf Darstellungsarten.

Panel-Widget und Menüleisten-Modul derselben Größe teilen sich die Messschleife
über Referenzzählung. Ohne das liefe für jede Anzeige ein eigener Timer mit
denselben IOKit- und SMC-Abfragen — bei sechs Modulen und ebenso vielen Widgets
ein Vielfaches der nötigen Arbeit. Meldet sich der letzte Konsument ab, hört die
Schleife auf; im Ruhezustand misst Onyx nichts.

Die Differenzrechnung ist testgetrieben, und die Tests haben zwei echte Fehler
gefunden. Erstens: mein Überlaufschutz beim Durchsatz hielt einen
zurückgesetzten Zähler (Interface-Wechsel) für einen Überlauf und zeigte
4,3 GB/s. Da if_data64 64-Bit-Zähler liefert, die bei 10 Gbit/s erst nach
470 Jahren umlaufen, ist ein kleinerer Wert immer ein Zurücksetzen — die
Unterscheidung produzierte nur den Ausreißer, den sie verhindern sollte.
Zweitens: ByteCountFormatter schrieb bei null „Zero KB/s" statt „0 KB/s".

Bei den CPU-Zählern bleibt der Überlaufschutz nötig: die sind 32 Bit breit und
laufen nach gut 400 Tagen wirklich um, deshalb `&-` statt `-`.

Sensoren kommen über den SMC-Leser aus Spike A statt über die private
IOHID-Schnittstelle — auf dieser Maschine nachweislich verifiziert, und die
dort gefundene Little-Endian-Eigenheit ist berücksichtigt. Nur eine kurze Liste
benannter Fühler statt aller 3486 Keys: eine Wand aus Kürzeln ist keine
Information.

Gegengemessen statt vermutet: 15 Kerne, 39,8 W, GPU 50 %, 16,6/24 GB mit
kritischem Druck, Akku 68 Zyklen bei 100 % Gesundheit, fünf Sensoren, zwei
Lüfter, en0 mit 661 KB/s.

Menüleisten-Module zeichnen in labelColor statt in den Onyx-Farben — nur die
Systemfarbe passt sich heller wie dunkler Leiste an. Feste Breite je
Darstellungsart, sonst schiebt jeder Messwert die halbe Leiste hin und her.

Standardmäßig ist kein Modul aktiv: sechs neue Symbole beim ersten Start wären
eine Zumutung.

145 Tests grün.
2026-08-10 21:48:47 +02:00
Guido Schmit
a198d88104 Dock-Eintrag nur solange ein Fenster offen ist
Im Hintergrund gehört Onyx nicht in den Dock — es hat kein Fenster, das ein
Klick öffnen könnte. Mit sichtbarem Einstellungsfenster ist das Gegenteil
richtig: als reine .accessory-App lässt sich das Fenster weder über den Dock
noch über ⌘-Tab zurückholen, wenn es hinter etwas anderes rutscht.

Deshalb wird beim Öffnen auf .regular geschaltet und beim Schließen zurück —
außer der Dock-Eintrag ist in den Einstellungen ausdrücklich gewünscht.

Das generische weiße Symbol im Dock kam nicht aus dem Bundle: CFBundleIconName,
CFBundleIconFile, AppIcon.icns und der Assets-Katalog waren alle in Ordnung.
Es war der Icon-Cache von Launch Services, der die App von den vielen Starts
her kannte, als es noch gar kein Icon gab. Neu registriert und Cache verworfen.
2026-08-10 21:29:09 +02:00
Guido Schmit
c0874c83b7 Berechtigungen zeigen ihren Zustand live, Dock-Symbol wird wählbar
Die Einstellungen zeigten weiter "Anfragen", obwohl die Ortung längst lief:
authorizationStatus ist eine Abfrage, keine Beobachtung, und ich hatte sie
einmalig beim Öffnen gelesen. Jetzt meldet locationManagerDidChangeAuthorization
die Änderung, WeatherModel führt daraus einen beobachtbaren Wert, und die
Einstellungen aktualisieren sich, während das Fenster offen steht. Nach der
Zustimmung wird sofort geladen statt beim nächsten Anlauf.

Der Knopf "Systemeinstellungen öffnen" ganz unten zeigte fest auf den
Kalenderbereich — wer ihn beim Standort drückte, landete am falschen Ort. Es
gibt ihn nicht mehr; stattdessen führt jede Berechtigung in ihren eigenen
Bereich, und der Knopf erscheint nur im Zustand "abgelehnt". Bei "noch nie
gefragt" steht dort Anfragen, bei "erteilt" gar nichts: drei Zustände, drei
Antworten. Genau daran ist die erste Fassung gescheitert.

Dock-Symbol ist jetzt eine Einstellung, standardmäßig aus. Onyx läuft als
LSUIElement — das war Absicht und keine Auslassung: es hat kein Hauptfenster
und wäre im Dock ein Symbol, das beim Anklicken nichts öffnet, und es stünde im
Programmumschalter zwischen den Programmen, mit denen man arbeitet. Wer es
trotzdem will, schaltet es ein; die Wahl wird beim Start wieder angewendet.
2026-08-10 21:24:49 +02:00
Guido Schmit
7f6ded7de0 Menüleistensymbol: Konturfassung statt Silhouette
Das Glyph des App-Icons besteht aus deckenden Flächen. Als Template-Bild — der
einzige Weg, macOS zum Einfärben zu bewegen — verschmolzen die Facetten zu
einer flachen Silhouette, weil dabei nur der Alphakanal zählt.

MenuBarIcon.svg ist als Kontur gezeichnet: zwischen den Linien ist nichts, also
bleibt beim Einfärben die Struktur erhalten. Das Symbol wird damit automatisch
weiß auf dunkler und schwarz auf heller Menüleiste — ohne zweite Datei für den
zweiten Fall.

Die ebenfalls angebotene helle Flächenfassung wird damit überflüssig und ist
nicht eingecheckt: sie wäre auf einer hellen Menüleiste unsichtbar, und eine
ungenutzte Alternative im Repo lädt später zum Verwechseln ein.

Der Kopierschritt für quartz.svg entfällt — die Datei liegt jetzt direkt im
App-Target und wird ohne Umweg als Ressource eingebunden.
2026-08-10 21:17:13 +02:00
Guido Schmit
95bc3b1931 Menüleistensymbol als Template-Bild
Das Glyph besteht aus zehn deckenden Pfaden in sehr dunklen Tönen (#1c1f1f bis
#000000). Im App-Icon liegt es auf blauem Verlauf und liest sich als Kristall;
allein auf einer dunklen Menüleiste war es schwarz auf schwarz.

macOS färbt ausschließlich Template-Bilder ein, und die benutzen nur den
Alphakanal — die Facetten verschmelzen dabei zu einer Silhouette. Das ist der
bewusste Tausch: erkennbare Form in hellen wie dunklen Menüleisten statt
Facetten, die man in keiner von beiden sieht.

Eine Fassung mit durchsichtigen Facettenfugen behielte beides. Dafür muss das
SVG entsprechend gezeichnet sein — als Outline statt als Flächen.
2026-08-10 21:13:18 +02:00
Guido Schmit
433cdcc3dd Phase 4: Medien-Widget mit dreistufiger Kaskade
Adapter für alles inklusive Browser, AppleScript für Spotify und Musik,
Medientasten als letzte Reserve. Die Reihenfolge ist keine Vorliebe, sondern
Absicherung: Apple hat die Now-Playing-Schnittstelle in macOS 15.4 schon
einmal zugemacht. Fällt der Adapter aus, verliert das Widget seine Anzeige —
aber nicht seine Funktion. Deshalb bleiben die Transporttasten auch dann
bedienbar, wenn gar keine Metadaten da sind.

Der Fortschritt zählt zwischen den Ereignissen selbst hoch. Spike B hatte
gezeigt, dass elapsedTime nicht laufend nachgeliefert wird: ein Scrubber, der
stur den letzten Wert anzeigt, steht still, während der Titel läuft. Gerechnet
wird aus asOf und playbackRate, begrenzt auf die Titellänge — sonst zeigt die
Anzeige nach dem Ende weiter steigende Zahlen.

Teilmeldungen (diff = true) ergänzen den Stand, statt ihn zu ersetzen. Eine
Meldung mit nur der Position würde sonst Titel, Interpret und Cover löschen.

In AnyDecodable wird Bool vor Double geprüft. Andernfalls wird "playing": true
zu 1 und die Wiedergabe gilt für immer als angehalten.

Play/Pause schaltet sofort um, statt auf das nächste Ereignis zu warten. Ein
Knopf, der eine halbe Sekunde später reagiert, fühlt sich kaputt an; das echte
Ereignis korrigiert den Stand ohnehin.

Der Adapter liegt als Quellcode unter Vendor/ (BSD-3, Stand in
ONYX-VERSION.txt) und wird beim Bauen kompiliert, statt als fertige Binary
eingecheckt zu werden. Er wird bewusst nicht gelinkt: geladen wird er von
/usr/bin/perl, im eigenen Prozess greift Apples Entitlement-Prüfung.

124 Tests grün.
2026-08-10 21:02:13 +02:00
Guido Schmit
03dd7b945e Menüleiste zeigt das Glyph, nicht das ganze App-Icon
Das vollständige App-Icon bringt die abgerundete Kachel samt Farbverlauf mit.
Die gehört in Dock und Finder; in der Menüleiste steht sie als farbiger Klotz
zwischen lauter Strichsymbolen.

Geladen wird jetzt dieselbe SVG-Datei, aus der auch das App-Icon gebaut wird.
NSImage liest sie als Vektor (_NSSVGImageRep), sie skaliert also verlustfrei
auf die 18 pt der Menüleiste.

Der Weg dorthin ist ein Kopierschritt und keine zweite Datei: actool kompiliert
das .icon-Bündel zu .icns und Assets.car und legt nichts Lesbares ins Bundle,
und eine einzelne Datei aus dem Bündel lässt sich nicht als Ressource
referenzieren — XcodeGen bricht dabei ab. Ein Build-Schritt kopiert sie
stattdessen, damit es bei einer Quelldatei bleibt.

Weiterhin kein Template-Bild: als Schablone bliebe vom Kristall nur eine
schwarze Silhouette.
2026-08-10 20:55:40 +02:00
Guido Schmit
4b559ba840 App-Icon eingehängt, Menüleistensymbol nutzt dasselbe
Das Icon-Composer-Bündel liegt jetzt im App-Target, actool erzeugt daraus alle
Größen samt der Transluzenz-Darstellung von macOS 26.

Das Menüleistensymbol wird zur Laufzeit aus applicationIconImage auf 18 pt
gebracht — die Leiste ist 22 pt hoch und braucht oben und unten Luft. Bewusst
kein Template-Bild: als Schablone bliebe vom Kristall nur eine schwarze
Silhouette und der Verlauf wäre weg, also genau das Erkennungsmerkmal. Der
Preis ist, dass es sich nicht automatisch an helle und dunkle Menüleisten
anpasst.

Rückfall auf ein SF-Symbol, falls das App-Icon fehlt: ein Menüleisteneintrag
ohne Bild wäre unsichtbar und die App darüber unerreichbar.
2026-08-10 20:47:41 +02:00
Guido Schmit
3d21cac3b7 Wetter: fehlende Ortungserlaubnis wird als solche benannt
"Wetter nicht abrufbar" war eine falsche Auskunft: abrufbar war es sehr wohl,
es fehlte nur die Erlaubnis. Derselbe Fehlertyp wie zuvor beim Kalender —
ein Zustand, den die App nicht kennt, landet im nächstbesten Sammelfall und
schickt den Nutzer damit in die Irre.

locationUndetermined ist jetzt ein eigener Zustand neben locationDenied. Der
Unterschied ist nicht kosmetisch: bei undetermined lohnt eine Anfrage, bei
denied fragt macOS nie wieder und es hilft nur der Weg über die
Systemeinstellungen oder ein fest gewählter Ort. Beide Fälle führen jetzt
dorthin, wo es weitergeht.

Die Anfrage kommt aus dem Berechtigungen-Reiter, nicht aus dem Hintergrund —
dieselbe Lehre wie beim Kalender: ohne Vordergrundfenster zeigt macOS keinen
Dialog, und die App wirkt kaputt, ohne dass irgendwo ein Fehler steht.

Aufgefallen war es daran, dass im Protokoll überhaupt kein Wetter-Eintrag
stand. Der einzige Pfad zu "nicht abrufbar" ohne Protokolleintrag war der
Standort — jetzt protokolliert auch der.
2026-08-10 20:45:49 +02:00
Guido Schmit
a557c81baf WeatherKit aktiviert: App-ID im Portal war bereits vorhanden
Die Fehlermeldung führte in die Irre: "cannot be registered to your development
team because it is not available" klingt nach einem Namenskonflikt mit einem
fremden Team. Tatsächlich versuchte Xcode, eine App-ID anzulegen, die es schon
gab — mit bereits angehakter WeatherKit-Capability. Nach dem erneuten Lauf mit
-allowProvisioningUpdates wurde das passende Profil gefunden und eingebettet.

Im signierten Build stehen jetzt alle sechs Entitlements: weatherkit,
application-groups, personal-information.calendars, personal-information.location,
automation.apple-events und device.audio-input.
2026-08-10 20:42:22 +02:00
Guido Schmit
fc92d226b4 Phase 3d: Wetter-Widget (WeatherKit-Entitlement noch offen)
Eigene WeatherCondition statt WeatherKits Typ: die Widget-Ebene soll nicht an
ein Framework gebunden sein, das eine kostenpflichtige Mitgliedschaft und eine
Entitlement voraussetzt. Fällt WeatherKit weg, wird nur die Abbildung getauscht.

Der Zwischenspeicher ist Teil der Funktion, kein Feinschliff: WeatherKit rechnet
nach Abrufen ab, und ein Widget, das bei jedem Öffnen des Panels neu lädt,
verbrennt das Kontingent für Daten, die sich stündlich ändern. 15 Minuten
Gültigkeit, und der Schlüssel führt den Ort mit — sonst zeigt das Widget nach
einem Ortswechsel weiter das alte Wetter, überzeugend, weil Zahlen dastehen.

Ein abgelaufener Stand bleibt als "letzter bekannter" erhalten. Bei einem
Netzfehler ist ein alter Messwert mit sichtbarem Zeitstempel ehrlicher und
nützlicher als ein leeres Feld; er wird abgeblendet und mit Warnzeichen gezeigt.

Apples Namensnennung steht im Datenmodell, nicht in einer Notiz: sie ist
Bedingung der Nutzung von WeatherKit und wird sonst beim Bauen der Oberfläche
übersehen. In der 2x1-Kachel als klickbarer Link, in der 1x1 im Tooltip.

Temperaturen ohne Nachkommastellen — die täuschen eine Genauigkeit vor, die
die Vorhersage nicht hat.

NICHT aktiviert: com.apple.developer.weatherkit. Die Entitlement erzwingt ein
echtes Provisioning-Profil, und die App-ID lässt sich nicht registrieren
("cannot be registered to your development team because it is not available").
Bis das im Portal geklärt ist, bleibt sie auskommentiert — die App baut und
läuft, das Widget meldet ehrlich "Wetter nicht abrufbar".

106 Tests grün.
2026-08-10 20:33:32 +02:00
Guido Schmit
d345a4fcab Hardened Runtime verlangt eigene TCC-Entitlements
Die Ursache stand wörtlich im Protokoll von tccd:

  Prompting policy for hardened runtime; service: kTCCServiceCalendar requires
  entitlement com.apple.security.personal-information.calendars but it is missing

Bei aktivierter Hardened Runtime verlangt TCC diese Entitlements auch ohne
App-Sandbox. Die verbreitete Annahme, sie gälten nur für sandboxed Apps, ist
falsch — und ich bin ihr aufgesessen. Ohne sie zeigt macOS den Dialog gar nicht
erst an: die Anfrage kommt nach Millisekunden mit false zurück, der Status
bleibt notDetermined, und die App taucht in den Systemeinstellungen nie auf.
Dort lässt sich auch nichts nachtragen, der Kalenderbereich hat kein
Pluszeichen — eine Sackgasse ohne jede Fehlermeldung.

Gesetzt sind jetzt calendars, location, apple-events und audio-input, also
alles, was Phase 3, 4 und 7 brauchen. Sie fehlen sonst genau dann, wenn das
Feature fertig ist und niemand mehr an TCC denkt.

Gefunden durch Ausschluss, nicht durch Nachdenken: eine minimale Test-App
(normales Fenster, im Vordergrund, gleiches Zertifikat, nichts von Onyx darin)
scheiterte identisch. Damit war belegt, dass es nicht an Onyx lag.

Der Weg dorthin führte über mehrere falsche Fährten, die in
docs/spikes/E-tcc-hardened-runtime.md stehen, damit sie niemand zweimal geht —
darunter eine eigene: `log show` wurde von zsh abgefangen, und weil stderr
umgeleitet war, sah die leere Ausgabe wie ein Befund aus.

In der Praxis geprüft: Dialog erscheint, Zugriff erteilt, Status 3.
2026-08-10 18:36:15 +02:00
Guido Schmit
873c763d50 Kalenderberechtigung: Anfrage aus dem Vordergrundfenster
TCC zeigt den Dialog nur, wenn eine App im Vordergrund ist, an die er gehören
kann. Bei einer App ohne Fenster verpufft die Anfrage kommentarlos: sie kommt
mit false zurück, der Status bleibt notDetermined, und in den
Systemeinstellungen taucht die App nie auf — dort lässt sich auch nichts von
Hand hinzufügen, Einträge entstehen ausschließlich durch eine echte Anfrage.

Deshalb fragt der Reiter Berechtigungen jetzt selbst, sobald er sichtbar ist,
mit 400 ms Vorlauf: Aktivierung wirkt asynchron, ohne die Pause fragt Onyx
während es formal noch im Hintergrund liegt.

CalendarAuthorization trennt undetermined von denied. Im ersten Fall lohnt eine
Anfrage, im zweiten zeigt macOS nie wieder einen Dialog und es hilft nur der
Weg über die Systemeinstellungen — dorthin führt der Knopf dann auch.

Korrektur einer eigenen Fehldiagnose: promptPolicy = 0 im tccd-Protokoll gehört
zu kTCCServiceReminders, nicht zum Kalender. Der bekommt durchgehend
promptPolicy = 4, Nachfragen war also die ganze Zeit erlaubt. Die tatsächlichen
Ursachen waren die ausgelagerte Debug-dylib und ein verklemmter TCC-Eintrag.
2026-08-10 18:26:10 +02:00
Guido Schmit
40bd83e49e Berechtigungsdialog und Einstellungsfenster reparieren
Zwei Fehler, beide durch Messen gefunden statt durch Raten — und einer davon
war meine eigene fehlerhafte Diagnose: `log show` wurde von zsh abgefangen, ich
hatte stderr umgeleitet und die Fehlermeldung nie gesehen. Meine Aussage "keine
TCC-Einträge, also wurde nie gefragt" stützte sich damit auf einen Befehl, der
gar nicht lief.

Einstellungsfenster: SwiftUIs Settings-Szene wird über den privaten Selektor
showSettingsWindow: geöffnet, dessen Name sich zwischen macOS-Versionen schon
geändert hat und der bei .accessory-Apps ohne Menüleiste unzuverlässig ankommt.
Der Menüeintrag tat schlicht nichts. Jetzt ein eigenes NSWindow.

Kalenderberechtigung, zwei Ursachen übereinander:

Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus (für
SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur ein Rumpf, und TCC ordnete
die Anfrage dem Rumpf zu — kTCCServiceCalendar erreichte tccd überhaupt nicht.
ENABLE_DEBUG_DYLIB = NO behebt das; danach erscheint die Anfrage im Protokoll.

Danach immer noch kein Dialog: promptType 1, aber promptPolicy 0. TCC bereitet
den Dialog vor und entscheidet sich dagegen, weil keine App im Vordergrund ist,
an die er gehören könnte. NSApp.activate davor hilft nicht — Aktivierung wirkt
asynchron und ist beim Aufruf noch nicht durch. Deshalb wird beim ersten Start
das Einstellungsfenster auf dem Reiter Berechtigungen geöffnet: dort steht ein
echtes Fenster im Vordergrund, und der Nutzer weiß, warum gefragt wird.

Folgefehler dabei: ohne Berechtigung zeigte das Widget "Nichts mehr für heute"
— eine Aussage, die es ohne Zugriff gar nicht treffen kann. refresh() fragt
jetzt nicht mehr selbst nach und meldet ohne Berechtigung ehrlich `denied`.

Fehler aus requestFullAccessToEvents werden nicht mehr mit try? verschluckt.
2026-08-10 18:20:21 +02:00
Guido Schmit
70d4d67cca Phase 3: Kalender-Widget mit echten EventKit-Daten
Die Unterscheidung zwischen "keine Termine" und "keine Information" steckt von
Anfang an in den Grundlagen, nicht als Nachtrag für Calendarr. Ein Kalender
ohne Termine und ein Kalender, über den nichts bekannt ist, sehen in einer
Liste identisch aus — nämlich leer. Ein leerer März ist überzeugend und
schlicht falsch, wenn die Quelle nur 49 Tage abdeckt.

EventWindow trägt deshalb eine coverage mit. Bei EventKit ist sie der
angefragte Zeitraum, weil direkt abgefragt wird; bei Calendarr wird sie später
aus dem Snapshot kommen. Tage außerhalb werden im Mini-Monat ausgegraut und
tragen einen erklärenden Hinweis, statt als frei durchzugehen.

Die Abdeckung gewinnt auch gegen widersprüchliche Eingaben: liefert eine Quelle
einen Termin für einen Tag, den sie nach eigener Angabe nicht abdeckt, bleibt
der Tag unbekannt. Sonst behauptet die Anzeige mehr zu wissen, als belegt ist.
Als Test festgehalten.

CalendarSourceState statt eines Optionals: denied, neverWritten, loggedOut,
incompatible und unreadable bekommen jeweils eine eigene Antwort in der
Oberfläche. "Nichts anzuzeigen" ist keine.

Serientermine bekommen eine aus Kennung und Beginn zusammengesetzte ID —
EKEvent.eventIdentifier ist für alle Vorkommen gleich, eine Terminliste mit
einer täglichen Serie fiele sonst auf einen Eintrag zusammen.

Monatsraster testgetrieben, inklusive Schaltjahr, Wochenstart Montag wie
Sonntag und der Regel, dass das Ende der Abdeckung nicht mehr dazugehört.

93 Tests grün.
2026-08-10 18:07:44 +02:00
Guido Schmit
dd25d788c0 Phase 2 abgeschlossen: Einstellungsfenster und Lokalisierung
Einstellungen wirken sofort statt erst nach einem Neustart. In einer App ohne
Fenster ist eine verzögerte Änderung praktisch unauffindbar — man sieht ja
nicht, dass etwas passiert ist. AppModel meldet Layout- und Anzeigeänderungen
an den Koordinator, das Panel zeichnet sich neu und die Panelgröße zieht mit.

Widgets lassen sich hinzufügen, entfernen, in der Größe ändern und per
Drag-and-Drop umsortieren. Angeboten werden nur die Größen, die ein Widget
sinnvoll ausfüllt — ein Mini-Monat in 1x1 wäre unleserlich.

Lokalisierung als String Catalog in DE und EN, Quellsprache Englisch damit die
Schlüssel im Code lesbar bleiben. Der Katalog von OnyxCore musste ausdrücklich
als Ressource deklariert werden: ohne das gibt es kein Bundle.module und die
Übersetzungen wären zur Laufzeit unauffindbar. Im gebauten Bundle liegen jetzt
de.lproj und en.lproj, auch für das Paket.

Kein Menüleisten-Tab: solange es keine Module gibt, wäre das eine Oberfläche
für ein Feature, das noch nicht existiert. Er kommt mit Phase 5.

80 Tests grün.
2026-08-10 18:03:34 +02:00
Guido Schmit
c0eb44b0b0 Einzelinstanz erzwingen: die zuletzt gestartete gewinnt
Zwei Onyx-Instanzen sind kein Schönheitsfehler. Jede bringt eine eigene
Zustandsmaschine, eigene Timer und eine eigene Animation auf dieselbe Notch —
das Ergebnis sieht aus wie ein Ruckler und ist keiner. Genau das ist beim
Testen passiert, mit zwei Builds aus verschiedenen Ordnern.

Die zuletzt gestartete Instanz gewinnt, nicht die erste. Beim Entwickeln ist
das die frisch gebaute; die andere Reihenfolge hieße, nach jedem Build weiter
die alte Fassung zu testen, ohne es zu merken. Im normalen Gebrauch ist der
Unterschied unsichtbar, weil Onyx kein Fenster hat, das man stattdessen in den
Vordergrund holen könnte.

Erkannt wird über die Bundle-Kennung, nicht über den Pfad: die Instanzen, die
sich real in die Quere kommen, stammen aus verschiedenen Build-Ordnern und
teilen sich nur die Kennung.

Erst terminate(), nach 1,5 s forceTerminate(). Überlebt eine trotzdem — der
reale Fall ist eine aus Xcode gestartete Instanz, die im Debugger hängt und
selbst SIGKILL übersteht — tritt die neue Instanz zurück und sagt per Dialog,
warum und was zu tun ist. Ein stiller Rücktritt sähe aus wie ein Startfehler.

Die Entscheidungsregel liegt als reine Funktion in OnyxCore und ist getestet,
inklusive des Falls, dass die eigene PID mehrfach in der Liste steht —
sich selbst zu beenden wäre der denkbar schlechteste Ausgang.

In der Praxis geprüft: `open -n` erzwingt einen Zweitstart, danach läuft genau
eine Instanz. 80 Tests grün.
2026-08-10 17:57:13 +02:00
Guido Schmit
65f9fbfa4e Inhalt beginnt unterhalb der Notch statt dahinter
Das Panel reichte oben bündig an die Bildschirmkante, damit seine Fläche mit
der schwarzen Notch-Hardware zu einem Körper verschmilzt. Die Notch ist aber
undurchsichtig — die obersten 38 pt Inhalt waren schlicht nicht zu sehen. Links
und rechts daneben liegt in diesem Streifen zusätzlich die Menüleiste, der ganze
Streifen ist also unbrauchbar.

Die Fläche bleibt deshalb, wo sie war; nur der Inhalt rückt nach unten. Das
Fenster wächst entsprechend mit (446x340 → 446x378), damit unten nichts
abgeschnitten wird.

Der Controller nimmt jetzt die Inhaltsgröße entgegen und schlägt den Freiraum
selbst auf. Keine Aufrufstelle muss daran denken, und beim Wechsel zwischen
eingebautem Display (38 pt) und externem (virtueller Balken) wandert der
Freiraum automatisch mit.

Gegengemessen statt geschätzt: die Zentrierung war korrekt (Panel-midX 900 =
Notch-midX 900), der Fehler lag allein in der Höhe. Vier Tests halten den
Inhaltsbereich jetzt außerhalb der Notch fest, auch für den virtuellen Balken.

70 Tests grün.
2026-08-10 17:43:30 +02:00
Guido Schmit
8e0629d3a5 Phase 2 (Teil 1): Widget-Raster, Layout-Engine, Persistenz
Layout-Engine mit vier Spalten, testgetrieben. Widgets werden nicht stur
hintereinander gesetzt, sondern jeweils an die erste Stelle, an die sie passen.
Der Unterschied zeigt sich neben einem 2x2-Widget: dort bleiben rechts zwei
1x1-Plätze frei, die ein reines Anhängen dauerhaft leer ließe.

Persistenz unterscheidet zwei Fälle, die gleich aussehen und es nicht sind. Eine
leere Layout-Datei ist eine Aussage — der Nutzer hat alle Widgets entfernt. Eine
Datei, aus der nach dem Filtern unbekannter Kennungen nichts übrig bleibt, ist
dagegen ein Zeichen, dass sich die Kennungen geändert haben; dort wäre ein leeres
Panel eine stille Fehlfunktion, also greift das Standardlayout.

Beschädigte und fehlende Dateien führen beide zum Standardlayout statt zu einem
Absturz oder einem leeren Panel.

Die Panelgröße folgt dem Layout statt umgekehrt. Positioniert wird über
LayoutEngine.frame, nicht über LazyVGrid — das kann keine Kacheln über zwei
Zeilen führen, und genau das braucht der Mini-Monat.

Platzhalter-Widgets für alle 13 geplanten Karten. Sie tragen die endgültigen
Kennungen, damit gespeicherte Layouts weitergelten, wenn die echten Widgets sie
Phase für Phase ersetzen.

66 Tests grün.
2026-08-10 17:32:48 +02:00
Guido Schmit
715031f153 Panel wächst aus der Notch, statt einzublenden
Die Fläche wird nicht eingeblendet — sie beginnt exakt in der Notch-Silhouette
und wächst von dort nach unten und zur Seite, deckungsgleich mit der schwarzen
Hardware darüber. Eingeblendetes Panel wirkt wie Glas; Onyx soll sich wie ein
fester Körper anfühlen, der herausfährt.

Der Inhalt hat eine eigene Kurve: beim Öffnen um 90 ms verzögert, damit die
Fläche vorweg läuft und kein Text sich mitdehnt; beim Schließen sofort weg,
damit beim Einfahren nichts gestaucht wird. Die Eckenrundung wandert mit
(10 → 26), sonst sieht die kleine Form aufgeblasen aus.

Drei Fallstricke, die im Code stehen:

Der Fensterschatten folgt dem Fensterrahmen, nicht der gezeichneten Form. Das
Fenster hat immer volle Panelgröße, damit die Animation Platz hat — der
Systemschatten läge also die ganze Zeit um eine unsichtbare Fläche. Deshalb
hasShadow = false und Schatten aus SwiftUI.

Ausgefahren wird erst im nächsten Runloop-Durchlauf. Im selben Durchlauf wie
orderFrontRegardless sieht SwiftUI keinen Zustandswechsel, sondern nur den
Endzustand, und das Panel wäre schlagartig da.

NotchHostingView nimmt Klicks nur dort an, wo etwas zu sehen ist. Sonst
verschluckt das volle Fenster während des Aus- und Einfahrens Klicks in seiner
leeren Fläche.
2026-08-10 17:25:27 +02:00
Guido Schmit
fbe13fd1cb Phase 1: Notch-Shell, Design-System, Menüleisten-Infrastruktur
Zustandsmaschine und Geometrie sind testgetrieben entstanden und tragen 40
Tests — sie sind frei von AppKit, damit jeder Übergang ohne Fenster und ohne
Warten prüfbar ist. Zeit kommt nur als Ereignis herein.

Zwei Entscheidungen, die im Code begründet sind:

Der Zeiger wird über einen globalen Ereignismonitor verfolgt, nicht über ein
unsichtbares Fenster auf der Notch. Ein solches Fenster müsste Mausereignisse
annehmen, um sie zu bemerken, und würde damit Menüleiste und Fensterknöpfe
darunter unbenutzbar machen.

"Nur internes Display" fällt auf ein externes zurück, wenn kein eingebautes da
ist. Am Dock mit geschlossenem Deckel hieße die Einstellung wörtlich genommen,
dass Onyx unerreichbar wird.

Design: NSVisualEffectView mit eigenem Tint statt Liquid Glass — Onyx ist Stein,
kein Glas. Alle Farben und Maße liegen als Tokens in OnyxDesign.

Menüleiste: jedes Modul bekommt ein eigenes NSStatusItem. Elemente, denen macOS
mangels Platz keine Breite gibt, werden erkannt und gemeldet, statt still zu
verschwinden.

App-Target über XcodeGen, damit die Projektdefinition im Diff lesbar bleibt.
Nicht sandboxed, mit App-Group-Entitlement — baut, startet, signiert mit
PP34X97WS3.
2026-08-10 17:17:16 +02:00