diff --git a/Kit/extensions.swift b/Kit/extensions.swift index 9671be20..4c21a505 100644 --- a/Kit/extensions.swift +++ b/Kit/extensions.swift @@ -202,6 +202,19 @@ public extension Double { return "\(h)h \(minutes)min" } + + func power(_ unit: String) -> Double { + switch unit { + case "mJ": + return self / 1e3 + case "uJ": + return self / 1e6 + case "nJ": + return self / 1e9 + default: + return 0 + } + } } public extension NSView { diff --git a/Kit/helpers.swift b/Kit/helpers.swift index aba3359a..7fa55142 100644 --- a/Kit/helpers.swift +++ b/Kit/helpers.swift @@ -1395,7 +1395,7 @@ public class PreferencesRow: NSStackView { self.addArrangedSubview(view) } - private func text(_ title: String? = nil, _ description: String? = nil) -> NSView { + fileprivate func text(_ title: String? = nil, _ description: String? = nil) -> NSView { let view: NSStackView = NSStackView() view.orientation = .vertical view.spacing = 0 diff --git a/Kit/module/settings.swift b/Kit/module/settings.swift index fee0605a..357c1aab 100644 --- a/Kit/module/settings.swift +++ b/Kit/module/settings.swift @@ -46,17 +46,16 @@ open class Settings: NSStackView, Settings_p { Store.shared.bool(key: "OneView", defaultValue: false) } private var oneViewState: Bool { - get { - return Store.shared.bool(key: "\(self.config.pointee.name)_oneView", defaultValue: false) - } - set { - Store.shared.set(key: "\(self.config.pointee.name)_oneView", value: newValue) - } + get { Store.shared.bool(key: "\(self.config.pointee.name)_oneView", defaultValue: false) } + set { Store.shared.set(key: "\(self.config.pointee.name)_oneView", value: newValue) } } private var isPopupSettingsAvailable: Bool private var isNotificationsSettingsAvailable: Bool + private var previewView: NSView? = nil + private var settingsView: NSView? = nil + init(config: UnsafePointer, widgets: UnsafeMutablePointer<[SWidget]>, moduleSettings: Settings_v?, popupSettings: Popup_p?, notificationsSettings: NotificationsWrapper?) { self.config = config self.widgets = widgets.pointee @@ -80,11 +79,15 @@ open class Settings: NSStackView, Settings_p { right: Constants.Settings.margin ) - let widgetSelector = WidgetSelectorView(module: self.config.pointee.name, widgets: self.widgets, stateCallback: self.loadWidget) - let tabView = self.settingsView() + let header = self.header() + let settingsView = self.settings() + self.settingsView = settingsView + let previewView = self.preview() + self.previewView = previewView - self.addArrangedSubview(widgetSelector) - self.addArrangedSubview(tabView) + self.addArrangedSubview(header) + self.addArrangedSubview(settingsView) + self.addArrangedSubview(previewView) NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil) self.segmentedControl?.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -(Constants.Settings.margin*2)).isActive = true @@ -102,7 +105,31 @@ open class Settings: NSStackView, Settings_p { toggleNSControlState(self.enableControl, state: newState ? .on : .off) } - private func settingsView() -> NSView { + private func header() -> NSView { + let view = NSStackView() + view.orientation = .horizontal + view.spacing = Constants.Settings.margin + + let widgetSelector = WidgetSelectorView(module: self.config.pointee.name, widgets: self.widgets, stateCallback: self.loadWidget) +// let button = ButtonSelectorView { [weak self] in +// self?.toggleView() +// } + + view.addArrangedSubview(widgetSelector) +// view.addArrangedSubview(button) + + return view + } + + private func preview() -> NSView { + let view = NSStackView() + view.isHidden = true + view.orientation = .vertical + view.addArrangedSubview(EmptyView(height: 0, msg: localizedString("Preview is not available for that module"))) + return view + } + + private func settings() -> NSView { let view = NSStackView() view.orientation = .vertical view.spacing = Constants.Settings.margin @@ -268,6 +295,13 @@ open class Settings: NSStackView, Settings_p { self.oneViewBtn?.state = self.oneViewState ? .on : .off } } + + @objc private func toggleView() { + guard let preview = self.previewView, let settings = self.settingsView else { return } + + preview.isHidden = !preview.isHidden + settings.isHidden = !settings.isHidden + } } private class WidgetSelectorView: NSStackView { @@ -622,3 +656,90 @@ private class WidgetSettings: NSStackView { return container } } + +private class ButtonSelectorView: NSStackView { + private var callback: () -> Void + + private var background: NSVisualEffectView = { + let view = NSVisualEffectView(frame: NSRect.zero) + view.blendingMode = .withinWindow + view.material = .contentBackground + view.state = .active + view.wantsLayer = true + view.layer?.cornerRadius = 5 + return view + }() + + private var settingsIcon: NSImage { + if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "gear", scale: .large) { + return icon + } + return NSImage(named: NSImage.Name("settings"))! + } + private var previewIcon: NSImage { + if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "command", scale: .large) { + return icon + } + return NSImage(named: NSImage.Name("chart"))! + } + + private var button: NSButton? = nil + private var isSettingsEnabled: Bool = false + + fileprivate init(callback: @escaping () -> Void) { + self.callback = callback + + super.init(frame: NSRect.zero) + + self.heightAnchor.constraint(equalToConstant: Constants.Widget.height + (Constants.Settings.margin*2)).isActive = true + self.translatesAutoresizingMaskIntoConstraints = false + self.edgeInsets = NSEdgeInsets( + top: Constants.Settings.margin, + left: Constants.Settings.margin, + bottom: Constants.Settings.margin, + right: Constants.Settings.margin + ) + self.spacing = Constants.Settings.margin + + self.addSubview(self.background, positioned: .below, relativeTo: .none) + + let button = NSButton() + button.toolTip = localizedString("Open module settings") + button.bezelStyle = .regularSquare + button.translatesAutoresizingMaskIntoConstraints = false + button.imageScaling = .scaleNone + button.image = self.settingsIcon + button.contentTintColor = .secondaryLabelColor + button.isBordered = false + button.action = #selector(self.action) + button.target = self + button.focusRingType = .none + button.widthAnchor.constraint(equalToConstant: Constants.Widget.height).isActive = true + self.button = button + + self.addArrangedSubview(button) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLayer() { + self.background.setFrameSize(self.frame.size) + } + + @objc private func action() { + guard let button = self.button else { return } + self.callback() + + self.isSettingsEnabled = !self.isSettingsEnabled + + if self.isSettingsEnabled { + button.image = self.previewIcon + button.toolTip = localizedString("Close module settings") + } else { + button.image = self.settingsIcon + button.toolTip = localizedString("Open module settings") + } + } +} diff --git a/Modules/CPU/readers.swift b/Modules/CPU/readers.swift index 707b311b..5afc18a1 100644 --- a/Modules/CPU/readers.swift +++ b/Modules/CPU/readers.swift @@ -415,8 +415,8 @@ public class FrequencyReader: Reader<[Double]> { private func getSamples() async -> [([IOSample], TimeInterval)] { let duration = 500 let step = UInt64(duration / self.measurementCount) - var prev = self.prev ?? self.getSample() ?? self.prev! var samples = [([IOSample], TimeInterval)]() + guard var prev = self.prev ?? self.getSample() else { return samples } for _ in 0.. +#include typedef struct __IOHIDEvent *IOHIDEventRef; typedef struct __IOHIDServiceClient *IOHIDServiceClientRef; +typedef struct IOReportSubscriptionRef* IOReportSubscriptionRef; #ifdef __LP64__ typedef double IOHIDFloat; #else @@ -32,3 +34,17 @@ CFTypeRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFString IOHIDFloat IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field); NSDictionary*AppleSiliconSensors(int page, int usage, int32_t type); + +CFDictionaryRef IOReportCopyChannelsInGroup(CFStringRef a, CFStringRef b, uint64_t c, uint64_t d, uint64_t e); +void IOReportMergeChannels(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef null); +IOReportSubscriptionRef IOReportCreateSubscription(void* a, CFMutableDictionaryRef b, CFMutableDictionaryRef* c, uint64_t d, CFTypeRef e); +CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef a, CFMutableDictionaryRef b, CFTypeRef c); +CFDictionaryRef IOReportCreateSamplesDelta(CFDictionaryRef a, CFDictionaryRef b, CFTypeRef c); +CFStringRef IOReportChannelGetGroup(CFDictionaryRef a); +CFStringRef IOReportChannelGetSubGroup(CFDictionaryRef a); +CFStringRef IOReportChannelGetChannelName(CFDictionaryRef a); +CFStringRef IOReportChannelGetUnitLabel(CFDictionaryRef a); +int32_t IOReportStateGetCount(CFDictionaryRef a); +CFStringRef IOReportStateGetNameForIndex(CFDictionaryRef a, int32_t b); +int64_t IOReportStateGetResidency(CFDictionaryRef a, int32_t b); +int64_t IOReportSimpleGetIntegerValue(CFDictionaryRef a, int32_t b); diff --git a/Modules/Sensors/readers.swift b/Modules/Sensors/readers.swift index d4449482..9615edd3 100644 --- a/Modules/Sensors/readers.swift +++ b/Modules/Sensors/readers.swift @@ -25,9 +25,19 @@ internal class SensorsReader: Reader { } private var unknownSensorsState: Bool + private var channels: CFMutableDictionary? = nil + private var subscription: IOReportSubscriptionRef? = nil + private var powers: (CPU: Double, GPU: Double, ANE: Double, RAM: Double, PCI: Double) = (0.0, 0.0, 0.0, 0.0, 0.0) + init(callback: @escaping (T?) -> Void = {_ in }) { self.unknownSensorsState = Store.shared.bool(key: "Sensors_unknown", defaultValue: false) super.init(.sensors, callback: callback) + + self.channels = self.getChannels() + var dict: Unmanaged? + self.subscription = IOReportCreateSubscription(nil, self.channels, &dict, 0, nil) + dict?.release() + self.list.sensors = self.sensors() } @@ -108,6 +118,7 @@ internal class SensorsReader: Reader { if self.HIDState { results += self.initHIDSensors() } + results += self.initIOSensors() #endif results += self.initCalculatedSensors(results) @@ -161,6 +172,24 @@ internal class SensorsReader: Reader { } } } + + if let (cpu, gpu, ane, ram, pci) = self.IOSensors() { + if let idx = self.list.sensors.firstIndex(where: { $0.key == "CPU Power" }) { + self.list.sensors[idx].value = cpu + } + if let idx = self.list.sensors.firstIndex(where: { $0.key == "GPU Power" }) { + self.list.sensors[idx].value = gpu + } + if let idx = self.list.sensors.firstIndex(where: { $0.key == "ANE Power" }) { + self.list.sensors[idx].value = ane + } + if let idx = self.list.sensors.firstIndex(where: { $0.key == "RAM Power" }) { + self.list.sensors[idx].value = ram + } + if let idx = self.list.sensors.firstIndex(where: { $0.key == "PCI Power" }) { + self.list.sensors[idx].value = pci + } + } #endif if !cpuSensors.isEmpty { @@ -448,3 +477,90 @@ extension SensorsReader { } } } + +// MARK: - Apple Silicon power sensors + +extension SensorsReader { + private func getChannels() -> CFMutableDictionary? { + let channelNames: [(String, String?)] = [("Energy Model", nil)] + + var channels: [CFDictionary] = [] + for (gname, sname) in channelNames { + let channel = IOReportCopyChannelsInGroup(gname as CFString?, sname as CFString?, 0, 0, 0) + guard let channel = channel?.takeRetainedValue() else { continue } + channels.append(channel) + } + + let chan = channels[0] + for i in 1.. [Sensor] { + guard let (cpu, gpu, ane, ram, pci) = self.IOSensors() else { return [] } + return [ + Sensor(key: "CPU Power", name: "CPU Power", value: cpu, group: .CPU, type: .power, platforms: Platform.apple, isComputed: true), + Sensor(key: "GPU Power", name: "GPU Power", value: gpu, group: .GPU, type: .power, platforms: Platform.apple, isComputed: true), + Sensor(key: "ANE Power", name: "ANE Power", value: ane, group: .system, type: .power, platforms: Platform.apple, isComputed: true), + Sensor(key: "RAM Power", name: "RAM Power", value: ram, group: .system, type: .power, platforms: Platform.apple, isComputed: true), + Sensor(key: "PCI Power", name: "PCI Power", value: pci, group: .system, type: .power, platforms: Platform.apple, isComputed: true) + ] + } + + private func IOSensors() -> (Double, Double, Double, Double, Double)? { + guard let sample = IOReportCreateSamples(self.subscription, self.channels, nil)?.takeRetainedValue(), + let dict = sample as? [String: Any] else { + return nil + } + let items = dict["IOReportChannels"] as! CFArray + + let prevCPU = self.powers.CPU + let prevGPU = self.powers.GPU + let prevANE = self.powers.ANE + let prevRAM = self.powers.RAM + let prevPCI = self.powers.PCI + + for i in 0..