From 504c518cfbffd7848ee27ddb4355a07d9658089b Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 13 Aug 2026 14:31:19 +0200 Subject: [PATCH] Akku-Popover sagt, was gerade geschieht MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit „Am Netz" beantwortet die Frage nicht, die man hat, wenn der Ladestand stehen bleibt: lädt es gerade nicht, weil das Limit erreicht ist, oder stimmt etwas nicht? Jetzt steht es dort — lädt, angehalten wegen Ladelimit mit der Prozentzahl, am Netz ohne zu laden, oder Akkubetrieb. Der Vergleich mit dem Limit hat zwei Punkte Toleranz: der Ladestand pendelt um die Grenze, und auf das Prozent genau zu vergleichen hieße, den Grund die halbe Zeit zu verschweigen. Weit unter dem Limit wird dagegen kein Limit-Halt behauptet — dann steht das Laden aus einem anderen Grund, und einen falschen zu nennen wäre schlimmer als keinen. Co-Authored-By: Claude Opus 5 --- .../MetricsProvider/ChargeActivity.swift | 37 + .../MetricsProvider/Localizable.xcstrings | 1328 +++++++++-------- .../MetricsProvider/MetricPopovers.swift | 45 + .../ChargeActivityTests.swift | 57 + 4 files changed, 826 insertions(+), 641 deletions(-) create mode 100644 Packages/OnyxKit/Sources/MetricsProvider/ChargeActivity.swift create mode 100644 Packages/OnyxKit/Tests/MetricsProviderTests/ChargeActivityTests.swift diff --git a/Packages/OnyxKit/Sources/MetricsProvider/ChargeActivity.swift b/Packages/OnyxKit/Sources/MetricsProvider/ChargeActivity.swift new file mode 100644 index 0000000..c67ef64 --- /dev/null +++ b/Packages/OnyxKit/Sources/MetricsProvider/ChargeActivity.swift @@ -0,0 +1,37 @@ +import Foundation + +/// Was mit dem Akku gerade geschieht — in Worten statt in Symbolen. +/// +/// „Am Netz" allein beantwortet die Frage nicht, die man hat, wenn der +/// Ladestand stehen bleibt: lädt es gerade nicht, weil das Limit erreicht ist, +/// oder stimmt etwas nicht? +public enum ChargeActivity: Equatable, Sendable { + case charging + /// Angehalten, weil das eingestellte Ladelimit erreicht ist. + case pausedAtLimit(Int) + /// Am Netz, lädt aber nicht — Grund unbekannt. + case pluggedNotCharging + case onBattery + + /// Wie weit unter dem Limit noch als „am Limit" gilt. + /// + /// Der Ladestand pendelt um die Grenze. Auf das Prozent genau zu + /// vergleichen hieße, den Grund die halbe Zeit zu verschweigen. + public static let limitTolerance = 2 + + public static func resolve(isCharging: Bool, isPluggedIn: Bool, + charge: Double, limit: Int?) -> ChargeActivity { + if isCharging { return .charging } + guard isPluggedIn else { return .onBattery } + + let percent = Int((min(max(charge, 0), 1) * 100).rounded()) + // Ein Limit von hundert ist keines. + if let limit, limit < 100, percent >= limit - limitTolerance { + return .pausedAtLimit(limit) + } + // Steht das Laden aus einem anderen Grund — etwa weil macOS den Akku + // schont —, ist das der ehrliche Befund. Einen falschen Grund zu + // nennen wäre schlimmer als keinen. + return .pluggedNotCharging + } +} diff --git a/Packages/OnyxKit/Sources/MetricsProvider/Localizable.xcstrings b/Packages/OnyxKit/Sources/MetricsProvider/Localizable.xcstrings index 9a20224..58370d7 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/Localizable.xcstrings +++ b/Packages/OnyxKit/Sources/MetricsProvider/Localizable.xcstrings @@ -1,1015 +1,1061 @@ { - "sourceLanguage" : "en", - "strings" : { - "" : { - - }, - "%lld" : { - "comment" : "A label displaying the speed of a fan. The argument is the speed of the fan, in revolutions per minute.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld" + "sourceLanguage": "en", + "strings": { + "": {}, + "%lld": { + "comment": "A label displaying the speed of a fan. The argument is the speed of the fan, in revolutions per minute.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld" } } } }, - "%lld U/min" : { - "comment" : "A label that shows the fan speed in revolutions per minute. The value is the fan speed in revolutions per minute.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld U/min" + "%lld U/min": { + "comment": "A label that shows the fan speed in revolutions per minute. The value is the fan speed in revolutions per minute.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld U/min" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld U/min" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld U/min" } } } }, - "battery.charging" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lädt" + "Lüfter %lld": { + "comment": "A label that shows the name of a fan. The argument is the index of the fan.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter %lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charging" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fan %lld" } } } }, - "battery.cycles" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zyklen" + "battery.charging": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lädt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cycles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Charging" } } } }, - "battery.draining" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbrauch" + "battery.cycles": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zyklen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Draw" + "en": { + "stringUnit": { + "state": "translated", + "value": "Cycles" } } } }, - "battery.health" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zustand" + "battery.draining": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbrauch" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Health" + "en": { + "stringUnit": { + "state": "translated", + "value": "Draw" } } } }, - "battery.lowPower" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Energiesparmodus" + "battery.health": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zustand" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Low Power Mode" + "en": { + "stringUnit": { + "state": "translated", + "value": "Health" } } } }, - "battery.pluggedIn" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Am Netz" + "battery.lowPower": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Energiesparmodus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Plugged in" + "en": { + "stringUnit": { + "state": "translated", + "value": "Low Power Mode" } } } }, - "battery.remaining %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "noch %@" + "battery.pluggedIn": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Am Netz" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ remaining" + "en": { + "stringUnit": { + "state": "translated", + "value": "Plugged in" } } } }, - "battery.untilFull %@" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ bis voll" + "battery.remaining %@": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "noch %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ until full" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ remaining" } } } }, - "charge.limit" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ladelimit" + "battery.untilFull %@": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ bis voll" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charge limit" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ until full" } } } }, - "charge.limit.hint" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "80 bis 100 % — der Bereich, den macOS selbst anbietet." + "charge.limit": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ladelimit" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "80 to 100 % — the range macOS itself offers." + "en": { + "stringUnit": { + "state": "translated", + "value": "Charge limit" } } } }, - "charge.limit.inSystemSettings" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "In den Systemeinstellungen einstellbar" + "charge.limit.hint": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "80 bis 100 % — der Bereich, den macOS selbst anbietet." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Set in System Settings" + "en": { + "stringUnit": { + "state": "translated", + "value": "80 to 100 % — the range macOS itself offers." } } } }, - "charge.limit.needsHelper" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Braucht den privilegierten Dienst aus Einstellungen → Lüfter." + "charge.limit.inSystemSettings": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "In den Systemeinstellungen einstellbar" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Needs the privileged service from Settings → Fans." + "en": { + "stringUnit": { + "state": "translated", + "value": "Set in System Settings" } } } }, - "charge.limit.off" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus" + "charge.limit.needsHelper": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Braucht den privilegierten Dienst aus Einstellungen → Lüfter." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Off" + "en": { + "stringUnit": { + "state": "translated", + "value": "Needs the privileged service from Settings → Fans." } } } }, - "Lüfter %lld" : { - "comment" : "A label that shows the name of a fan. The argument is the index of the fan.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter %lld" + "charge.limit.off": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fan %lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "Off" } } } }, - "menubar.short.battery" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akku" + "menubar.short.battery": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Akku" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Batt" + "en": { + "stringUnit": { + "state": "translated", + "value": "Batt" } } } }, - "menubar.short.temperature" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temp" + "menubar.short.temperature": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Temp" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temp" + "en": { + "stringUnit": { + "state": "translated", + "value": "Temp" } } } }, - "metric.battery" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akku" + "metric.battery": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Akku" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Battery" + "en": { + "stringUnit": { + "state": "translated", + "value": "Battery" } } } }, - "metric.battery.charging" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lädt" + "metric.battery.charging": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lädt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charging" + "en": { + "stringUnit": { + "state": "translated", + "value": "Charging" } } } }, - "metric.cpu" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "CPU" + "metric.cpu": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "CPU" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "CPU" + "en": { + "stringUnit": { + "state": "translated", + "value": "CPU" } } } }, - "metric.cpu.perCore" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Je Kern" + "metric.cpu.perCore": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Je Kern" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Per core" + "en": { + "stringUnit": { + "state": "translated", + "value": "Per core" } } } }, - "metric.fan %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter %lld" + "metric.fan %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter %lld" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fan %lld" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fan %lld" } } } }, - "metric.fan.controlComing" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Lüftersteuerung kommt mit dem privilegierten Helfer — Schreibzugriff auf den SMC verlangt root." + "metric.fan.controlComing": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Lüftersteuerung kommt mit dem privilegierten Helfer — Schreibzugriff auf den SMC verlangt root." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fan control arrives with the privileged helper — writing to the SMC requires root." + "en": { + "stringUnit": { + "state": "translated", + "value": "Fan control arrives with the privileged helper — writing to the SMC requires root." } } } }, - "metric.gpu" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPU" + "metric.gpu": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "GPU" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPU" + "en": { + "stringUnit": { + "state": "translated", + "value": "GPU" } } } }, - "metric.memory" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Speicher" + "metric.memory": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Speicher" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Memory" + "en": { + "stringUnit": { + "state": "translated", + "value": "Memory" } } } }, - "metric.mode.temperature" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperatur" + "metric.mode.temperature": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Temperatur" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperature" + "en": { + "stringUnit": { + "state": "translated", + "value": "Temperature" } } } }, - "metric.mode.usage" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auslastung" + "metric.mode.usage": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auslastung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usage" + "en": { + "stringUnit": { + "state": "translated", + "value": "Usage" } } } }, - "metric.rpm" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "U/min" + "metric.rpm": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "U/min" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "rpm" + "en": { + "stringUnit": { + "state": "translated", + "value": "rpm" } } } }, - "metric.sensors" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sensoren" + "metric.sensors": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sensoren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sensors" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sensors" } } } }, - "metric.sensors.none" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Sensoren lesbar" + "metric.sensors.none": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Sensoren lesbar" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No sensors readable" + "en": { + "stringUnit": { + "state": "translated", + "value": "No sensors readable" } } } }, - "metric.show" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anzeigen" + "metric.show": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anzeigen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show" + "en": { + "stringUnit": { + "state": "translated", + "value": "Show" } } } }, - "popover.battery.calculating" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wird berechnet …" + "popover.battery.calculating": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird berechnet …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calculating…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Calculating…" } } } }, - "popover.battery.charging" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lädt mit" + "popover.battery.charging": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lädt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charging at" + "en": { + "stringUnit": { + "state": "translated", + "value": "Charging" } } } }, - "popover.battery.condition" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zustand" + "popover.battery.condition": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zustand" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Condition" + "en": { + "stringUnit": { + "state": "translated", + "value": "Condition" } } } }, - "popover.battery.cycles" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ladezyklen" + "popover.battery.cycles": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ladezyklen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cycles" + "en": { + "stringUnit": { + "state": "translated", + "value": "Cycles" } } } }, - "popover.battery.draining" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbrauch" + "popover.battery.draining": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbrauch" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Draw" + "en": { + "stringUnit": { + "state": "translated", + "value": "Draw" } } } }, - "popover.battery.health" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kapazität" + "popover.battery.health": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kapazität" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Capacity" + "en": { + "stringUnit": { + "state": "translated", + "value": "Capacity" } } } }, - "popover.battery.limitComing" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Ladelimit kommt mit dem privilegierten Helfer. Der zuständige SMC-Key ist auf diesem Modell noch nicht identifiziert — siehe docs/spikes/A-smc.md." + "popover.battery.limitComing": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Ladelimit kommt mit dem privilegierten Helfer. Der zuständige SMC-Key ist auf diesem Modell noch nicht identifiziert — siehe docs/spikes/A-smc.md." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The charge limit arrives with the privileged helper. The relevant SMC key is not yet identified on this model — see docs/spikes/A-smc.md." + "en": { + "stringUnit": { + "state": "translated", + "value": "The charge limit arrives with the privileged helper. The relevant SMC key is not yet identified on this model — see docs/spikes/A-smc.md." } } } }, - "popover.battery.none" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein Akku gefunden" + "popover.battery.none": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Akku gefunden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No battery found" + "en": { + "stringUnit": { + "state": "translated", + "value": "No battery found" } } } }, - "popover.battery.pluggedIn" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Am Netzteil" + "popover.battery.notCharging": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Am Netz, lädt nicht" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "On adapter" + "en": { + "stringUnit": { + "state": "translated", + "value": "Plugged in, not charging" } } } }, - "popover.battery.remaining" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "verbleibend" + "popover.battery.onBattery": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Akkubetrieb" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "remaining" + "en": { + "stringUnit": { + "state": "translated", + "value": "Running on battery" } } } }, - "popover.battery.untilFull" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "bis voll" + "popover.battery.pausedAtLimit %lld": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Angehalten — Ladelimit von %lld %% erreicht" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "until full" + "en": { + "stringUnit": { + "state": "translated", + "value": "Paused — charge limit of %lld %% reached" } } } }, - "popover.battery.voltage" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Spannung" + "popover.battery.pluggedIn": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Am Netzteil" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Voltage" + "en": { + "stringUnit": { + "state": "translated", + "value": "On adapter" } } } }, - "popover.fans" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter" + "popover.battery.remaining": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "verbleibend" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fans" + "en": { + "stringUnit": { + "state": "translated", + "value": "remaining" } } } }, - "popover.memory.app" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Programme" + "popover.battery.untilFull": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "bis voll" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Apps" + "en": { + "stringUnit": { + "state": "translated", + "value": "until full" } } } }, - "popover.memory.compressed" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Komprimiert" + "popover.battery.voltage": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Spannung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Compressed" + "en": { + "stringUnit": { + "state": "translated", + "value": "Voltage" } } } }, - "popover.memory.free" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Frei" + "popover.fans": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Free" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fans" } } } }, - "popover.memory.swap" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auslagerung" + "popover.memory.app": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Programme" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Swap" + "en": { + "stringUnit": { + "state": "translated", + "value": "Apps" } } } }, - "popover.memory.swapUsed" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Benutzt" + "popover.memory.compressed": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Komprimiert" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Used" + "en": { + "stringUnit": { + "state": "translated", + "value": "Compressed" } } } }, - "popover.memory.used" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Belegt" + "popover.memory.free": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Frei" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Used" + "en": { + "stringUnit": { + "state": "translated", + "value": "Free" } } } }, - "popover.memory.wired" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Reserviert" + "popover.memory.swap": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auslagerung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wired" + "en": { + "stringUnit": { + "state": "translated", + "value": "Swap" } } } }, - "popover.perCore" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Je Kern" + "popover.memory.swapUsed": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Benutzt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Per core" + "en": { + "stringUnit": { + "state": "translated", + "value": "Used" } } } }, - "popover.power" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Leistung" + "popover.memory.used": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Belegt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Power" + "en": { + "stringUnit": { + "state": "translated", + "value": "Used" } } } }, - "popover.power.adapter" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Netzteil" + "popover.memory.wired": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Reserviert" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Adapter" + "en": { + "stringUnit": { + "state": "translated", + "value": "Wired" } } } }, - "popover.power.cpu" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "CPU" + "popover.perCore": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Je Kern" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "CPU" + "en": { + "stringUnit": { + "state": "translated", + "value": "Per core" } } } }, - "popover.power.display" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anzeige" + "popover.power": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Leistung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Display" + "en": { + "stringUnit": { + "state": "translated", + "value": "Power" } } } }, - "popover.power.total" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Gesamt" + "popover.power.adapter": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Netzteil" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Total" + "en": { + "stringUnit": { + "state": "translated", + "value": "Adapter" } } } }, - "popover.processes" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Größte Verbraucher" + "popover.power.cpu": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "CPU" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Top consumers" + "en": { + "stringUnit": { + "state": "translated", + "value": "CPU" } } } }, - "popover.temperature" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperatur" + "popover.power.display": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anzeige" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperature" + "en": { + "stringUnit": { + "state": "translated", + "value": "Display" } } } }, - "settings.menubar.labelAndValue" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kürzel + Wert" + "popover.power.total": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Gesamt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Label + value" + "en": { + "stringUnit": { + "state": "translated", + "value": "Total" } } } }, - "settings.menubar.symbolAndValue" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Symbol + Wert" + "popover.processes": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Größte Verbraucher" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Symbol + value" + "en": { + "stringUnit": { + "state": "translated", + "value": "Top consumers" + } + } + } + }, + "popover.temperature": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Temperatur" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Temperature" + } + } + } + }, + "settings.menubar.labelAndValue": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kürzel + Wert" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Label + value" + } + } + } + }, + "settings.menubar.symbolAndValue": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol + Wert" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Symbol + value" } } } } }, - "version" : "1.1" -} \ No newline at end of file + "version": "1.1" +} diff --git a/Packages/OnyxKit/Sources/MetricsProvider/MetricPopovers.swift b/Packages/OnyxKit/Sources/MetricsProvider/MetricPopovers.swift index 0257947..b8bc856 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/MetricPopovers.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/MetricPopovers.swift @@ -266,6 +266,10 @@ private struct BatteryDetail: View { } } + // Was gerade geschieht, in Worten. „Am Netz" allein beantwortet die + // Frage nicht, die man hat, wenn der Ladestand stehen bleibt. + activityLine(battery) + // Das Ladelimit steht hier und nicht in den Einstellungen: es ist // eine Akkusache, und man greift danach, während man auf den // Ladestand schaut. @@ -316,6 +320,47 @@ private struct BatteryDetail: View { } } + @ViewBuilder + private func activityLine(_ battery: BatteryReading) -> some View { + let activity = ChargeActivity.resolve(isCharging: battery.isCharging, + isPluggedIn: battery.isPluggedIn, + charge: battery.charge, + limit: control?.chargeLimit) + HStack(spacing: 6) { + Image(systemName: symbol(for: activity)) + .font(.caption) + .foregroundStyle(tint(for: activity)) + switch activity { + case .charging: + Text("popover.battery.charging", bundle: .module) + case .pausedAtLimit(let limit): + Text("popover.battery.pausedAtLimit \(limit)", bundle: .module) + case .pluggedNotCharging: + Text("popover.battery.notCharging", bundle: .module) + case .onBattery: + Text("popover.battery.onBattery", bundle: .module) + } + Spacer(minLength: 0) + } + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + } + + private func symbol(for activity: ChargeActivity) -> String { + switch activity { + case .charging: "bolt.fill" + case .pausedAtLimit: "pause.fill" + case .pluggedNotCharging: "powerplug.fill" + case .onBattery: "battery.50percent" + } + } + + private func tint(for activity: ChargeActivity) -> Color { + // Farbe nur, wo sie etwas meldet: Laden ist ein Zustand, den man + // sucht. Alles andere bleibt zurückhaltend. + activity == .charging ? .green : .secondary + } + private func duration(_ seconds: TimeInterval) -> String { let total = Int(seconds / 60) return total >= 60 ? "\(total / 60) h \(total % 60) min" : "\(total) min" diff --git a/Packages/OnyxKit/Tests/MetricsProviderTests/ChargeActivityTests.swift b/Packages/OnyxKit/Tests/MetricsProviderTests/ChargeActivityTests.swift new file mode 100644 index 0000000..2127b0d --- /dev/null +++ b/Packages/OnyxKit/Tests/MetricsProviderTests/ChargeActivityTests.swift @@ -0,0 +1,57 @@ +import Testing +@testable import MetricsProvider + +// „Am Netz" allein beantwortet die Frage nicht, die man hat, wenn der +// Ladestand stehen bleibt: lädt es gerade nicht, weil das Limit erreicht ist, +// oder stimmt etwas nicht? + +@Suite("Ladezustand in Worten") +struct ChargeActivityTests { + + @Test("Lädt schlägt alles andere") + func chargingWins() { + #expect(ChargeActivity.resolve(isCharging: true, isPluggedIn: true, + charge: 0.5, limit: 85) == .charging) + } + + @Test("Ohne Netzteil ist es Akkubetrieb") + func unpluggedIsOnBattery() { + #expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: false, + charge: 0.5, limit: 85) == .onBattery) + } + + @Test("Am Limit steht der Grund dabei") + func atLimitIsExplained() { + #expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true, + charge: 0.85, limit: 85) == .pausedAtLimit(85)) + } + + @Test("Knapp unter dem Limit zählt auch") + func justBelowTheLimitCounts() { + // Der Ladestand pendelt um die Grenze; auf das Prozent genau zu + // vergleichen hieße, den Grund die halbe Zeit zu verschweigen. + #expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true, + charge: 0.84, limit: 85) == .pausedAtLimit(85)) + } + + @Test("Weit unter dem Limit ist es kein Limit-Halt") + func wellBelowIsNotTheLimit() { + // Dann steht das Laden aus einem anderen Grund — etwa weil macOS den + // Akku schont. Einen falschen Grund zu nennen wäre schlimmer als + // keinen. + #expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true, + charge: 0.40, limit: 85) == .pluggedNotCharging) + } + + @Test("Ohne bekanntes Limit bleibt es beim schlichten Befund") + func withoutLimitStaysPlain() { + #expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true, + charge: 0.99, limit: nil) == .pluggedNotCharging) + } + + @Test("Ein Limit von hundert ist kein Limit") + func hundredIsNoLimit() { + #expect(ChargeActivity.resolve(isCharging: false, isPluggedIn: true, + charge: 1.0, limit: 100) == .pluggedNotCharging) + } +}