Commit Graph

21 Commits

Author SHA1 Message Date
Scarriffle
70a665f532 Menüleiste: Breite mitwachsend, Symbole unverzerrt, Sensor wählbar
Die Netzwerkkachel war auf „↓ 999,9 MB/s" dimensioniert und stand damit
neben „↓ 13 KB/s" zur Hälfte leer. Auf den schlimmsten Fall auszulegen ist
für einen Wert, der sich um den Faktor tausend ändert, die falsche Antwort.
Gemessen wird jetzt, was dasteht — mit unsymmetrischer Hysterese: sofort
wachsen, denn abgeschnittene Messwerte sind falsche Messwerte, und erst nach
zehn ruhigeren Messungen schrumpfen, damit ein Lastausschlag die Leiste nicht
zum Pumpen bringt.

Das Akkusymbol war zerquetscht. Ein Batteriesymbol ist doppelt so breit wie
hoch und wurde in ein Quadrat gezeichnet. Symbole behalten jetzt ihr
Seitenverhältnis, und die Breite des Elements richtet sich nach dem, was das
Symbol tatsächlich braucht — sonst hätte ein breites Glyph dieselbe Spalte
wie ein schmales.

Bei den Sensoren stand stumm das Maximum über alle Punkte: eine Zahl, von
der niemand weiß, woher sie kommt. Jetzt wählbar, und der gewählte Sensor
gibt sein eigenes Kürzel — „Temp 46°" sagt weniger als „Heatpipe 46°".
Voreinstellung bleibt der wärmste Punkt, weil das die häufigste Frage ist.

Und die Lüfterkachel zeigte „2500" ohne Einheit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:46:06 +02:00
Scarriffle
386eb6ae70 Ladelimit ist eine Anzeige, Akku-Kachel sagt etwas, Menüleiste bekommt Kürzel
Der Regler war eine Pseudobedienung. Der SMC lehnt Schreibzugriffe auf CHLT
ab — der Helfer hat es mit korrekter Größe versucht und „geschrieben (false),
zurückgelesen 85" gemeldet. Das Sicherheitsnetz hat also die Wahrheit gesagt;
die Oberfläche hat sie nur nicht gezeigt.

Weiter zu raten hieße, im Lade-Subsystem Keys durchzuprobieren, und das ist
die eine Operation in diesem Projekt, die Hardware dauerhaft beschädigen
kann. Also: das Limit wird gelesen und angezeigt, mit einem Weg in die
Systemeinstellungen, wo es tatsächlich geht. Der Schreibpfad ist aus dem
privilegierten Helfer entfernt — ein Root-Dienst trägt nur, was benutzt wird.

Die Akku-Kachel zeigte „40 %" und einen Verlauf über die letzten Sekunden.
Der Ladestand ändert sich in Minuten, nicht in Sekunden; der Graph war eine
waagerechte Linie. Jetzt: verbleibende Zeit bzw. Zeit bis voll, Leistung in
Watt mit Richtung, Ladelimit, Zustand, Zyklen — und der Energiesparmodus.
Der erklärt, warum die Maschine langsamer wirkt, und ohne diesen Hinweis
sucht man den Grund woanders.

