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>
This commit is contained in:
Scarriffle
2026-08-11 12:46:06 +02:00
parent 386eb6ae70
commit 70a665f532
9 changed files with 969 additions and 671 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -35,13 +35,18 @@ public final class MetricMenuBarModule: MenuBarModule {
case .gpu: "GPU"
case .memory: "RAM"
case .battery: String(localized: "menubar.short.battery", bundle: .module)
case .temperature: String(localized: "menubar.short.temperature", bundle: .module)
// Der gewählte Sensor gibt sein eigenes Kürzel Temp" neben einer
// Zahl sagt nicht, welcher Punkt gemeint ist.
case .temperature: model.selectedSensorName
?? String(localized: "menubar.short.temperature", bundle: .module)
}
}
public func makeStatusView(presentation: MenuBarPresentation) -> NSView {
let view = MetricStatusView(metric: metric, presentation: presentation,
label: shortLabel)
view.sensorCelsius = model.selectedSensorCelsius
view.sensorCelsius = model.selectedSensorCelsius
view.update(snapshot: model.snapshot, history: model.series(metric),
mode: model.displayMode(for: metric))
self.view = view
@@ -117,13 +122,17 @@ final class MetricStatusView: NSView, WidthReporting {
/// jedes Prozent alle Symbole rechts davon hin und her.
private var width: CGFloat {
switch presentation {
case .symbol: MenuBarText.minimumWidth
case .symbol: max(MenuBarText.minimumWidth,
MenuBarText.symbolWidth(metric.symbolName, height: 15) + 4)
case .graph: 28
case .bars: barsWidth
case .value: MenuBarText.width(for: reference)
case .valueAndGraph: MenuBarText.width(for: reference) + 2 + 28
case .labelAndValue: MenuBarText.width(for: "\(label) \(reference)")
case .symbolAndValue: MenuBarText.width(for: reference, includesSymbol: true)
case .symbolAndValue:
MenuBarText.width(for: reference)
+ MenuBarText.symbolWidth(metric.symbolName, height: 13)
+ MenuBarText.innerGap
}
}
@@ -135,6 +144,15 @@ final class MetricStatusView: NSView, WidthReporting {
return min(CGFloat(count) * 2 + CGFloat(count - 1) * 1 + 4, 60)
}
/// Was tatsächlich dasteht. Bei den Sensoren der gewählte Punkt statt des
/// stillen Maximums über alle.
private var displayValue: String {
if metric == .temperature, let celsius = sensorCelsius {
return MetricFormat.temperature(celsius)
}
return MetricSummary.value(metric, snapshot, mode: mode)
}
/// Die breiteste Zeichenfolge dieser Metrik.
private var reference: String {
switch (metric, mode) {
@@ -147,6 +165,9 @@ final class MetricStatusView: NSView, WidthReporting {
/// das Element danach neu ein.
var onWidthChange: ((CGFloat) -> Void)?
/// Der gewählte Sensor, falls einer gewählt ist.
var sensorCelsius: Double?
func update(snapshot: MetricsSnapshot, history: [Double], mode: MetricDisplayMode) {
let before = width
self.snapshot = snapshot
@@ -168,7 +189,7 @@ final class MetricStatusView: NSView, WidthReporting {
switch presentation {
case .value:
drawText(MetricSummary.value(metric, snapshot, mode: mode), color: color)
drawText(displayValue, color: color)
case .symbol:
drawSymbol(color: color)
case .graph:
@@ -176,18 +197,21 @@ final class MetricStatusView: NSView, WidthReporting {
case .bars:
drawBars(color: color)
case .labelAndValue:
drawText("\(label) \(MetricSummary.value(metric, snapshot, mode: mode))", color: color)
drawText("\(label) \(displayValue)", color: color)
case .symbolAndValue:
let glyph = MenuBarText.symbolWidth(metric.symbolName, height: 13)
MenuBarText.drawSymbol(metric.symbolName, color: color,
in: NSRect(x: 0, y: bounds.midY - 6.5,
width: 13, height: 13))
drawText(MetricSummary.value(metric, snapshot, mode: mode), color: color,
in: NSRect(x: 15, y: 0, width: bounds.width - 15, height: bounds.height))
width: glyph, height: 13))
drawText(displayValue, color: color,
in: NSRect(x: glyph + MenuBarText.innerGap, y: 0,
width: bounds.width - glyph - MenuBarText.innerGap,
height: bounds.height))
case .valueAndGraph:
let split = bounds.width * 0.55
drawText(MetricSummary.value(metric, snapshot, mode: mode), color: color,
drawText(displayValue, color: color,
in: NSRect(x: 0, y: 0, width: split, height: bounds.height))
drawGraph(color: color,
in: NSRect(x: split + 2, y: 5,

View File

@@ -18,6 +18,36 @@ public final class MetricsModel {
/// `nil` heißt: kein Regler, weil es nichts zu regeln gibt.
public weak var chargeControl: (any ChargeLimitReading)?
/// Welcher Sensor in der Menüleiste steht. `nil` = der wärmste.
///
/// Ohne diese Wahl zeigte das Modul stumm das Maximum über alle Sensoren
/// eine Zahl, von der niemand weiß, woher sie kommt. Der wärmste Punkt"
/// bleibt die Voreinstellung, weil das die Frage ist, die man meistens
/// hat; aber wer den Akku oder die GPU im Blick behalten will, soll das
/// sagen können.
public var selectedSensorKey: String? {
didSet {
guard selectedSensorKey != oldValue else { return }
defaults.set(selectedSensorKey, forKey: "onyx.metric.sensorKey")
}
}
/// Der Wert des gewählten Sensors oder der wärmste, wenn keiner gewählt
/// ist oder der gewählte gerade nicht meldet.
public var selectedSensorCelsius: Double? {
if let key = selectedSensorKey,
let sensor = snapshot.sensors.first(where: { $0.key == key }) {
return sensor.celsius
}
return snapshot.sensors.map(\.celsius).max()
}
/// Wie der gewählte Sensor heißt fürs Kürzel in der Menüleiste.
public var selectedSensorName: String? {
guard let key = selectedSensorKey else { return nil }
return snapshot.sensors.first(where: { $0.key == key })?.name
}
public private(set) var snapshot = MetricsSnapshot()
/// Verlauf für die Graphen, jüngster Wert zuletzt.
public private(set) var history: [MetricsSnapshot] = []
@@ -37,6 +67,7 @@ public final class MetricsModel {
private var demands: [UUID: TimeInterval] = [:]
public init(defaults: UserDefaults = .standard) {
self.selectedSensorKey = defaults.string(forKey: "onyx.metric.sensorKey")
self.defaults = defaults
for metric in MetricKind.allCases where metric.supportsDisplayModes {
let stored = defaults.string(forKey: Self.key(metric))