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.
This commit is contained in:
@@ -2,6 +2,7 @@ import SwiftUI
|
||||
import AppKit
|
||||
import OnyxNotch
|
||||
import OnyxWidgetKit
|
||||
import OnyxMenuBar
|
||||
|
||||
/// Der geteilte Zustand von Onyx: was im Panel liegt und auf welchen Displays.
|
||||
///
|
||||
@@ -54,7 +55,36 @@ final class AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,8 @@ import OnyxWidgetKit
|
||||
import CalendarProvider
|
||||
import WeatherProvider
|
||||
import MediaProvider
|
||||
import MetricsProvider
|
||||
import NetworkProvider
|
||||
|
||||
@main
|
||||
struct OnyxApp: App {
|
||||
@@ -34,6 +36,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var calendarModel: CalendarModel?
|
||||
private var weatherModel: WeatherModel?
|
||||
private var mediaModel: MediaModel?
|
||||
private var metricsModel: MetricsModel?
|
||||
private var networkModel: NetworkModel?
|
||||
private let settingsWindow = SettingsWindowController()
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
@@ -76,8 +80,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
|
||||
let menuBar = MenuBarController()
|
||||
if let metricsModel {
|
||||
for metric in MetricKind.allCases {
|
||||
menuBar.register(MetricMenuBarModule(metric: metric, model: metricsModel),
|
||||
settings: model.menuBarSettings(for: "metric.\(metric.rawValue)"))
|
||||
}
|
||||
}
|
||||
if let networkModel {
|
||||
menuBar.register(NetworkMenuBarModule(model: networkModel),
|
||||
settings: model.menuBarSettings(for: "network"))
|
||||
}
|
||||
menuBar.start()
|
||||
self.menuBar = menuBar
|
||||
model.menuBar = menuBar
|
||||
|
||||
installOnyxStatusItem()
|
||||
observeMenuBarPreference()
|
||||
@@ -136,6 +151,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
self.mediaModel = mediaModel
|
||||
WidgetRegistry.shared.register(MediaWidget(model: mediaModel))
|
||||
|
||||
let metricsModel = MetricsModel()
|
||||
self.metricsModel = metricsModel
|
||||
for metric in MetricKind.allCases {
|
||||
WidgetRegistry.shared.register(MetricWidget(metric: metric, model: metricsModel))
|
||||
}
|
||||
|
||||
let networkModel = NetworkModel()
|
||||
self.networkModel = networkModel
|
||||
WidgetRegistry.shared.register(NetworkWidget(model: networkModel))
|
||||
|
||||
PlaceholderWidgets.registerAll()
|
||||
}
|
||||
|
||||
|
||||
@@ -44,18 +44,6 @@ enum PlaceholderWidgets {
|
||||
/// damit gespeicherte Layouts weitergelten.
|
||||
static func registerAll() {
|
||||
let widgets: [PlaceholderWidget] = [
|
||||
.init(id: "cpu", displayName: "CPU", symbolName: "cpu",
|
||||
supportedSizes: [.small, .medium], phase: "Phase 5"),
|
||||
.init(id: "gpu", displayName: "GPU", symbolName: "cpu.fill",
|
||||
supportedSizes: [.small, .medium], phase: "Phase 5"),
|
||||
.init(id: "memory", displayName: "Speicher", symbolName: "memorychip",
|
||||
supportedSizes: [.small, .medium], phase: "Phase 5"),
|
||||
.init(id: "battery", displayName: "Akku", symbolName: "battery.100",
|
||||
supportedSizes: [.small, .medium], phase: "Phase 5"),
|
||||
.init(id: "sensors", displayName: "Sensoren", symbolName: "thermometer",
|
||||
supportedSizes: [.medium, .large], phase: "Phase 5"),
|
||||
.init(id: "network", displayName: "Netzwerk", symbolName: "network",
|
||||
supportedSizes: [.small, .medium, .large], phase: "Phase 5"),
|
||||
.init(id: "fans", displayName: "Lüfter", symbolName: "fan",
|
||||
supportedSizes: [.small, .medium], phase: "Phase 6"),
|
||||
.init(id: "audio", displayName: "Audio-Mixer", symbolName: "slider.horizontal.3",
|
||||
|
||||
@@ -3,10 +3,11 @@ import OnyxDesign
|
||||
import OnyxNotch
|
||||
import OnyxWidgetKit
|
||||
import CalendarProvider
|
||||
import OnyxMenuBar
|
||||
import WeatherProvider
|
||||
|
||||
enum SettingsTab: Hashable {
|
||||
case widgets, display, permissions
|
||||
case widgets, display, menubar, permissions
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@@ -23,6 +24,9 @@ struct SettingsView: View {
|
||||
DisplaySettings(model: model)
|
||||
.tabItem { Label("settings.tab.display", systemImage: "macbook") }
|
||||
.tag(SettingsTab.display)
|
||||
MenuBarSettings(model: model)
|
||||
.tabItem { Label("settings.tab.menubar", systemImage: "menubar.rectangle") }
|
||||
.tag(SettingsTab.menubar)
|
||||
PermissionSettings(calendarModel: calendarModel, weatherModel: weatherModel)
|
||||
.tabItem { Label("settings.tab.permissions", systemImage: "hand.raised") }
|
||||
.tag(SettingsTab.permissions)
|
||||
@@ -147,6 +151,99 @@ private struct PermissionRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Menüleiste
|
||||
|
||||
/// Jedes Modul einzeln an- und abschaltbar, mit eigener Darstellungsart.
|
||||
///
|
||||
/// Standardmäßig ist keines an: sechs neue Symbole beim ersten Start wären eine
|
||||
/// Zumutung, und die Menüleiste ist ohnehin knapp — auf einem Notch-Display
|
||||
/// blockiert die Kamera zusätzlich die Mitte.
|
||||
private struct MenuBarSettings: View {
|
||||
@Bindable var model: AppModel
|
||||
|
||||
private static let modules: [(id: String, label: LocalizedStringKey, symbol: String)] = [
|
||||
("metric.cpu", "metric.cpu", "cpu"),
|
||||
("metric.gpu", "metric.gpu", "cpu.fill"),
|
||||
("metric.memory", "metric.memory", "memorychip"),
|
||||
("metric.battery", "metric.battery", "battery.100"),
|
||||
("metric.temperature", "metric.sensors", "thermometer"),
|
||||
("network", "network.name", "network"),
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("settings.menubar.hint")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
List {
|
||||
ForEach(Self.modules, id: \.id) { module in
|
||||
ModuleRow(model: model, id: module.id,
|
||||
label: module.label, symbol: module.symbol)
|
||||
}
|
||||
}
|
||||
.listStyle(.inset)
|
||||
}
|
||||
.padding()
|
||||
// Der Zähler zwingt die Liste zum Neuzeichnen, wenn sich ein Modul
|
||||
// ändert — sonst müsste jede Zeile ihren eigenen Zustand beobachten.
|
||||
.id(model.menuBarRevision)
|
||||
}
|
||||
}
|
||||
|
||||
private struct ModuleRow: View {
|
||||
@Bindable var model: AppModel
|
||||
let id: String
|
||||
let label: LocalizedStringKey
|
||||
let symbol: String
|
||||
|
||||
var body: some View {
|
||||
let settings = model.menuBarSettings(for: id)
|
||||
|
||||
HStack {
|
||||
Toggle(isOn: Binding(
|
||||
get: { settings.isEnabled },
|
||||
set: { enabled in
|
||||
var updated = settings
|
||||
updated.isEnabled = enabled
|
||||
model.setMenuBarSettings(updated, for: id)
|
||||
})) {
|
||||
Label(label, systemImage: symbol)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Picker("", selection: Binding(
|
||||
get: { settings.presentation },
|
||||
set: { presentation in
|
||||
var updated = settings
|
||||
updated.presentation = presentation
|
||||
model.setMenuBarSettings(updated, for: id)
|
||||
})) {
|
||||
ForEach(MenuBarPresentation.allCases) { presentation in
|
||||
Text(presentation.localizedName).tag(presentation)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.frame(width: 150)
|
||||
.disabled(!settings.isEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MenuBarPresentation {
|
||||
var localizedName: LocalizedStringKey {
|
||||
switch self {
|
||||
case .value: "settings.menubar.value"
|
||||
case .graph: "settings.menubar.graph"
|
||||
case .bars: "settings.menubar.bars"
|
||||
case .valueAndGraph: "settings.menubar.valueAndGraph"
|
||||
case .symbol: "settings.menubar.symbol"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Widgets
|
||||
|
||||
private struct WidgetSettings: View {
|
||||
|
||||
Reference in New Issue
Block a user