In der Menüleiste standen fünf nackte Prozentzahlen nebeneinander. Man sah
Zahlen und wusste nicht, welche wozu gehört. Zwei neue Darstellungen: Kürzel
und Wert („CPU 34 %") sowie Symbol und Wert. Beim Akku stehen sie zuoberst in
der Auswahl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:37:04 +02:00
Scarriffle
a54b332723 Notch: Toleranz für zitternde Hände, „Panel öffnen" fliegt raus
Warum meine Proben grün waren und deine Hand nicht: jedes einzelne
Abtastbild außerhalb der Auslösefläche hat das Aufziehen sofort
abgebrochen. Ein Zeiger, den eine Hand an den oberen Rand führt, steht
dort aber nicht still — er wandert um ein paar Punkte. Damit fing die
Entprellzeit von 220 ms dauernd von vorn an und kam nie ans Ziel.
`CGWarpMouseCursorPosition` setzt den Zeiger auf einen Punkt und hält ihn
absolut still; deshalb war jede automatisierte Probe 19 von 19 grün.

Kurzes Herausrutschen bricht jetzt nicht mehr ab: die Entprellzeit läuft
weiter, parallel läuft die Abbruchfrist von 300 ms. Kommt der Zeiger
zurück, wird die Frist gestoppt — die Entprellzeit setzt bewusst nicht neu
an, sonst dauerte es bei jedem Grenzübertritt wieder von vorn.

Das allein hätte einen neuen Fehler gebracht, und ein vorhandener Test hat
ihn gefunden: beim beiläufigen Streifen der Notch wäre die Entprellzeit
abgelaufen, während der Zeiger längst weg war — das Panel wäre für ein
Zehntel aufgeblitzt. Die Maschine merkt sich deshalb, wo der Zeiger steht,
und öffnet nur, wenn er da ist. Ist die Entprellzeit abgelaufen, während er
draußen war, geht es beim Zurückkommen ohne weiteres Warten auf.

Die Auslösefläche reicht außerdem 14 statt 4 Punkte unter die Notch. Vier
Punkte sind keine Fläche, die eine Hand trifft.

„Panel öffnen" ist aus dem Menü verschwunden. Ein Menüeintrag für etwas,
das beim Berühren der Notch von selbst passiert, beschreibt einen Umweg um
die eigentliche Bedienung herum. Als Notausgang bleibt es erreichbar, nur
ohne eigene Zeile: Linksklick auf das Symbol fährt das Panel aus,
Rechtsklick zeigt Einstellungen und Beenden.

Dazu Protokollierung jedes Zustandswechsels und jeder Unterdrückung. Ohne
diese Spur ist ein Panel, das sich „manchmal nicht öffnet", nicht zu
untersuchen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:12:31 +02:00
Scarriffle
b2f69f34ae Ladelimit: Key gemessen, Schreibpfad gebaut
Der Key heißt CHLT, drei Bytes. Identifiziert per Differenzmessung gegen
macOS' eigene Ladebegrenzung, ohne einen einzigen Schreibzugriff:

  80 %  →  50 01 00
  95 %  →  5F 01 00
  85 %  →  55 01 00

Erstes Byte ist die Prozentzahl, zweites offenbar „Limit aktiv", drittes
unbenutzt. Drei Messpunkte, weil einer Zufall sein kann: BACC hatte beim
ersten Vergleich ebenfalls gepasst — ein Zähler, in dem gerade 50 stand.

ChargerConfiguration aus ioreg sah zunächst wie eine zweite Quelle aus
(niedriges Byte 0x50 = 80), blieb aber unverändert, als CHLT längst auf
95 stand. Also Zufall, und gut, dass darauf nichts gebaut wurde.

Geschrieben wird nur das erste Byte, und nur zwischen 80 und 100 — dem
Bereich, den macOS selbst anbietet und in dem gemessen wurde. Darunter ist
ungemessenes Gebiet. Bei den Lüftern liefert die Firmware mit F0Mn/F0Mx
eigene Grenzen mit, an denen sich ein Sicherheitsnetz festhalten kann; hier
gibt es das nicht, hier gibt es nur die Messung. Ein falscher Wert im
Lade-Subsystem ist die eine Operation in diesem Projekt, die den Akku
dauerhaft beschädigen kann.

Nach jedem Schreiben wird zurückgelesen und gemeldet, was tatsächlich
steht — nicht, was gewünscht war. „Kein Limit" heißt 100 Prozent und nicht
das Aktiv-Byte auf null: diese Kodierung hat macOS nie geschrieben, sie
wäre also ungemessen.

Bedienung im Lüfter-Bereich der Einstellungen und in der Lüfterkachel,
also auch im Menüleisten-Popover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:01:50 +02:00
Scarriffle
453353de58 Notch öffnet zuverlässig, Einstellungen beenden Onyx nicht mehr
Das Öffnen hing allein an Mausereignissen. Der Auslösebereich liegt aber in
der Menüleiste, und dorthin stellt macOS Bewegungen nicht verlässlich an
fremde Programme zu — mal kommen sie an, mal nicht, je nachdem, was gerade
den Zeiger führt. Ein Verfahren, das eine Zustellung voraussetzt, die es
nicht gibt, kann nicht zuverlässig sein.

Die Zeigerposition wird deshalb jetzt immer abgefragt, im Ruhezustand
zehnmal je Sekunde, bei offenem Panel sechzigmal. Die Monitore sind nur noch
Beschleunigung. Kosten: 0,1 % CPU im Leerlauf, gemessen. Nachgeprüft mit
fünf Anläufen aus verschiedenen Richtungen — fünfmal offen.

Die Unterdrückung war zu grob: jedes Vollbildfenster galt als Grund, das
Panel wegzuhalten. Damit war die Notch tot, sobald Xcode im Vollbild lief.
Gemeint war immer nur der Film über den ganzen Bildschirm, und den erkennt
man daran, dass etwas den Bildschirm wachhält — ein Textfenster tut das
nicht. Beide Bedingungen zusammen, nicht eine davon.

Dazu ein Loch, das dabei auffiel: fiel die Unterdrückung weg, während der
Zeiger schon in der Notch stand, wartete die Maschine auf ein Ereignis, das
erst bei der nächsten Bewegung kam.

Der rote Knopf blendet Fenster jetzt aus, statt sie zu schließen. Onyx hat
kein Hauptfenster; „Fenster zu" heißt hier nie „fertig". Manche
Hilfsprogramme erzwingen aber genau diese Gleichung und beenden eine App,
sobald ihr letztes Fenster verschwindet — dann war Onyx nach einem Blick in
die Einstellungen weg, samt Menüleiste und Notch. Ein Fenster, das nie
wirklich schließt, gibt dafür keinen Anlass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:21:00 +02:00
Scarriffle
b22a44ba70 Mediensteuerung hochkant, Symbole wieder sichtbar, „Auto" als Zustand
Die Mediensteuerung stand nebeneinander: Cover links, alles andere in den
verbleibenden gut hundert Punkten. Daher „Bernhard H…" statt eines Titels.
Jetzt übereinander, wie es zu einer hochkanten Kachel passt.

Das Cover ragte zusätzlich über die Kachel hinaus, weil `clipShape` vor dem
Rahmen stand — beschnitten wurde damit auf die Eigengröße des Bildes, und
die ist bei einem Podcast-Cover ein Vielfaches. Erst Rahmen, dann
beschneiden.

Menüleistensymbole waren kaum zu sehen. `color.set()` vor `image.draw(in:)`
färbt ein Template-Bild nicht: es wird schwarz gezeichnet, auf einer dunklen
Menüleiste also unsichtbar. AppKit färbt Template-Bilder nur ein, wenn sie
als `image` eines Buttons gesetzt sind — in einer selbst gezeichneten Ansicht
muss die Farbe in die Symbolkonfiguration. Betraf alle fünf Zeichenstellen,
nicht nur die Lüfter.

„fans.auto" stand als roher Schlüssel da: Xcode hatte ihn extrahiert, gefüllt
wurde er nie. Beim Nachsehen waren es 35 solche Einträge über alle Kataloge
hinweg — die meisten reine Formatschlüssel, die als Durchreicher trotzdem
einen Wert brauchen.

Der Automatik-Knopf ist jetzt eine gefüllte Pille, wenn sie greift. Blauer
Text sieht aus wie ein Link und nicht wie ein Zustand.

Und ein Absturz, der noch niemandem passiert ist: meldet der Helfer für einen
stehenden Lüfter keine Grenzen, hätte `Slider(in: 0...0)` die App beendet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:12:33 +02:00
Scarriffle
93fe95827f Panel wächst in die Breite, Menüleiste wird schmaler
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>
2026-08-11 11:07:20 +02:00
Scarriffle
a0342205fd Phase 9: Einrichtung, Autostart, Notarisierung — und die Mixer-Optik
Die Einrichtung beim ersten Start erklärt jede der vier Sachen einzeln und
fragt jede einzeln an. Vier Systemdialoge hintereinander wären der übliche
Weg und der falsche: wer nicht weiß wofür, klickt viermal „Nicht erlauben",
und danach fragt macOS nie wieder. Sie kommt beim ersten Start immer, auch
wenn zufällig schon alles erlaubt ist — sie erklärt auch den Helfer und den
Autostart.

Beim Autostart ist `requiresApproval` der Zustand, der weh tut. Wer das
Anmeldeobjekt in den Systemeinstellungen abgeschaltet hat, kann in Onyx auf
den Schalter drücken, so oft er will. Als „aus" angezeigt führt das in eine
Schleife, also steht dort jetzt, wo es weitergeht.

Der Mixer verliert den Schalter je Programm. Zehn Mini-Schalter untereinander
sind das, was den Eindruck macht; am Regler zu ziehen ist ohnehin die
Entscheidung, dieses Programm zu regeln, und der große Ausschalter stellt
alles zurück. Der Regler ist selbst gezeichnet — der Systemregler bringt sein
eigenes Erscheinungsbild mit und kann nicht zeigen, worauf es hier ankommt:
den Pegel und die Grenze bei 100 %.

Der Pegel wird im selben Durchlauf gemessen, in dem verstärkt wird, und über
`Atomic` weitergereicht. Eine gewöhnliche Eigenschaft wäre ein Wettlauf, eine
Sperre im Audiothread der klassische Weg zu Aussetzern. Dabei fällt das
`Date()` aus dem Echtzeitpfad, das dort nie hingehört hat.

Zwei Funde beim Bauen des Release-Wegs, beide hätten erst nach dem Hochladen
zur Ablehnung geführt: der Helfer wurde ohne sicheren Zeitstempel signiert,
und das Adapter-Framework ad-hoc. Beides gilt jetzt nur noch im Debug.
Scripts/release.sh prüft Zertifikat und Notarisierungsprofil vorweg, statt
nach zehn Minuten Bauen zu scheitern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:36:26 +02:00
Scarriffle
8308536227 Phase 8: Ablage, AirDrop und Calendarr
Calendarr liefert Termine über den Gruppencontainer. Streng lesend: keine
eigene Anmeldung, kein Netz, kein Schreiben zurück.

Die Abdeckung ist der Kern und kein Detail. Der Schnappschuss reicht 49
Tage nach vorne, und das steht nirgends in der Datei — es ist zugesagtes
Verhalten des Schreibers. Außerhalb liegt keine Information vor, nicht
keine Termine; das Monatsraster graut solche Tage aus, statt „frei" zu
behaupten. Verlockend wäre, die Abdeckung aus dem frühesten Termin
abzuleiten. Das ist falsch, und ein Test hält es fest: mehrtägige Termine
ragen ins Fenster hinein und werden mitgeschrieben, obwohl ihr Anfang
nicht abgedeckt ist.

Bei „beide Quellen" ist die Abdeckung der Schnitt, nicht die Vereinigung.
Ein Tag gilt nur als bekannt, wenn ihn jede Quelle abdeckt. Umgekehrt
darf ein Ausfall einer Quelle die andere nicht mitreißen: Calendarr nie
geöffnet zu haben knipst den Apple-Kalender nicht aus.

Die Ablage kopiert, statt zu verweisen. Anhänge aus Mail oder WhatsApp
liegen in temporären Verzeichnissen — ein Verweis wäre in Stunden tot,
und die Ablage wäre leer, wenn man sie braucht. Aus demselben Grund nimmt
die Fläche UTType.item statt public.file-url an: solche Anhänge kommen
als Dateiversprechen, und loadFileRepresentation löst beides ein. Die
übergebene Datei existiert nur innerhalb des Rückrufs, also wird sie dort
sofort gesichert statt später auf dem MainActor.

Gelöscht wird in den Papierkorb. Automatisches Aufräumen ist standardmäßig
aus — eine Ablage, die ungefragt löscht, ist ein Papierkorb mit
Zeitschaltuhr.

Nebenbei: „mixer.on" und „network.vpn.active" standen als rohe Schlüssel
in der Oberfläche. Label(_:systemImage:) kennt kein Bundle und sucht immer
im Hauptbundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 10:16:19 +02:00
Guido Schmit
f48b29e53e Helfer startet nicht: --deep hat ihm die App-Entitlements verpasst
launchd lehnte den Start ab, ohne Absturzbericht und ohne sichtbaren Fehler:

  launchd: exited with exit reason (namespace: 9 code: 0x8) - OS_REASON_EXEC
  amfid: OnyxHelper not valid: Code=-413 "No matching profile found"
  taskgated-helper: Disallowing com.scarriffleservices.onyx.helper
                    because no eligible provisioning profiles found

Ursache war OTHER_CODE_SIGN_FLAGS = --deep, das ich anfangs für das ad-hoc
signierte MediaRemoteAdapter.framework gesetzt hatte. --deep überträgt die
Entitlements der App auf jede eingebettete Binary. Der Helfer bekam dadurch
application-identifier, weatherkit und application-groups verpasst — alles
eingeschränkte Entitlements, die ein Provisioning-Profil verlangen. Ein nacktes
Kommandozeilenprogramm kann aber keines einbetten, denn das ist eine Datei im
Bundle. Also verweigerte AMFI die Ausführung.

Für das Framework wird --deep gar nicht gebraucht: es wird von /usr/bin/perl
geladen, nicht von Onyx, und dafür ist ohnehin disable-library-validation da.

Der Helfer trägt jetzt keine Entitlements — er braucht auch keine. Ein
root-Dienst mit möglichst wenig Rechten ist genau das, was man will.

Gefunden über das Protokoll von amfid und taskgated. Der Weg dorthin führte
über zwei Ausschlüsse: der Helfer läuft direkt gestartet einwandfrei, und seine
Signatur erfüllt die Anforderung der App. Damit war klar, dass es weder an der
Binary noch an der Signatur liegt, sondern an der Umgebung.
2026-08-10 23:01:51 +02:00
Guido Schmit
d73b789fb7 Phase 6: Lüftersteuerung über privilegierten Helfer
Das Sicherheitsnetz kommt zuerst und liegt als reine Funktion vor — 17 Tests,
kein Hardwarezugriff nötig. Es sitzt im Helfer, nicht in der Oberfläche: die
kann abstürzen, hängen oder beendet sein, während die Lüfter auf einem festen
Wert stehen. Genau dafür ist es da.

Vier Regeln, nicht abschaltbar: nie unter das Firmware-Minimum, Rückfall auf
Automatik ab 95 °C, Rückfall wenn der Herzschlag fünf Sekunden ausbleibt,
Rückfall beim Beenden. Ohne lesbare Temperatur wird gar nicht gesteuert —
blind eine feste Drehzahl zu halten wäre die falsche Antwort auf fehlende
Information.

Die App sagt dem Helfer NICHT, wie warm es ist. Er misst selbst. Sonst hinge
das Netz an der Ehrlichkeit eines Prozesses, der abstürzen, hängen oder — bei
einer manipulierten Kopie — schlicht lügen kann. Der Herzschlag sagt nur "ich
lebe noch".

Der Helfer entscheidet jede Sekunde neu, nicht nur beim Setzen: nur so greifen
Wachhund und Temperaturwächter auch dann, wenn von der App nie wieder etwas
kommt. Geschrieben wird nur bei Änderung — jeder SMC-Schreibvorgang ist ein
Eingriff.

Der Schreibpfad existiert ausschließlich im Helfer, als eigene Kopie statt
geteilter Bibliothek. Was im App-Prozess nicht vorhanden ist, kann dort auch
nicht versehentlich aufgerufen werden.

XPC prüft die Signatur in BEIDE Richtungen. Ohne das könnte jedes Programm auf
dem Rechner die Lüfter eines root-Dienstes steuern — der Mach-Dienst ist
systemweit sichtbar.

Drei Fallstricke beim Einbetten, alle nachgemessen:

Xcode signiert Kommandozeilenprogramme unter dem Dateinamen. Der Helfer hieß
damit "OnyxHelper" statt com.scarriffleservices.onyx.helper, und die App hätte
ihren eigenen Helfer abgelehnt. Behoben über eine in die Binary eingebettete
Info.plist (CREATE_INFOPLIST_SECTION_IN_BINARY).

Der Build-Schritt läuft nach Xcodes Signatur; das Kopieren bricht das Siegel
des App-Bundles. Also wird zum Schluss neu signiert — mit denselben
Entitlements, sonst gingen App-Group, WeatherKit und die TCC-Berechtigungen
verloren.

Mit deklarierten outputFiles übersprang Xcode den Schritt, obwohl sich das
Programm geändert hatte.

Geprüft: App-Siegel gültig (deep), beide Signaturanforderungen erfüllt,
alle sieben Entitlements erhalten. 162 Tests grün.
2026-08-10 22:49:41 +02:00
Guido Schmit
d85425671d Aussagekräftige Popovers für CPU, Speicher, Akku und Netzwerk
Ein Popover, das nur den Wert größer wiederholt, den man gerade angeklickt hat,
ist verschenkter Platz. Hier ist Raum — anders als in der Menüleiste, wo jede
Zahl um Breite kämpft.

Prozessor: Ringe für Auslastung, Temperatur und Lüfter, alle Kerne als Raster
statt als Balkenreihe (bei fünfzehn Kernen wird eine Reihe unleserlich schmal),
Leistungsaufschlüsselung und beide Lüfter mit Namen.

Die Leistungsschlüssel sind gegen iStat Menus abgeglichen: PHPC liefert 14,02 W
und deckt sich mit deren CPU-Wert, PSTR mit der Gesamtleistung. Frequenzen
fehlen bewusst — die stehen nicht im SMC, dafür bräuchte es IOReport.

Speicher: Ring mit Aufschlüsselung nach Programmen, Reserviertem, Komprimiertem
und Freiem, Auslagerung mit Gesamtgröße und die fünf größten Verbraucher über
phys_footprint — derselbe Wert, den der Aktivitätsmonitor zeigt, nicht der
virtuelle Adressraum.

Akku: Restzeit, Kapazität, Zyklen, Temperatur, Lade- oder Entladeleistung,
Netzteilleistung und Spannung.

Netzwerk: Verlauf in beide Richtungen auf gemeinsamem Maßstab — getrennte
Maßstäbe ließen einen Upload von 20 KB/s so hoch aussehen wie einen Download
von 20 MB/s. Dazu Spitzenwerte, VPN-Hinweis, alle Adressen und die Summen seit
dem Start.

Was fehlt, wird benannt statt weggelassen: Durchsatz je Programm gibt es ohne
root nicht, das Ladelimit braucht den privilegierten Helfer und einen Key, der
auf diesem Modell noch nicht identifiziert ist.

Sensorliste erweitert und benannt: CPU-Kerne und Grafik stehen vorn, dazu
Luftstrom, Thunderbolt links und rechts. Lüfter heißen links und rechts statt
0 und 1.

Nebenbei ein Übersetzungsfehler behoben: die Modus-Auswahl in den Einstellungen
zeigte die Rohschlüssel, weil die Texte im MetricsProvider-Bündel liegen und
die Einstellungen im App-Bündel suchen.

145 Tests grün.
2026-08-10 22:17:13 +02:00
Guido Schmit
a4cc75ab6e CPU und GPU zwischen Auslastung und Temperatur umschaltbar
Die M-Serie-Konvention gilt auch auf dem M5: Tp* sind die CPU-Kerne, Tg* die
GPU-Cluster. Gezeigt wird der wärmste Punkt, nicht der Durchschnitt —
gedrosselt wird nach dem heißesten Kern. Gemessen: CPU 76 °C, GPU 70 °C.

Die Fühlerliste wird zur Laufzeit gesucht statt fest verdrahtet: welche es
gibt, hängt am Modell. Auf diesem Mac sind es 285 Temperaturfühler.

Zwei Leistungsprobleme, gefunden bevor sie jemandem auffallen konnten:

Die Suche geht alle 3486 Keys durch und braucht knapp eine Sekunde. Auf dem
Hauptthread wäre das eine spürbare Startverzögerung — sie läuft jetzt im
Hintergrund, bis dahin zeigt die Oberfläche "—" statt einer erfundenen Zahl.

Fünfzig Fühler zu lesen kostete 34 ms je Abtastung, bei 1 Hz also 3,4 % CPU.
Zwei Maßnahmen: Typ und Größe je Key werden zwischengespeichert (sie stehen in
der Firmware fest, sie bei jedem Lesen zu erfragen verdoppelte die
IOKit-Aufrufe), und Temperaturen werden höchstens alle zwei Sekunden neu
gelesen — sie bewegen sich in einer Sekunde ohnehin kaum. Ergebnis: 6 ms
Grundlast, 18 ms alle zwei Sekunden, im Mittel gut 1 %.

Dabei noch eine Eigenheit gefunden: die Byte-Reihenfolge im SMC ist nicht
durchgängig. Messwerte wie B0AV kommen little-endian aus dem Akku-Baustein,
die Metadaten des SMC selbst dagegen big-endian. #KEY little-endian gelesen
ergibt 2 651 652 096 statt 3486 — die Fühlersuche brach damit still ab und
alle Temperaturen blieben leer.

Lüfterdrehzahlen stehen jetzt im Sensor-Popover, mit Balken zwischen Minimum
und Maximum der Firmware: "2321 U/min" allein sagt niemandem, ob das viel ist.
Die Steuerung selbst kommt mit Phase 6 — Schreibzugriff auf den SMC verlangt
root — und der Popover sagt das auch.

145 Tests grün.
2026-08-10 22:05:38 +02:00
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
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
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
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
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