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>
170 lines
6.5 KiB
Swift
170 lines
6.5 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import OnyxMenuBar
|
|
import MetricsProvider
|
|
|
|
/// Das Netzwerk in der Menüleiste.
|
|
///
|
|
/// Anders als die Hardwaregrößen hat es **zwei** Werte, die beide interessieren.
|
|
/// Sie untereinander zu setzen ist der einzige Weg, beide in der Höhe einer
|
|
/// Menüleiste unterzubringen — nebeneinander bräuchte es die doppelte Breite,
|
|
/// und die hat man dort nicht.
|
|
@MainActor
|
|
public final class NetworkMenuBarModule: MenuBarModule {
|
|
|
|
public let id = "network"
|
|
public var displayName: String { String(localized: "network.name", bundle: .module) }
|
|
|
|
private let model: NetworkModel
|
|
private var token: UUID?
|
|
private var view: NetworkStatusView?
|
|
|
|
public init(model: NetworkModel) { self.model = model }
|
|
|
|
public var shortLabel: String { String(localized: "menubar.short.network", bundle: .module) }
|
|
|
|
public func makeStatusView(presentation: MenuBarPresentation) -> NSView {
|
|
let view = NetworkStatusView(presentation: presentation)
|
|
view.update(model.snapshot, history: model.series(download: true))
|
|
self.view = view
|
|
return view
|
|
}
|
|
|
|
public func makePopoverView() -> AnyView {
|
|
AnyView(NetworkPopoverContent(model: model))
|
|
}
|
|
|
|
public func activate() {
|
|
guard token == nil else { return }
|
|
token = model.addConsumer(interval: 2)
|
|
startObserving()
|
|
}
|
|
|
|
public func deactivate() {
|
|
if let token { model.removeConsumer(token) }
|
|
token = nil
|
|
view = nil
|
|
}
|
|
|
|
private func startObserving() {
|
|
withObservationTracking {
|
|
_ = model.snapshot
|
|
} onChange: {
|
|
Task { @MainActor [weak self] in
|
|
guard let self, token != nil else { return }
|
|
view?.update(model.snapshot, history: model.series(download: true))
|
|
startObserving()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final class NetworkStatusView: NSView {
|
|
|
|
private let presentation: MenuBarPresentation
|
|
private var snapshot = NetworkSnapshot()
|
|
private var history: [Double] = []
|
|
|
|
init(presentation: MenuBarPresentation) {
|
|
self.presentation = presentation
|
|
super.init(frame: NSRect(x: 0, y: 0, width: Self.width(presentation), height: 22))
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
/// Breiter als die Hardwaremodule: „↓ 2,4 MB/s" braucht schlicht mehr Platz
|
|
/// als „46 %".
|
|
/// Gemessen an der breitesten Zeichenfolge, die vorkommen kann.
|
|
///
|
|
/// „↓ 999,9 MB/s" ist der Extremfall — meistens steht dort „↓ 12 KB/s".
|
|
/// Vorher waren 76 Punkte fest verdrahtet, und daneben blieb dauerhaft
|
|
/// Platz für ein weiteres Symbol frei, in dem nichts stand.
|
|
static func width(_ presentation: MenuBarPresentation) -> CGFloat {
|
|
switch presentation {
|
|
case .symbol: MenuBarText.minimumWidth
|
|
case .graph, .bars: 28
|
|
case .value: ratesWidth
|
|
case .valueAndGraph: ratesWidth + 26
|
|
// Die Raten tragen mit ↓ und ↑ ihre Beschriftung schon in sich.
|
|
case .labelAndValue, .symbolAndValue: ratesWidth + MenuBarText.symbolWidth + 2
|
|
}
|
|
}
|
|
|
|
/// Die Raten stehen zweizeilig in 9 pt — dafür misst `MenuBarText` mit
|
|
/// seiner 11-pt-Schrift zu breit, also hier eigens gemessen.
|
|
private static var ratesWidth: CGFloat {
|
|
let font = NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .regular)
|
|
let widest = NSAttributedString(string: "↓ 999,9 MB/s", attributes: [.font: font])
|
|
return ceil(widest.size().width) + 2 * MenuBarText.sidePadding
|
|
}
|
|
|
|
override var intrinsicContentSize: NSSize {
|
|
NSSize(width: Self.width(presentation), height: 22)
|
|
}
|
|
|
|
func update(_ snapshot: NetworkSnapshot, history: [Double]) {
|
|
self.snapshot = snapshot
|
|
self.history = history
|
|
needsDisplay = true
|
|
}
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
let color = NSColor.labelColor
|
|
|
|
switch presentation {
|
|
case .symbol:
|
|
MenuBarText.drawSymbol(snapshot.interfaceKind.symbolName, color: color,
|
|
in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5,
|
|
width: 15, height: 15))
|
|
|
|
case .graph, .bars:
|
|
drawGraph(color: color, in: bounds.insetBy(dx: 2, dy: 5))
|
|
|
|
case .labelAndValue, .symbolAndValue:
|
|
MenuBarText.drawSymbol(snapshot.interfaceKind.symbolName, color: color,
|
|
in: NSRect(x: 0, y: bounds.midY - 6.5, width: 13, height: 13))
|
|
drawRates(color: color,
|
|
in: NSRect(x: 15, y: 0, width: bounds.width - 15, height: bounds.height))
|
|
|
|
case .value, .valueAndGraph:
|
|
drawRates(color: color,
|
|
in: presentation == .value ? bounds
|
|
: NSRect(x: 0, y: 0, width: bounds.width - 26, height: bounds.height))
|
|
if presentation == .valueAndGraph {
|
|
drawGraph(color: color,
|
|
in: NSRect(x: bounds.width - 24, y: 5, width: 22, height: 12))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Zwei Zeilen à 9 pt — die einzige Art, Hoch und Runter in 22 pt Höhe
|
|
/// unterzubringen.
|
|
private func drawRates(color: NSColor, in rect: NSRect) {
|
|
let attributes: [NSAttributedString.Key: Any] = [
|
|
.font: NSFont.monospacedDigitSystemFont(ofSize: 9, weight: .regular),
|
|
.foregroundColor: color,
|
|
]
|
|
let down = NSAttributedString(string: "↓ " + Throughput.formatted(snapshot.downloadRate),
|
|
attributes: attributes)
|
|
let up = NSAttributedString(string: "↑ " + Throughput.formatted(snapshot.uploadRate),
|
|
attributes: attributes)
|
|
down.draw(at: NSPoint(x: rect.minX + 2, y: rect.midY - 0.5))
|
|
up.draw(at: NSPoint(x: rect.minX + 2, y: rect.midY - 10.5))
|
|
}
|
|
|
|
private func drawGraph(color: NSColor, in rect: NSRect) {
|
|
guard history.count > 1, let context = NSGraphicsContext.current?.cgContext else { return }
|
|
let step = rect.width / CGFloat(history.count - 1)
|
|
context.setStrokeColor(color.withAlphaComponent(0.85).cgColor)
|
|
context.setLineWidth(1)
|
|
for (index, value) in history.enumerated() {
|
|
let point = CGPoint(x: rect.minX + CGFloat(index) * step,
|
|
y: rect.minY + rect.height * CGFloat(min(max(value, 0), 1)))
|
|
index == 0 ? context.move(to: point) : context.addLine(to: point)
|
|
}
|
|
context.strokePath()
|
|
}
|
|
}
|
|
|