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.
146 lines
5.4 KiB
Swift
146 lines
5.4 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
import OnyxNotch
|
|
import OnyxWidgetKit
|
|
import OnyxMenuBar
|
|
|
|
/// Der geteilte Zustand von Onyx: was im Panel liegt und auf welchen Displays.
|
|
///
|
|
/// Änderungen wirken sofort. Eine Einstellung, die erst nach einem Neustart
|
|
/// greift, ist in einer App ohne Fenster praktisch unauffindbar — man sieht ja
|
|
/// nicht, dass etwas passiert ist.
|
|
@MainActor
|
|
@Observable
|
|
final class AppModel {
|
|
|
|
private let store: LayoutStore
|
|
private let defaults: UserDefaults
|
|
|
|
/// Wird gerufen, wenn sich die Panelgröße geändert haben könnte.
|
|
var onLayoutChanged: (([WidgetPlacement]) -> Void)?
|
|
/// Wird gerufen, wenn Panels auf anderen Displays erscheinen sollen.
|
|
var onDisplayPolicyChanged: ((DisplayPolicy) -> Void)?
|
|
|
|
var layout: [WidgetPlacement] {
|
|
didSet {
|
|
guard layout != oldValue else { return }
|
|
persistLayout()
|
|
onLayoutChanged?(layout)
|
|
}
|
|
}
|
|
|
|
var displayPolicy: DisplayPolicy {
|
|
didSet {
|
|
guard displayPolicy != oldValue else { return }
|
|
defaults.set(displayPolicy.rawValue, forKey: Keys.displayPolicy)
|
|
onDisplayPolicyChanged?(displayPolicy)
|
|
}
|
|
}
|
|
|
|
var showsMenuBarIcon: Bool {
|
|
didSet { defaults.set(showsMenuBarIcon, forKey: Keys.showsMenuBarIcon) }
|
|
}
|
|
|
|
/// Ob Onyx im Dock auftaucht.
|
|
///
|
|
/// Standardmäßig nicht: Onyx ist ein Werkzeug, das in der Notch und in der
|
|
/// Menüleiste lebt, hat kein Hauptfenster und wäre im Dock ein Symbol, das
|
|
/// beim Anklicken nichts öffnet. Es taucht dann auch im Programmumschalter
|
|
/// auf und steht dort zwischen den Programmen, mit denen man arbeitet.
|
|
var showsDockIcon: Bool {
|
|
didSet {
|
|
guard showsDockIcon != oldValue else { return }
|
|
defaults.set(showsDockIcon, forKey: Keys.showsDockIcon)
|
|
NSApp.setActivationPolicy(showsDockIcon ? .regular : .accessory)
|
|
}
|
|
}
|
|
|
|
/// Der Menüleisten-Controller. Wird nach dem Aufbau gesetzt, damit
|
|
/// Änderungen aus den Einstellungen sofort durchschlagen.
|
|
weak var menuBar: MenuBarController?
|
|
|
|
/// Die gespeicherten Einstellungen eines Menüleisten-Moduls.
|
|
///
|
|
/// Standardmäßig ist **kein** Modul an. Sechs Symbole beim ersten Start
|
|
/// wären eine Zumutung — wer sie will, schaltet sie einzeln ein.
|
|
func menuBarSettings(for moduleID: String) -> MenuBarModuleSettings {
|
|
guard let data = defaults.data(forKey: Keys.menuBarModule(moduleID)),
|
|
let decoded = try? JSONDecoder().decode(MenuBarModuleSettings.self, from: data)
|
|
else { return MenuBarModuleSettings() }
|
|
return decoded
|
|
}
|
|
|
|
func setMenuBarSettings(_ settings: MenuBarModuleSettings, for moduleID: String) {
|
|
if let data = try? JSONEncoder().encode(settings) {
|
|
defaults.set(data, forKey: Keys.menuBarModule(moduleID))
|
|
}
|
|
menuBar?.setEnabled(settings.isEnabled, forModuleID: moduleID)
|
|
menuBar?.setPresentation(settings.presentation, forModuleID: moduleID)
|
|
menuBarRevision += 1
|
|
}
|
|
|
|
/// Zählt hoch, wenn sich ein Modul ändert — die Einstellungen zeichnen sich
|
|
/// daran neu, ohne dass jedes Modul einzeln beobachtet werden müsste.
|
|
private(set) var menuBarRevision = 0
|
|
|
|
private enum Keys {
|
|
static func menuBarModule(_ id: String) -> String { "onyx.menubar.module.\(id)" }
|
|
static let displayPolicy = "onyx.displayPolicy"
|
|
static let showsMenuBarIcon = "onyx.showsMenuBarIcon"
|
|
static let showsDockIcon = "onyx.showsDockIcon"
|
|
}
|
|
|
|
init(store: LayoutStore = .standard(), defaults: UserDefaults = .standard) {
|
|
self.store = store
|
|
self.defaults = defaults
|
|
|
|
let known = Set(WidgetRegistry.shared.all.map(\.id))
|
|
self.layout = store.load(knownWidgetIDs: known)
|
|
|
|
self.displayPolicy = defaults.string(forKey: Keys.displayPolicy)
|
|
.flatMap(DisplayPolicy.init(rawValue:)) ?? .builtInOnly
|
|
|
|
// Standardmäßig sichtbar: das Menüleistensymbol ist der einzige Weg
|
|
// zurück, wenn die Notch einmal nicht reagiert.
|
|
self.showsMenuBarIcon = defaults.object(forKey: Keys.showsMenuBarIcon) as? Bool ?? true
|
|
self.showsDockIcon = defaults.object(forKey: Keys.showsDockIcon) as? Bool ?? false
|
|
}
|
|
|
|
// MARK: - Widgets
|
|
|
|
var panelContentSize: CGSize { WidgetGrid.panelSize(for: layout) }
|
|
|
|
func isInPanel(_ widgetID: String) -> Bool {
|
|
layout.contains { $0.widgetID == widgetID }
|
|
}
|
|
|
|
func add(_ widget: any OnyxWidget) {
|
|
guard !isInPanel(widget.id) else { return }
|
|
let size = widget.supportedSizes.first ?? .small
|
|
layout.append(WidgetPlacement(widgetID: widget.id, size: size))
|
|
}
|
|
|
|
func remove(_ widgetID: String) {
|
|
layout.removeAll { $0.widgetID == widgetID }
|
|
}
|
|
|
|
func setSize(_ size: WidgetSize, for widgetID: String) {
|
|
guard let index = layout.firstIndex(where: { $0.widgetID == widgetID }) else { return }
|
|
layout[index].size = size
|
|
}
|
|
|
|
func move(fromOffsets source: IndexSet, toOffset destination: Int) {
|
|
layout.move(fromOffsets: source, toOffset: destination)
|
|
}
|
|
|
|
private func persistLayout() {
|
|
do {
|
|
try store.save(layout)
|
|
} catch {
|
|
// Ein fehlgeschlagener Schreibvorgang darf die App nicht anhalten;
|
|
// beim nächsten Start greift dann das Standardlayout.
|
|
NSLog("Onyx: Layout konnte nicht gespeichert werden — \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|