diff --git a/Onyx/FanControl.swift b/Onyx/FanControl.swift index c64472c..1532209 100644 --- a/Onyx/FanControl.swift +++ b/Onyx/FanControl.swift @@ -13,7 +13,7 @@ private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Fa /// selbst misst. Diese Klasse hält nur die Verbindung und den Herzschlag. @MainActor @Observable -public final class FanControl: ChargeLimitControlling { +public final class FanControl: ChargeLimitReading { public enum InstallState: Equatable { case notInstalled @@ -178,13 +178,7 @@ public final class FanControl: ChargeLimitControlling { /// Setzt das Limit und übernimmt, was der Helfer **zurückgelesen** hat — /// nicht das, was gewünscht war. Ein Schreibvorgang, den der SMC verwirft, /// darf sich in der Oberfläche nicht als Erfolg zeigen. - /// Ohne laufenden Helfer lässt sich nichts schreiben — der Regler wird - /// dann angezeigt, aber gesperrt, statt still zu versagen. - public var canControlCharge: Bool { installState == .installed } - - public func setChargeLimit(_ percent: Int) { - proxy()?.setChargeLimit(percent: percent) { [weak self] result in - Task { @MainActor in self?.chargeLimit = result > 0 ? result : nil } - } - } + /// Ohne laufenden Helfer kommt kein Wert — dann steht ein Strich da statt + /// einer Zahl, die es nicht gibt. + public var canReadCharge: Bool { installState == .installed } } diff --git a/Onyx/FanWidget.swift b/Onyx/FanWidget.swift index 32c5b4f..94599a5 100644 --- a/Onyx/FanWidget.swift +++ b/Onyx/FanWidget.swift @@ -167,6 +167,8 @@ final class FanMenuBarModule: MenuBarModule { let id = "fans" var displayName: String { String(localized: "widget.fans.name") } + var shortLabel: String { String(localized: "fans.menubar.short") } + private let control: FanControl private var view: FanStatusView? private var isActive = false @@ -234,6 +236,9 @@ private final class FanStatusView: NSView, WidthReporting { /// änderte das Element bei jeder Messung seine Breite. private var reference: String { presentation == .symbol ? "" : "9999" } + /// Der Lüfter zeigt ohnehin Symbol und Zahl — die neuen Darstellungen + /// ändern daran nichts, sie stehen nur in der Auswahl nicht zur Verfügung. + var onWidthChange: ((CGFloat) -> Void)? func update(rpm: Double?) { diff --git a/Onyx/Localizable.xcstrings b/Onyx/Localizable.xcstrings index 6d38b29..4c4274e 100644 --- a/Onyx/Localizable.xcstrings +++ b/Onyx/Localizable.xcstrings @@ -1,1890 +1,1957 @@ { - "sourceLanguage" : "en", - "strings" : { - "%@" : { - "comment" : "A label showing the minimum RPM of a fan.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@" + "sourceLanguage": "en", + "strings": { + "%@": { + "comment": "A label showing the minimum RPM of a fan.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@" } } } }, - "%@ °C" : { - "comment" : "A label showing the temperature of the hottest fan in the report, in degrees Celsius. The argument is the temperature in degrees Celsius.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ °C" + "%@ U/min": { + "comment": "A label that shows the current speed of a fan. The argument is the current speed of the fan.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ U/min" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ °C" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ U/min" } } } }, - "%@ U/min" : { - "comment" : "A label that shows the current speed of a fan. The argument is the current speed of the fan.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ U/min" + "%@ °C": { + "comment": "A label showing the temperature of the hottest fan in the report, in degrees Celsius. The argument is the temperature in degrees Celsius.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ °C" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ U/min" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ °C" } } } }, - "%@°" : { - "comment" : "A temperature in °C.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@°" + "%@°": { + "comment": "A temperature in °C.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@°" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@°" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@°" } } } }, - "charge.limit" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ladelimit" + "charge.limit": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ladelimit" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Charge limit" + "en": { + "stringUnit": { + "state": "translated", + "value": "Charge limit" } } } }, - "charge.limit.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ein Akku, der dauerhaft bei 100 % am Netz hängt, altert schneller. 80 bis 100 % ist der Bereich, den macOS selbst anbietet — darunter ist für dieses Gerät ungemessen." + "charge.limit.hint": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Akku, der dauerhaft bei 100 % am Netz hängt, altert schneller. 80 bis 100 % ist der Bereich, den macOS selbst anbietet — darunter ist für dieses Gerät ungemessen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A battery kept at 100 % on power ages faster. 80 to 100 % is the range macOS itself offers — below that is untested for this machine." + "en": { + "stringUnit": { + "state": "translated", + "value": "A battery kept at 100 % on power ages faster. 80 to 100 % is the range macOS itself offers — below that is untested for this machine." } } } }, - "charge.limit.off" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus" + "charge.limit.off": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Off" + "en": { + "stringUnit": { + "state": "translated", + "value": "Off" } } } }, - "charge.section" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akku" + "charge.section": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Akku" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Battery" + "en": { + "stringUnit": { + "state": "translated", + "value": "Battery" } } } }, - "fans.approval.needed" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS wartet auf deine Freigabe. Öffne die Systemeinstellungen und erlaube den Hintergrunddienst von Onyx." + "fans.approval.needed": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "macOS wartet auf deine Freigabe. Öffne die Systemeinstellungen und erlaube den Hintergrunddienst von Onyx." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "macOS is waiting for your approval. Open System Settings and allow Onyx's background service." + "en": { + "stringUnit": { + "state": "translated", + "value": "macOS is waiting for your approval. Open System Settings and allow Onyx's background service." } } } }, - "fans.approval.open" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Systemeinstellungen öffnen" + "fans.approval.open": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Systemeinstellungen öffnen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open System Settings" + "en": { + "stringUnit": { + "state": "translated", + "value": "Open System Settings" } } } }, - "fans.approval.recheck" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erneut prüfen" + "fans.approval.recheck": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut prüfen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Check again" + "en": { + "stringUnit": { + "state": "translated", + "value": "Check again" } } } }, - "fans.auto" : { - "comment" : "A button to set a fan to automatic mode.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto" + "fans.auto": { + "comment": "A button to set a fan to automatic mode.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auto" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auto" + "en": { + "stringUnit": { + "state": "translated", + "value": "Auto" } } } }, - "fans.connecting" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbinde mit dem Hilfsdienst …" + "fans.connecting": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbinde mit dem Hilfsdienst …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connecting to the helper…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting to the helper…" } } } }, - "fans.fan %@" : { - "comment" : "A label showing the index of a fan. The argument is the index of the fan.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter %@" + "fans.fan %@": { + "comment": "A label showing the index of a fan. The argument is the index of the fan.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter %@" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fan %@" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fan %@" } } } }, - "fans.fan %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter %lld" + "fans.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" } } } }, - "fans.hottest" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wärmster Kern" + "fans.hottest": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wärmster Kern" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hottest core" + "en": { + "stringUnit": { + "state": "translated", + "value": "Hottest core" } } } }, - "fans.install" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Installieren" + "fans.install": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Installieren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Install" + "en": { + "stringUnit": { + "state": "translated", + "value": "Install" } } } }, - "fans.intro" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx kann die Lüfterdrehzahl fest vorgeben, statt sie macOS zu überlassen. Dafür wird ein Hilfsdienst mit Systemrechten eingerichtet — Schreibzugriff auf den SMC geht nicht anders." + "fans.intro": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Onyx kann die Lüfterdrehzahl fest vorgeben, statt sie macOS zu überlassen. Dafür wird ein Hilfsdienst mit Systemrechten eingerichtet — Schreibzugriff auf den SMC geht nicht anders." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx can set a fixed fan speed instead of leaving it to macOS. This requires a helper service with system privileges — writing to the SMC is not possible otherwise." + "en": { + "stringUnit": { + "state": "translated", + "value": "Onyx can set a fixed fan speed instead of leaving it to macOS. This requires a helper service with system privileges — writing to the SMC is not possible otherwise." } } } }, - "fans.left" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter links" + "fans.left": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter links" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Left fan" + "en": { + "stringUnit": { + "state": "translated", + "value": "Left fan" } } } }, - "fans.manual" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuell" + "fans.manual": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Manuell" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manual" + "en": { + "stringUnit": { + "state": "translated", + "value": "Manual" } } } }, - "fans.mode.auto" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatik" + "fans.menubar.short": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatic" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fan" } } } }, - "fans.mode.manual" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manuell" + "fans.mode.auto": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Automatik" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Manual" + "en": { + "stringUnit": { + "state": "translated", + "value": "Automatic" } } } }, - "fans.noData" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Noch keine Lüfterdaten." + "fans.mode.manual": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Manuell" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No fan data yet." + "en": { + "stringUnit": { + "state": "translated", + "value": "Manual" } } } }, - "fans.noRange" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kein Regelbereich" + "fans.noData": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch keine Lüfterdaten." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No adjustable range" + "en": { + "stringUnit": { + "state": "translated", + "value": "No fan data yet." } } } }, - "fans.notInstalled" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Lüftersteuerung braucht einen kleinen privilegierten Dienst." + "fans.noRange": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Regelbereich" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fan control needs a small privileged service." + "en": { + "stringUnit": { + "state": "translated", + "value": "No adjustable range" } } } }, - "fans.reason.noTemperature" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurück auf Automatik: keine Temperatur messbar, damit lässt sich Sicherheit nicht beurteilen." + "fans.notInstalled": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Lüftersteuerung braucht einen kleinen privilegierten Dienst." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back to automatic: no temperature reading, so safety cannot be judged." + "en": { + "stringUnit": { + "state": "translated", + "value": "Fan control needs a small privileged service." } } } }, - "fans.reason.other" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurück auf Automatik." + "fans.reason.noTemperature": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück auf Automatik: keine Temperatur messbar, damit lässt sich Sicherheit nicht beurteilen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back to automatic." + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to automatic: no temperature reading, so safety cannot be judged." } } } }, - "fans.reason.temperature" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurück auf Automatik: zu heiß für eine feste Drehzahl." + "fans.reason.other": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück auf Automatik." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back to automatic: too hot for a fixed speed." + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to automatic." } } } }, - "fans.reason.unknownLimits" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurück auf Automatik: der erlaubte Drehzahlbereich ist nicht lesbar." + "fans.reason.temperature": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück auf Automatik: zu heiß für eine feste Drehzahl." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back to automatic: the permitted speed range cannot be read." + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to automatic: too hot for a fixed speed." } } } }, - "fans.reason.watchdog" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zurück auf Automatik: Onyx hat sich zu lange nicht gemeldet." + "fans.reason.unknownLimits": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück auf Automatik: der erlaubte Drehzahlbereich ist nicht lesbar." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Back to automatic: Onyx went silent for too long." + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to automatic: the permitted speed range cannot be read." } } } }, - "fans.right" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter rechts" + "fans.reason.watchdog": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück auf Automatik: Onyx hat sich zu lange nicht gemeldet." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Right fan" + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to automatic: Onyx went silent for too long." } } } }, - "fans.safety" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Deshalb greift ein Sicherheitsnetz, das sich nicht abschalten lässt: nie unter das Minimum der Firmware, Rückfall auf Automatik ab 95 °C, Rückfall wenn Onyx sich fünf Sekunden nicht meldet, und Rückfall beim Beenden." + "fans.right": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter rechts" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A safety net you cannot switch off is therefore always active: never below the firmware minimum, back to automatic above 95 °C, back to automatic if Onyx goes silent for five seconds, and back to automatic on quit." + "en": { + "stringUnit": { + "state": "translated", + "value": "Right fan" } } } }, - "fans.safety.active" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Das Sicherheitsnetz läuft im Hilfsdienst, nicht in Onyx. Es greift auch dann, wenn Onyx abstürzt oder beendet wird." + "fans.safety": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Deshalb greift ein Sicherheitsnetz, das sich nicht abschalten lässt: nie unter das Minimum der Firmware, Rückfall auf Automatik ab 95 °C, Rückfall wenn Onyx sich fünf Sekunden nicht meldet, und Rückfall beim Beenden." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The safety net runs in the helper, not in Onyx. It works even if Onyx crashes or is quit." + "en": { + "stringUnit": { + "state": "translated", + "value": "A safety net you cannot switch off is therefore always active: never below the firmware minimum, back to automatic above 95 °C, back to automatic if Onyx goes silent for five seconds, and back to automatic on quit." } } } }, - "fans.uninstall" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hilfsdienst entfernen" + "fans.safety.active": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Sicherheitsnetz läuft im Hilfsdienst, nicht in Onyx. Es greift auch dann, wenn Onyx abstürzt oder beendet wird." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove helper" + "en": { + "stringUnit": { + "state": "translated", + "value": "The safety net runs in the helper, not in Onyx. It works even if Onyx crashes or is quit." } } } }, - "fans.warning" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eine zu niedrige Drehzahl kann den Rechner überhitzen lassen. Die Automatik regelt nach Temperatur, ein fester Wert nicht." + "fans.uninstall": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Hilfsdienst entfernen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "A fan speed set too low can let the Mac overheat. The automatic control adjusts to temperature; a fixed value does not." + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove helper" } } } }, - "menu.openPanel" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Panel öffnen" + "fans.warning": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Eine zu niedrige Drehzahl kann den Rechner überhitzen lassen. Die Automatik regelt nach Temperatur, ein fester Wert nicht." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open Panel" + "en": { + "stringUnit": { + "state": "translated", + "value": "A fan speed set too low can let the Mac overheat. The automatic control adjusts to temperature; a fixed value does not." } } } }, - "menu.quit" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx beenden" + "menu.openPanel": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Panel öffnen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quit Onyx" + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Panel" } } } }, - "menu.settings" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Einstellungen …" + "menu.quit": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Onyx beenden" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Settings…" + "en": { + "stringUnit": { + "state": "translated", + "value": "Quit Onyx" } } } }, - "metric.battery" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Akku" + "menu.settings": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen …" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Battery" + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings…" } } } }, - "metric.cpu" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "CPU" + "metric.battery": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Akku" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "CPU" + "en": { + "stringUnit": { + "state": "translated", + "value": "Battery" } } } }, - "metric.gpu" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPU" + "metric.cpu": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "CPU" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "GPU" + "en": { + "stringUnit": { + "state": "translated", + "value": "CPU" } } } }, - "metric.memory" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Speicher" + "metric.gpu": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "GPU" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Memory" + "en": { + "stringUnit": { + "state": "translated", + "value": "GPU" } } } }, - "metric.mode.temperature" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperatur" + "metric.memory": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Speicher" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Temperature" + "en": { + "stringUnit": { + "state": "translated", + "value": "Memory" } } } }, - "metric.mode.usage" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auslastung" + "metric.mode.temperature": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Temperatur" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Usage" + "en": { + "stringUnit": { + "state": "translated", + "value": "Temperature" } } } }, - "metric.sensors" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sensoren" + "metric.mode.usage": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auslastung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sensors" + "en": { + "stringUnit": { + "state": "translated", + "value": "Usage" } } } }, - "mixer.menubar.name" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lautstärkemixer" + "metric.sensors": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Sensoren" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Volume mixer" + "en": { + "stringUnit": { + "state": "translated", + "value": "Sensors" } } } }, - "network.name" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Netzwerk" + "mixer.menubar.name": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lautstärkemixer" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Network" + "en": { + "stringUnit": { + "state": "translated", + "value": "Volume mixer" } } } }, - "onboarding.allow" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erlauben" + "mixer.menubar.short": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mix" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Allow" + "en": { + "stringUnit": { + "state": "translated", + "value": "Mix" } } } }, - "onboarding.audio" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Systemton" + "network.name": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Netzwerk" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "System audio" + "en": { + "stringUnit": { + "state": "translated", + "value": "Network" } } } }, - "onboarding.audio.why" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nötig für die Lautstärke einzelner Programme. Onyx leitet den Ton der geregelten Programme um — und nur deren." + "onboarding.allow": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Erlauben" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Needed for per-app volume. Onyx routes the audio of the apps you control, and only those." + "en": { + "stringUnit": { + "state": "translated", + "value": "Allow" } } } }, - "onboarding.calendar" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kalender" + "onboarding.audio": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Systemton" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar" + "en": { + "stringUnit": { + "state": "translated", + "value": "System audio" } } } }, - "onboarding.calendar.why" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zeigt deine Termine im Panel. Nur lesend — Onyx ändert nie einen Kalender." + "onboarding.audio.why": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nötig für die Lautstärke einzelner Programme. Onyx leitet den Ton der geregelten Programme um — und nur deren." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shows your appointments in the panel. Read-only — Onyx never changes a calendar." + "en": { + "stringUnit": { + "state": "translated", + "value": "Needed for per-app volume. Onyx routes the audio of the apps you control, and only those." } } } }, - "onboarding.done" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fertig" + "onboarding.calendar": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kalender" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Done" + "en": { + "stringUnit": { + "state": "translated", + "value": "Calendar" } } } }, - "onboarding.helper" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüftersteuerung" + "onboarding.calendar.why": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt deine Termine im Panel. Nur lesend — Onyx ändert nie einen Kalender." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fan control" + "en": { + "stringUnit": { + "state": "translated", + "value": "Shows your appointments in the panel. Read-only — Onyx never changes a calendar." } } } }, - "onboarding.helper.why" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Installiert einen kleinen privilegierten Dienst. Er setzt nur Lüfterdrehzahlen und fällt auf Automatik zurück, wenn Onyx schweigt." + "onboarding.done": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Fertig" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Installs a small privileged service. It only sets fan speeds and falls back to automatic if Onyx goes silent." + "en": { + "stringUnit": { + "state": "translated", + "value": "Done" } } } }, - "onboarding.launchAtLogin" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx beim Anmelden starten" + "onboarding.helper": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüftersteuerung" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start Onyx at login" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fan control" } } } }, - "onboarding.location" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standort" + "onboarding.helper.why": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Installiert einen kleinen privilegierten Dienst. Er setzt nur Lüfterdrehzahlen und fällt auf Automatik zurück, wenn Onyx schweigt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Location" + "en": { + "stringUnit": { + "state": "translated", + "value": "Installs a small privileged service. It only sets fan speeds and falls back to automatic if Onyx goes silent." } } } }, - "onboarding.location.why" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wetter für deinen Aufenthaltsort. Alternativ lässt sich je Widget ein fester Ort einstellen." + "onboarding.launchAtLogin": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Onyx beim Anmelden starten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Weather for where you are. You can also set a fixed place per widget instead." + "en": { + "stringUnit": { + "state": "translated", + "value": "Start Onyx at login" } } } }, - "onboarding.openSettings" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Systemeinstellungen öffnen" + "onboarding.location": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Standort" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open System Settings" + "en": { + "stringUnit": { + "state": "translated", + "value": "Location" } } } }, - "onboarding.subtitle" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx braucht vier Dinge vom System. Jedes für etwas anderes, und keines davon ist Pflicht — erlaube, was du nutzen möchtest." + "onboarding.location.why": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wetter für deinen Aufenthaltsort. Alternativ lässt sich je Widget ein fester Ort einstellen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx needs four things from the system. Each one is for something different, and none of them is required — grant what you want to use." + "en": { + "stringUnit": { + "state": "translated", + "value": "Weather for where you are. You can also set a fixed place per widget instead." } } } }, - "onboarding.title" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Willkommen bei Onyx" + "onboarding.openSettings": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Systemeinstellungen öffnen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Welcome to Onyx" + "en": { + "stringUnit": { + "state": "translated", + "value": "Open System Settings" } } } }, - "settings.display.all" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Allen Displays" + "onboarding.subtitle": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Onyx braucht vier Dinge vom System. Jedes für etwas anderes, und keines davon ist Pflicht — erlaube, was du nutzen möchtest." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "All displays" + "en": { + "stringUnit": { + "state": "translated", + "value": "Onyx needs four things from the system. Each one is for something different, and none of them is required — grant what you want to use." } } } }, - "settings.display.builtInOnly" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nur dem internen Display" + "onboarding.title": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Willkommen bei Onyx" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Built-in display only" + "en": { + "stringUnit": { + "state": "translated", + "value": "Welcome to Onyx" } } } }, - "settings.display.dockIcon" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Symbol im Dock anzeigen" + "settings.display.all": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Allen Displays" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show icon in the Dock" + "en": { + "stringUnit": { + "state": "translated", + "value": "All displays" } } } }, - "settings.display.dockIcon.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standardmäßig aus. Onyx lebt in der Notch und in der Menüleiste und hat kein Hauptfenster — im Dock wäre es ein Symbol, das beim Anklicken nichts öffnet, und es stünde im Programmumschalter zwischen den Programmen, mit denen du arbeitest." + "settings.display.builtInOnly": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur dem internen Display" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Off by default. Onyx lives in the notch and the menu bar and has no main window — in the Dock it would be an icon that opens nothing, and it would sit in the app switcher among the apps you actually work with." + "en": { + "stringUnit": { + "state": "translated", + "value": "Built-in display only" } } } }, - "settings.display.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Auf Displays ohne echte Notch erscheint oben mittig ein Balken, der sich genauso verhält. Ist der Deckel geschlossen und kein internes Display vorhanden, erscheint das Panel trotzdem auf einem externen — sonst wäre Onyx nicht erreichbar." + "settings.display.dockIcon": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol im Dock anzeigen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "On displays without a real notch, a bar appears at the top centre and behaves the same. With the lid closed and no built-in display present, the panel still appears on an external one — otherwise Onyx would be unreachable." + "en": { + "stringUnit": { + "state": "translated", + "value": "Show icon in the Dock" } } } }, - "settings.display.menuBarIcon" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Symbol in der Menüleiste anzeigen" + "settings.display.dockIcon.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Standardmäßig aus. Onyx lebt in der Notch und in der Menüleiste und hat kein Hauptfenster — im Dock wäre es ein Symbol, das beim Anklicken nichts öffnet, und es stünde im Programmumschalter zwischen den Programmen, mit denen du arbeitest." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show icon in the menu bar" + "en": { + "stringUnit": { + "state": "translated", + "value": "Off by default. Onyx lives in the notch and the menu bar and has no main window — in the Dock it would be an icon that opens nothing, and it would sit in the app switcher among the apps you actually work with." } } } }, - "settings.display.menuBarIcon.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Über das Symbol erreichst du Panel, Einstellungen und Beenden. Ohne es kommst du nur über die Notch an Onyx heran." + "settings.display.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf Displays ohne echte Notch erscheint oben mittig ein Balken, der sich genauso verhält. Ist der Deckel geschlossen und kein internes Display vorhanden, erscheint das Panel trotzdem auf einem externen — sonst wäre Onyx nicht erreichbar." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The icon gives you the panel, settings and quit. Without it, the notch is the only way to reach Onyx." + "en": { + "stringUnit": { + "state": "translated", + "value": "On displays without a real notch, a bar appears at the top centre and behaves the same. With the lid closed and no built-in display present, the panel still appears on an external one — otherwise Onyx would be unreachable." } } } }, - "settings.display.policy" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Panel anzeigen auf" + "settings.display.menuBarIcon": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol in der Menüleiste anzeigen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show panel on" + "en": { + "stringUnit": { + "state": "translated", + "value": "Show icon in the menu bar" } } } }, - "settings.general" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Allgemein" + "settings.display.menuBarIcon.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Über das Symbol erreichst du Panel, Einstellungen und Beenden. Ohne es kommst du nur über die Notch an Onyx heran." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "General" + "en": { + "stringUnit": { + "state": "translated", + "value": "The icon gives you the panel, settings and quit. Without it, the notch is the only way to reach Onyx." } } } }, - "settings.launchAtLogin" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Beim Anmelden starten" + "settings.display.policy": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Panel anzeigen auf" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Start at login" + "en": { + "stringUnit": { + "state": "translated", + "value": "Show panel on" } } } }, - "settings.launchAtLogin.blocked" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anmeldeobjekte wurden für Onyx in den Systemeinstellungen abgeschaltet." + "settings.general": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Allgemein" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Login items were turned off for Onyx in System Settings." + "en": { + "stringUnit": { + "state": "translated", + "value": "General" } } } }, - "settings.menubar.bars" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Balken" + "settings.launchAtLogin": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Beim Anmelden starten" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Bars" + "en": { + "stringUnit": { + "state": "translated", + "value": "Start at login" } } } }, - "settings.menubar.graph" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verlauf" + "settings.launchAtLogin.blocked": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anmeldeobjekte wurden für Onyx in den Systemeinstellungen abgeschaltet." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Graph" + "en": { + "stringUnit": { + "state": "translated", + "value": "Login items were turned off for Onyx in System Settings." } } } }, - "settings.menubar.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Jedes Modul erscheint als eigenes Symbol in der Menüleiste und teilt sich die Messung mit dem Widget im Panel. Standardmäßig ist keines an — die Menüleiste ist knapp, auf einem Notch-Display zusätzlich in der Mitte blockiert." + "settings.menubar.bars": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Balken" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Each module appears as its own menu bar icon and shares its measurement with the panel widget. None are on by default — the menu bar is tight, and on a notched display the middle is blocked as well." + "en": { + "stringUnit": { + "state": "translated", + "value": "Bars" } } } }, - "settings.menubar.symbol" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nur Symbol" + "settings.menubar.graph": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verlauf" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Symbol only" + "en": { + "stringUnit": { + "state": "translated", + "value": "Graph" } } } }, - "settings.menubar.value" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wert" + "settings.menubar.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Jedes Modul erscheint als eigenes Symbol in der Menüleiste und teilt sich die Messung mit dem Widget im Panel. Standardmäßig ist keines an — die Menüleiste ist knapp, auf einem Notch-Display zusätzlich in der Mitte blockiert." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Value" + "en": { + "stringUnit": { + "state": "translated", + "value": "Each module appears as its own menu bar icon and shares its measurement with the panel widget. None are on by default — the menu bar is tight, and on a notched display the middle is blocked as well." } } } }, - "settings.menubar.valueAndGraph" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wert und Verlauf" + "settings.menubar.labelAndValue": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kürzel + Wert" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Value and graph" + "en": { + "stringUnit": { + "state": "translated", + "value": "Label + value" } } } }, - "settings.onboarding.show" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Einrichtung erneut zeigen" + "settings.menubar.symbol": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Symbol" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show setup again" + "en": { + "stringUnit": { + "state": "translated", + "value": "Symbol only" } } } }, - "settings.permissions.calendar" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kalender" + "settings.menubar.symbolAndValue": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol + Wert" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Calendar" + "en": { + "stringUnit": { + "state": "translated", + "value": "Symbol + value" } } } }, - "settings.permissions.calendar.why" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben." + "settings.menubar.value": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wert" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx shows your events in the calendar widget. It only ever reads, never writes." + "en": { + "stringUnit": { + "state": "translated", + "value": "Value" } } } }, - "settings.permissions.granted" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Erteilt" + "settings.menubar.valueAndGraph": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Wert und Verlauf" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Granted" + "en": { + "stringUnit": { + "state": "translated", + "value": "Value and graph" } } } }, - "settings.permissions.location" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Standort" + "settings.onboarding.show": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Einrichtung erneut zeigen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Location" + "en": { + "stringUnit": { + "state": "translated", + "value": "Show setup again" } } } }, - "settings.permissions.location.why" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx zeigt das Wetter an deinem aktuellen Standort. Ohne Zugriff kannst du stattdessen einen festen Ort wählen." + "settings.permissions.calendar": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kalender" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Onyx shows the weather for your current location. Without access you can pick a fixed place instead." + "en": { + "stringUnit": { + "state": "translated", + "value": "Calendar" } } } }, - "settings.permissions.openSystem" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Systemeinstellungen öffnen" + "settings.permissions.calendar.why": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Open System Settings" + "en": { + "stringUnit": { + "state": "translated", + "value": "Onyx shows your events in the calendar widget. It only ever reads, never writes." } } } }, - "settings.permissions.request" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anfragen" + "settings.permissions.granted": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Erteilt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Request" + "en": { + "stringUnit": { + "state": "translated", + "value": "Granted" } } } }, - "settings.shelf.cleanup" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Automatisch löschen" + "settings.permissions.location": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Standort" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Delete automatically" + "en": { + "stringUnit": { + "state": "translated", + "value": "Location" } } } }, - "settings.shelf.count %@" : { - "comment" : "A label showing the number of items in the shelf. The argument is the number of items in the shelf.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ Dateien abgelegt" + "settings.permissions.location.why": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Onyx zeigt das Wetter an deinem aktuellen Standort. Ohne Zugriff kannst du stattdessen einen festen Ort wählen." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%@ files parked" + "en": { + "stringUnit": { + "state": "translated", + "value": "Onyx shows the weather for your current location. Without access you can pick a fixed place instead." } } } }, - "settings.shelf.count %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld Dateien abgelegt" + "settings.permissions.openSystem": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Systemeinstellungen öffnen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "%lld files parked" + "en": { + "stringUnit": { + "state": "translated", + "value": "Open System Settings" } } } }, - "settings.shelf.days %@" : { - "comment" : "A label that lets the user select how many days to wait before automatically removing items from the shelf. The argument is the number of days.", - "isCommentAutoGenerated" : true, - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nach %@ Tagen" + "settings.permissions.request": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anfragen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "After %@ days" + "en": { + "stringUnit": { + "state": "translated", + "value": "Request" } } } }, - "settings.shelf.days %lld" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nach %lld Tagen" + "settings.shelf.cleanup": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Automatisch löschen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "After %lld days" + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete automatically" } } } }, - "settings.shelf.never" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Nie" + "settings.shelf.count %@": { + "comment": "A label showing the number of items in the shelf. The argument is the number of items in the shelf.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Dateien abgelegt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Never" + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ files parked" } } } }, - "settings.shelf.reveal" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Im Finder zeigen" + "settings.shelf.count %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%lld Dateien abgelegt" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Show in Finder" + "en": { + "stringUnit": { + "state": "translated", + "value": "%lld files parked" } } } }, - "settings.shelf.title" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ablage" + "settings.shelf.days %@": { + "comment": "A label that lets the user select how many days to wait before automatically removing items from the shelf. The argument is the number of days.", + "isCommentAutoGenerated": true, + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach %@ Tagen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Shelf" + "en": { + "stringUnit": { + "state": "translated", + "value": "After %@ days" } } } }, - "settings.size.large" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Groß (2×2)" + "settings.shelf.days %lld": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach %lld Tagen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Large (2×2)" + "en": { + "stringUnit": { + "state": "translated", + "value": "After %lld days" } } } }, - "settings.size.medium" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mittel (2×1)" + "settings.shelf.never": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Nie" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Medium (2×1)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Never" } } } }, - "settings.size.small" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Klein (1×1)" + "settings.shelf.reveal": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Finder zeigen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Small (1×1)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Show in Finder" } } } }, - "settings.size.wide" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Breit (4×1)" + "settings.shelf.title": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Ablage" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Wide (4×1)" + "en": { + "stringUnit": { + "state": "translated", + "value": "Shelf" } } } }, - "settings.tab.display" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Anzeige" + "settings.size.large": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Groß (2×2)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Display" + "en": { + "stringUnit": { + "state": "translated", + "value": "Large (2×2)" } } } }, - "settings.tab.fans" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter" + "settings.size.medium": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Mittel (2×1)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fans" + "en": { + "stringUnit": { + "state": "translated", + "value": "Medium (2×1)" } } } }, - "settings.tab.menubar" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menüleiste" + "settings.size.small": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Klein (1×1)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Menu bar" + "en": { + "stringUnit": { + "state": "translated", + "value": "Small (1×1)" } } } }, - "settings.tab.permissions" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Berechtigungen" + "settings.size.wide": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Breit (4×1)" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Permissions" + "en": { + "stringUnit": { + "state": "translated", + "value": "Wide (4×1)" } } } }, - "settings.tab.sources" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quellen" + "settings.tab.display": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Anzeige" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Sources" + "en": { + "stringUnit": { + "state": "translated", + "value": "Display" } } } }, - "settings.tab.widgets" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Widgets" + "settings.tab.fans": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Widgets" + "en": { + "stringUnit": { + "state": "translated", + "value": "Fans" } } } }, - "settings.widgets.add" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hinzufügen" + "settings.tab.menubar": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Menüleiste" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Add" + "en": { + "stringUnit": { + "state": "translated", + "value": "Menu bar" } } } }, - "settings.widgets.available" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verfügbar" + "settings.tab.permissions": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Berechtigungen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Available" + "en": { + "stringUnit": { + "state": "translated", + "value": "Permissions" } } } }, - "settings.widgets.empty" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Keine Widgets im Panel. Das Panel bleibt leer, bis du eins hinzufügst." + "settings.tab.sources": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "No widgets in the panel. It stays empty until you add one." + "en": { + "stringUnit": { + "state": "translated", + "value": "Sources" } } } }, - "settings.widgets.hint" : { - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Die Reihenfolge bestimmt die Anordnung im Panel. Jedes Widget rückt an die erste Stelle, an die es passt." + "settings.tab.widgets": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Widgets" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "The order determines the arrangement in the panel. Each widget takes the first spot it fits into." + "en": { + "stringUnit": { + "state": "translated", + "value": "Widgets" } } } }, - "settings.widgets.hint.order" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Alle Kacheln sind gleich groß. Die Reihenfolge hier ist die Reihenfolge im Panel — zum Sortieren ziehen." + "settings.widgets.add": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Hinzufügen" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "All tiles are the same size. The order here is the order in the panel — drag to sort." + "en": { + "stringUnit": { + "state": "translated", + "value": "Add" } } } }, - "settings.widgets.inPanel" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Im Panel" + "settings.widgets.available": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verfügbar" } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "In the panel" + "en": { + "stringUnit": { + "state": "translated", + "value": "Available" } } } }, - "settings.widgets.remove" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Aus dem Panel entfernen" + "settings.widgets.empty": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Widgets im Panel. Das Panel bleibt leer, bis du eins hinzufügst." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Remove from panel" + "en": { + "stringUnit": { + "state": "translated", + "value": "No widgets in the panel. It stays empty until you add one." } } } }, - "widget.fans.name" : { - "extractionState" : "stale", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Lüfter" + "settings.widgets.hint": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Reihenfolge bestimmt die Anordnung im Panel. Jedes Widget rückt an die erste Stelle, an die es passt." } }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fans" + "en": { + "stringUnit": { + "state": "translated", + "value": "The order determines the arrangement in the panel. Each widget takes the first spot it fits into." + } + } + } + }, + "settings.widgets.hint.order": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Kacheln sind gleich groß. Die Reihenfolge hier ist die Reihenfolge im Panel — zum Sortieren ziehen." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "All tiles are the same size. The order here is the order in the panel — drag to sort." + } + } + } + }, + "settings.widgets.inPanel": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Panel" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "In the panel" + } + } + } + }, + "settings.widgets.remove": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus dem Panel entfernen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove from panel" + } + } + } + }, + "widget.fans.name": { + "extractionState": "stale", + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lüfter" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Fans" } } } } }, - "version" : "1.1" -} \ No newline at end of file + "version": "1.1" +} diff --git a/Onyx/MixerMenuBarModule.swift b/Onyx/MixerMenuBarModule.swift index b84b745..aebb700 100644 --- a/Onyx/MixerMenuBarModule.swift +++ b/Onyx/MixerMenuBarModule.swift @@ -14,6 +14,8 @@ final class MixerMenuBarModule: MenuBarModule { let id = "audio" var displayName: String { String(localized: "mixer.menubar.name") } + var shortLabel: String { String(localized: "mixer.menubar.short") } + private let mixer: AudioMixer private var view: MixerStatusView? private var isActive = false diff --git a/Onyx/SettingsView.swift b/Onyx/SettingsView.swift index 70e450b..8ab6d23 100644 --- a/Onyx/SettingsView.swift +++ b/Onyx/SettingsView.swift @@ -259,6 +259,10 @@ private struct ModuleRow: View { // Drehzahl als Zahl oder nur das Symbol — ein Verlauf der Drehzahl // sagt in 28 Punkten Breite nichts. case "fans": [.value, .symbol] + // Der Akku kann alles, und gerade dort will man Symbol oder Kürzel: + // eine nackte Prozentzahl neben vier anderen sagt nichts. + case "metric.battery": [.symbolAndValue, .labelAndValue, .value, .symbol, + .graph, .valueAndGraph] default: MenuBarPresentation.allCases } } @@ -331,6 +335,8 @@ extension MenuBarPresentation { case .bars: "settings.menubar.bars" case .valueAndGraph: "settings.menubar.valueAndGraph" case .symbol: "settings.menubar.symbol" + case .labelAndValue: "settings.menubar.labelAndValue" + case .symbolAndValue: "settings.menubar.symbolAndValue" } } } diff --git a/OnyxHelper/SMCAccess.swift b/OnyxHelper/SMCAccess.swift index 4fe0a4c..2a3dccc 100644 --- a/OnyxHelper/SMCAccess.swift +++ b/OnyxHelper/SMCAccess.swift @@ -104,12 +104,6 @@ final class SMCAccess { write(key, bytes: [value]) } - /// Schreibt beliebige Bytes — für `CHLT`, das drei davon hat. - @discardableResult - func writeBytes(_ key: String, _ bytes: [UInt8]) -> Bool { - write(key, bytes: bytes) - } - private func write(_ key: String, bytes: [UInt8]) -> Bool { guard let info = keyInfo(for: key) else { return false } // Nur schreiben, wenn die Größe zur Firmware passt. Ein zu kurzer oder diff --git a/OnyxHelper/main.swift b/OnyxHelper/main.swift index a6e47be..0fc795b 100644 --- a/OnyxHelper/main.swift +++ b/OnyxHelper/main.swift @@ -138,21 +138,6 @@ final class FanService: NSObject, OnyxHelperProtocol { reply(percent) } - func setChargeLimit(percent: Int, reply: @escaping (Int) -> Void) { - lastHeartbeat = Date() - guard let smc else { reply(-1); return } - - let wanted = ChargeSafety.clamp(percent) - let written = smc.writeBytes(ChargeSafety.key, ChargeSafety.encode(wanted)) - - // Immer zurücklesen. Ein Schreibvorgang, den der SMC stillschweigend - // verwirft, darf nicht als Erfolg gemeldet werden — sonst zeigt die - // Oberfläche eine Grenze an, die es nicht gibt. - let readBack = smc.readBytes(ChargeSafety.key).flatMap(ChargeSafety.decode) ?? -1 - log.notice("Ladelimit: \(percent, privacy: .public) % gewünscht, \(wanted, privacy: .public) % geschrieben (\(written, privacy: .public)), zurückgelesen \(readBack, privacy: .public) %") - reply(readBack) - } - func setFanAutomatic(index: Int, reply: @escaping (Data?) -> Void) { lastHeartbeat = Date() requests[index] = nil diff --git a/Packages/OnyxKit/Sources/MetricsProvider/ChargeLimitView.swift b/Packages/OnyxKit/Sources/MetricsProvider/ChargeLimitView.swift index aa06122..ee9059b 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/ChargeLimitView.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/ChargeLimitView.swift @@ -1,90 +1,72 @@ import SwiftUI +import AppKit import OnyxDesign import OnyxHelperProtocol -/// Wer das Ladelimit setzen kann. +/// Wer das Ladelimit **lesen** kann. /// -/// Ein Protokoll und kein direkter Zugriff: das Limit schreibt der privilegierte -/// Helfer, und der gehört zur App, nicht zu diesem Package. Die Akku-Ansicht -/// soll ihn bedienen können, ohne ihn zu kennen. +/// Nur lesen, und das ist eine gemessene Grenze, keine Bequemlichkeit: der +/// SMC lehnt Schreibzugriffe auf `CHLT` ab. Der Helfer hat es versucht und +/// `false` gemeldet, der Wert blieb unverändert. macOS setzt das Limit +/// offenbar auf einem anderen Weg als über den SMC-Userclient. +/// +/// Ein Regler, der sich ziehen lässt und dann zurückspringt, ist schlimmer als +/// gar keiner — er behauptet eine Fähigkeit, die nicht existiert. @MainActor -public protocol ChargeLimitControlling: AnyObject { +public protocol ChargeLimitReading: AnyObject { /// Das eingestellte Limit in Prozent, `nil` wenn keines gilt. var chargeLimit: Int? { get } - /// Ob überhaupt geschrieben werden kann — ohne Helfer geht nichts. - var canControlCharge: Bool { get } + /// Ob der Wert überhaupt gelesen werden kann — ohne Helfer nicht. + var canReadCharge: Bool { get } func refreshChargeLimit() - func setChargeLimit(_ percent: Int) } -/// Der Ladelimit-Regler, wie er im Akku-Widget und im Akku-Popover steht. -/// -/// Ein Akku, der dauerhaft bei 100 % am Netz hängt, altert schneller. Deshalb -/// gibt es die Einstellung — und deshalb ist ihr Bereich so schmal: 80 bis -/// 100 % ist, was macOS selbst anbietet und wofür gemessen wurde, wie der -/// zuständige SMC-Key aussieht. Darunter wäre geraten. +/// Zeigt das Ladelimit und führt dorthin, wo man es ändern kann. public struct ChargeLimitControl: View { - private let control: any ChargeLimitControlling - /// Im Popover ist Platz für die Erklärung, in der Kachel nicht. + private let control: any ChargeLimitReading + /// Im Popover ist Platz für den Hinweis, in der Kachel nicht. private let roomy: Bool - @State private var pending: Double? - - public init(control: any ChargeLimitControlling, roomy: Bool) { + public init(control: any ChargeLimitReading, roomy: Bool) { self.control = control self.roomy = roomy } - private var current: Double { - pending ?? Double(control.chargeLimit ?? ChargeSafety.noLimit) - } - - private var isOff: Bool { current >= Double(ChargeSafety.noLimit) } - public var body: some View { - VStack(alignment: .leading, spacing: 5) { - HStack(spacing: 6) { - Text("charge.limit", bundle: .module) - .font(Onyx.Font.caption) - .foregroundStyle(Onyx.Color.textSecondary) - Spacer(minLength: 4) - Group { - // Hundert Prozent ist kein Limit — und soll auch nicht so - // heißen, sonst sucht man den Ausschalter. - if isOff { - Text("charge.limit.off", bundle: .module) - } else { - Text(verbatim: "\(Int(current)) %") - } - } - .font(Onyx.Font.metricSmall).monospacedDigit() - .foregroundStyle(isOff ? Onyx.Color.textTertiary : Onyx.Color.accent) + HStack(spacing: 6) { + Text("charge.limit", bundle: .module) + .font(Onyx.Font.caption) + .foregroundStyle(Onyx.Color.textSecondary) + + Spacer(minLength: 4) + + if let limit = control.chargeLimit, limit < ChargeSafety.noLimit { + Text(verbatim: "\(limit) %") + .font(Onyx.Font.metricSmall).monospacedDigit() + .foregroundStyle(Onyx.Color.accent) + } else if control.canReadCharge { + Text("charge.limit.off", bundle: .module) + .font(Onyx.Font.metricSmall) + .foregroundStyle(Onyx.Color.textTertiary) + } else { + Text(verbatim: "–") + .font(Onyx.Font.metricSmall) + .foregroundStyle(Onyx.Color.textTertiary) } - Slider(value: Binding(get: { current }, set: { pending = ($0 / 5).rounded() * 5 }), - in: Double(ChargeSafety.minimumPercent)...Double(ChargeSafety.maximumPercent), - onEditingChanged: { editing in - // Erst beim Loslassen schreiben. Bei jedem Zwischenwert - // in den Ladepfad zu schreiben wäre ein Dutzend - // Schreibvorgänge für eine einzige Entscheidung. - guard !editing, let pending else { return } - control.setChargeLimit(Int(pending)) - self.pending = nil - }) - .controlSize(.mini) - .disabled(!control.canControlCharge) - - if !control.canControlCharge { - Text("charge.limit.needsHelper", bundle: .module) - .font(Onyx.Font.metricSmall) - .foregroundStyle(Onyx.Color.textTertiary) - .fixedSize(horizontal: false, vertical: true) - } else if roomy { - Text("charge.limit.hint", bundle: .module) - .font(Onyx.Font.metricSmall) - .foregroundStyle(Onyx.Color.textTertiary) - .fixedSize(horizontal: false, vertical: true) + if roomy { + // Kein Regler, sondern der Weg dorthin, wo es wirklich geht. + Button { + NSWorkspace.shared.open( + URL(string: "x-apple.systempreferences:com.apple.Battery-Settings.extension")!) + } label: { + Image(systemName: "arrow.up.forward.app") + .font(.system(size: 10)) + .foregroundStyle(Onyx.Color.textTertiary) + } + .buttonStyle(.plain) + .help(Text("charge.limit.inSystemSettings", bundle: .module)) } } .onAppear { control.refreshChargeLimit() } diff --git a/Packages/OnyxKit/Sources/MetricsProvider/Localizable.xcstrings b/Packages/OnyxKit/Sources/MetricsProvider/Localizable.xcstrings index 3d2ac54..c9e42e1 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/Localizable.xcstrings +++ b/Packages/OnyxKit/Sources/MetricsProvider/Localizable.xcstrings @@ -56,6 +56,134 @@ } } }, + "battery.charging": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Lädt" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Charging" + } + } + } + }, + "battery.cycles": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zyklen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Cycles" + } + } + } + }, + "battery.draining": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbrauch" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Draw" + } + } + } + }, + "battery.health": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Zustand" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Health" + } + } + } + }, + "battery.lowPower": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Energiesparmodus" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Low Power Mode" + } + } + } + }, + "battery.pluggedIn": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Am Netz" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Plugged in" + } + } + } + }, + "battery.remaining %@": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "noch %@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ remaining" + } + } + } + }, + "battery.untilFull %@": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ bis voll" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ until full" + } + } + } + }, "charge.limit": { "localizations": { "de": { @@ -88,6 +216,22 @@ } } }, + "charge.limit.inSystemSettings": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "In den Systemeinstellungen einstellbar" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Set in System Settings" + } + } + } + }, "charge.limit.needsHelper": { "localizations": { "de": { @@ -120,6 +264,38 @@ } } }, + "menubar.short.battery": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Akku" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Batt" + } + } + } + }, + "menubar.short.temperature": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Temp" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Temp" + } + } + } + }, "metric.battery": { "localizations": { "de": { @@ -795,6 +971,38 @@ } } } + }, + "settings.menubar.labelAndValue": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Kürzel + Wert" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Label + value" + } + } + } + }, + "settings.menubar.symbolAndValue": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol + Wert" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Symbol + value" + } + } + } } }, "version": "1.1" diff --git a/Packages/OnyxKit/Sources/MetricsProvider/MetricMenuBarModule.swift b/Packages/OnyxKit/Sources/MetricsProvider/MetricMenuBarModule.swift index de19102..dbf17b5 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/MetricMenuBarModule.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/MetricMenuBarModule.swift @@ -28,8 +28,20 @@ public final class MetricMenuBarModule: MenuBarModule { self.id = "metric.\(metric.rawValue)" } + /// Kurz genug für die Menüleiste. „Arbeitsspeicher" wäre dort ein Balken. + public var shortLabel: String { + switch metric { + case .cpu: "CPU" + case .gpu: "GPU" + case .memory: "RAM" + case .battery: String(localized: "menubar.short.battery", bundle: .module) + case .temperature: String(localized: "menubar.short.temperature", bundle: .module) + } + } + public func makeStatusView(presentation: MenuBarPresentation) -> NSView { - let view = MetricStatusView(metric: metric, presentation: presentation) + let view = MetricStatusView(metric: metric, presentation: presentation, + label: shortLabel) view.update(snapshot: model.snapshot, history: model.series(metric), mode: model.displayMode(for: metric)) self.view = view @@ -84,9 +96,12 @@ final class MetricStatusView: NSView, WidthReporting { private var history: [Double] = [] private var mode: MetricDisplayMode = .usage - init(metric: MetricKind, presentation: MenuBarPresentation) { + private let label: String + + init(metric: MetricKind, presentation: MenuBarPresentation, label: String) { self.metric = metric self.presentation = presentation + self.label = label super.init(frame: NSRect(x: 0, y: 0, width: 22, height: 22)) } @@ -107,6 +122,8 @@ final class MetricStatusView: NSView, WidthReporting { 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) } } @@ -158,6 +175,16 @@ final class MetricStatusView: NSView, WidthReporting { drawGraph(color: color, in: bounds.insetBy(dx: 2, dy: 5)) case .bars: drawBars(color: color) + case .labelAndValue: + drawText("\(label) \(MetricSummary.value(metric, snapshot, mode: mode))", color: color) + + case .symbolAndValue: + 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)) + case .valueAndGraph: let split = bounds.width * 0.55 drawText(MetricSummary.value(metric, snapshot, mode: mode), color: color, diff --git a/Packages/OnyxKit/Sources/MetricsProvider/MetricPopovers.swift b/Packages/OnyxKit/Sources/MetricsProvider/MetricPopovers.swift index 2c7cab5..4b6e2df 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/MetricPopovers.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/MetricPopovers.swift @@ -232,7 +232,7 @@ private struct MemoryDetail: View { private struct BatteryDetail: View { let snapshot: MetricsSnapshot - let control: (any ChargeLimitControlling)? + let control: (any ChargeLimitReading)? var body: some View { if let battery = snapshot.battery { diff --git a/Packages/OnyxKit/Sources/MetricsProvider/MetricWidgets.swift b/Packages/OnyxKit/Sources/MetricsProvider/MetricWidgets.swift index 44bfdce..67f0ef0 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/MetricWidgets.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/MetricWidgets.swift @@ -54,14 +54,8 @@ private struct MetricWidgetView: View { // alles andere zeigt Zahl und Verlauf. if metric == .temperature { SensorList(snapshot: model.snapshot, detailed: true) - } else if metric == .battery, let control = model.chargeControl { - // Die Akku-Kachel bekommt den Ladelimit-Regler direkt: dort - // schaut man ohnehin hin, wenn man ihn braucht. - VStack(alignment: .leading, spacing: 8) { - WideMetric(model: model, metric: metric) - Divider().overlay(Onyx.Color.hairline) - ChargeLimitControl(control: control, roomy: false) - } + } else if metric == .battery { + BatteryTile(model: model) } else { WideMetric(model: model, metric: metric) } @@ -327,3 +321,97 @@ public enum MetricSummary { } } } + + +/// Die Akku-Kachel. +/// +/// „40 %" und ein Verlauf über die letzten Sekunden ist keine Auskunft — der +/// Ladestand ändert sich in Minuten, nicht in Sekunden, und ein Graph darüber +/// ist eine waagerechte Linie. Was man wissen will: wie lange noch, zieht oder +/// gibt der Akku gerade, läuft der Energiesparmodus, wo endet das Laden. +private struct BatteryTile: View { + let model: MetricsModel + + var body: some View { + let battery = model.snapshot.battery + + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text(MetricFormat.percent(battery?.charge ?? 0)) + .font(Onyx.Font.metric).monospacedDigit() + .foregroundStyle(tint(battery)) + if let battery, battery.isCharging { + Image(systemName: "bolt.fill") + .font(.system(size: 11)) + .foregroundStyle(Onyx.Color.positive) + } + Spacer(minLength: 0) + } + + if let battery { + if let remaining = battery.timeRemaining { + Text(battery.isCharging ? "battery.untilFull \(duration(remaining))" + : "battery.remaining \(duration(remaining))", + bundle: .module) + .font(Onyx.Font.caption) + .foregroundStyle(Onyx.Color.textSecondary) + } else if battery.isPluggedIn { + Text("battery.pluggedIn", bundle: .module) + .font(Onyx.Font.caption) + .foregroundStyle(Onyx.Color.textSecondary) + } + + Spacer(minLength: 0) + + VStack(alignment: .leading, spacing: 3) { + if let watts = battery.watts, abs(watts) > 0.1 { + row(watts > 0 ? "battery.charging" : "battery.draining", + value: String(format: "%.1f W", abs(watts))) + } + if let limit = model.chargeControl?.chargeLimit, limit < 100 { + row("charge.limit", value: "\(limit) %") + } + if let health = battery.health { + row("battery.health", value: MetricFormat.percent(health)) + } + if let cycles = battery.cycleCount { + row("battery.cycles", value: "\(cycles)") + } + } + + // Der Energiesparmodus erklärt, warum die Maschine langsamer + // wirkt. Ohne diesen Hinweis sucht man den Grund woanders. + if battery.isLowPower { + HStack(spacing: 4) { + Image(systemName: "leaf.fill").font(.system(size: 9)) + Text("battery.lowPower", bundle: .module).font(Onyx.Font.metricSmall) + } + .foregroundStyle(Onyx.Color.warning) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } + + private func row(_ key: String.LocalizationValue, value: String) -> some View { + HStack(spacing: 4) { + Text(String(localized: key, bundle: .module)) + .foregroundStyle(Onyx.Color.textTertiary) + Spacer(minLength: 4) + Text(value).monospacedDigit().foregroundStyle(Onyx.Color.textSecondary) + } + .font(Onyx.Font.metricSmall) + } + + private func tint(_ battery: BatteryReading?) -> Color { + guard let battery else { return Onyx.Color.textPrimary } + if battery.isCharging { return Onyx.Color.positive } + return battery.charge < 0.2 ? Onyx.Color.critical : Onyx.Color.textPrimary + } + + private func duration(_ seconds: TimeInterval) -> String { + let total = Int(seconds) + return total >= 3600 ? "\(total / 3600) h \(total % 3600 / 60) min" + : "\(total / 60) min" + } +} diff --git a/Packages/OnyxKit/Sources/MetricsProvider/MetricsModel.swift b/Packages/OnyxKit/Sources/MetricsProvider/MetricsModel.swift index 331683e..b446000 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/MetricsModel.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/MetricsModel.swift @@ -16,7 +16,7 @@ public final class MetricsModel { /// Wer das Ladelimit setzen kann — die App reicht den Helfer herein. /// `nil` heißt: kein Regler, weil es nichts zu regeln gibt. - public weak var chargeControl: (any ChargeLimitControlling)? + public weak var chargeControl: (any ChargeLimitReading)? public private(set) var snapshot = MetricsSnapshot() /// Verlauf für die Graphen, jüngster Wert zuletzt. diff --git a/Packages/OnyxKit/Sources/MetricsProvider/SystemMetrics.swift b/Packages/OnyxKit/Sources/MetricsProvider/SystemMetrics.swift index 7580b69..a7ca16f 100644 --- a/Packages/OnyxKit/Sources/MetricsProvider/SystemMetrics.swift +++ b/Packages/OnyxKit/Sources/MetricsProvider/SystemMetrics.swift @@ -72,6 +72,11 @@ public struct BatteryReading: Equatable, Sendable { public var health: Double? // 0 bis 1 public var watts: Double? // negativ = Entladung public var timeRemaining: TimeInterval? + /// Energiesparmodus. Erklärt, warum die Maschine langsamer wirkt — und + /// gehört deshalb sichtbar dorthin, wo man nach dem Grund sucht. + public var isLowPower = false + /// Temperatur der Zelle, falls der SMC sie hergibt. + public var celsius: Double? } /// Ein Programm mit seinem Speicherverbrauch. @@ -334,6 +339,9 @@ public final class SystemMetrics: @unchecked Sendable { } // Leistung aus dem SMC: positiv beim Laden, negativ beim Entladen. + reading.isLowPower = ProcessInfo.processInfo.isLowPowerModeEnabled + // TB0T ist die Akkutemperatur; nicht jede Maschine führt sie. + reading.celsius = smc?.float("TB0T").map { Double($0) } if let watts = smc?.float("PPBR") { reading.watts = reading.isCharging ? watts : -watts } diff --git a/Packages/OnyxKit/Sources/NetworkProvider/Localizable.xcstrings b/Packages/OnyxKit/Sources/NetworkProvider/Localizable.xcstrings index ecd1a67..d5a2c2d 100644 --- a/Packages/OnyxKit/Sources/NetworkProvider/Localizable.xcstrings +++ b/Packages/OnyxKit/Sources/NetworkProvider/Localizable.xcstrings @@ -1,6 +1,22 @@ { "sourceLanguage": "en", "strings": { + "menubar.short.network": { + "localizations": { + "de": { + "stringUnit": { + "state": "translated", + "value": "Netz" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Net" + } + } + } + }, "network.addresses": { "localizations": { "de": { diff --git a/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift b/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift index a2a592c..ffae423 100644 --- a/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift +++ b/Packages/OnyxKit/Sources/NetworkProvider/NetworkMenuBarModule.swift @@ -21,6 +21,8 @@ public final class NetworkMenuBarModule: MenuBarModule { 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)) @@ -84,6 +86,8 @@ final class NetworkStatusView: NSView { 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 } } @@ -117,6 +121,12 @@ final class NetworkStatusView: NSView { 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 diff --git a/Packages/OnyxKit/Sources/OnyxHelperProtocol/HelperProtocol.swift b/Packages/OnyxKit/Sources/OnyxHelperProtocol/HelperProtocol.swift index 1911e79..3edbb04 100644 --- a/Packages/OnyxKit/Sources/OnyxHelperProtocol/HelperProtocol.swift +++ b/Packages/OnyxKit/Sources/OnyxHelperProtocol/HelperProtocol.swift @@ -31,12 +31,12 @@ import Foundation /// Das aktuelle Ladelimit in Prozent, `-1` wenn keines gilt. /// + /// Nur lesen. Der SMC lehnt Schreibzugriffe auf `CHLT` ab — nachgemessen, + /// siehe docs/spikes/A-smc.md. macOS setzt den Wert offenbar auf einem + /// anderen Weg als über den SMC-Userclient. + /// /// `Int` statt `Int?`: Optionals sind über `@objc` nicht darstellbar. func chargeLimit(reply: @escaping (Int) -> Void) - - /// Setzt das Ladelimit. Der Helfer begrenzt auf den gemessenen Bereich und - /// liest nach dem Schreiben zurück — gemeldet wird, was tatsächlich steht. - func setChargeLimit(percent: Int, reply: @escaping (Int) -> Void) } /// Was der Helfer über einen Lüfter berichtet. diff --git a/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarModule.swift b/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarModule.swift index 9c016cd..dc4d299 100644 --- a/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarModule.swift +++ b/Packages/OnyxKit/Sources/OnyxMenuBar/MenuBarModule.swift @@ -12,6 +12,13 @@ public enum MenuBarPresentation: String, Codable, Sendable, CaseIterable, Identi case valueAndGraph /// Nur ein Glyph, nach Grenzwert eingefärbt. case symbol + /// Kürzel und Wert, z. B. `CPU 34 %`. + /// + /// Fünf nackte Prozentzahlen nebeneinander sind unbrauchbar: man sieht + /// Zahlen und weiß nicht, welche wozu gehört. + case labelAndValue + /// Glyph und Wert, z. B. 🔋 `34 %`. + case symbolAndValue public var id: String { rawValue } } @@ -25,6 +32,9 @@ public protocol MenuBarModule: AnyObject { var id: String { get } var displayName: String { get } + /// Das Kürzel für die Menüleiste — kurz, sonst frisst es die Leiste auf. + var shortLabel: String { get } + /// Zeichnet den kompakten Zustand. Muss eine feste Breite je Darstellungsart /// liefern, sonst springt die ganze Menüleiste bei jedem Messwert. func makeStatusView(presentation: MenuBarPresentation) -> NSView diff --git a/docs/spikes/A-smc.md b/docs/spikes/A-smc.md index a99ce14..921b97b 100644 --- a/docs/spikes/A-smc.md +++ b/docs/spikes/A-smc.md @@ -72,7 +72,39 @@ Vorhanden ist stattdessen ein umfangreicher `CH*`-Satz. Plausible Kandidaten: **Das bleibt Vermutung, und Vermutungen werden hier nicht geschrieben.** Ein falscher Wert im Lade-Subsystem ist die eine Operation in diesem Projekt, die Hardware dauerhaft beschädigen kann. -### Nächster Schritt: Differenzmessung statt Raten +### Nachtrag 11.08.2026: Key gefunden, aber nicht beschreibbar + +Die Differenzmessung hat ihn geliefert. Der Key heißt **`CHLT`**, Typ `hex_`, drei Bytes: + +| macOS-Einstellung | `CHLT` | +|---|---| +| 80 % | `50 01 00` | +| 85 % | `55 01 00` | +| 95 % | `5F 01 00` | + +Erstes Byte ist die Prozentzahl, zweites offenbar „Limit aktiv", drittes unbenutzt. +Drei Messpunkte, weil einer Zufall sein kann: `BACC` passte beim ersten Vergleich ebenfalls — +ein Zähler, in dem gerade `50` stand. `ChargerConfiguration` aus `ioreg` sah mit niedrigem +Byte `0x50` ebenfalls wie eine Quelle aus, blieb aber unverändert, als `CHLT` längst auf 95 +stand. + +**Schreiben scheitert.** Der Helfer hat es mit korrekter Größe (3 Byte) versucht; der +SMC-Userclient gibt einen Fehler zurück, der Wert bleibt stehen: + +``` +Ladelimit: 95 % gewünscht, 95 % geschrieben (false), zurückgelesen 85 % +``` + +macOS setzt das Limit also auf einem anderen Weg. Welchem, ist offen — und weiter zu raten +hieße, im Lade-Subsystem Keys durchzuprobieren. Das ist die eine Operation in diesem Projekt, +die Hardware dauerhaft beschädigen kann, also unterbleibt es. + +**Folge für Onyx:** das Ladelimit ist eine **Anzeige**. Der Wert wird gelesen und im +Akku-Widget sowie im Akku-Popover gezeigt, mit einem Weg in die Systemeinstellungen. Der +Schreibpfad ist aus dem privilegierten Helfer wieder entfernt — ein Root-Dienst trägt nur, +was benutzt wird. + +### Damals geplanter Schritt: Differenzmessung statt Raten macOS hat selbst eine Ladebegrenzung (Systemeinstellungen → Batterie → Batteriezustand → „Optimiertes Laden" bzw. das 80-%-Limit). Wird sie umgeschaltet, muss sich der zuständige