From 80d7789ef72c26b91964b26dc205fcf4a9b2c494 Mon Sep 17 00:00:00 2001 From: Serhiy Mytrovtsiy Date: Wed, 17 Jul 2024 19:34:51 +0200 Subject: [PATCH] feat: added macOS widget for GPU module --- Kit/module/module.swift | 9 +-- Modules/CPU/main.swift | 2 - Modules/Disk/main.swift | 1 - Modules/GPU/main.swift | 12 +++- Modules/GPU/widget.swift | 101 +++++++++++++++++++++++++++++++- Modules/RAM/main.swift | 2 - Stats.xcodeproj/project.pbxproj | 64 ++++++++++++++++++++ Widgets/widgets.swift | 2 + 8 files changed, 179 insertions(+), 14 deletions(-) diff --git a/Kit/module/module.swift b/Kit/module/module.swift index 4cc3cc64..e12aefea 100644 --- a/Kit/module/module.swift +++ b/Kit/module/module.swift @@ -77,13 +77,10 @@ open class Module { config.name } public var combinedPosition: Int { - get { - Store.shared.int(key: "\(self.name)_position", defaultValue: 0) - } - set { - Store.shared.set(key: "\(self.name)_position", value: newValue) - } + get { Store.shared.int(key: "\(self.name)_position", defaultValue: 0) } + set { Store.shared.set(key: "\(self.name)_position", value: newValue) } } + public var userDefaults: UserDefaults? = UserDefaults(suiteName: "eu.exelban.Stats.widgets") private var settingsView: Settings_v? = nil private var popup: PopupWindow? = nil diff --git a/Modules/CPU/main.swift b/Modules/CPU/main.swift index 5f27b3f5..5417de1c 100644 --- a/Modules/CPU/main.swift +++ b/Modules/CPU/main.swift @@ -83,8 +83,6 @@ public class CPU: Module { return color.additional as! NSColor } - private var userDefaults: UserDefaults? = UserDefaults(suiteName: "eu.exelban.Stats.widgets") - public init() { self.settingsView = Settings(.CPU) self.popupView = Popup(.CPU) diff --git a/Modules/Disk/main.swift b/Modules/Disk/main.swift index a4fc1b05..ea38a3eb 100644 --- a/Modules/Disk/main.swift +++ b/Modules/Disk/main.swift @@ -191,7 +191,6 @@ public class Disk: Module { private var processReader: ProcessReader? private var selectedDisk: String = "" - private var userDefaults: UserDefaults? = UserDefaults(suiteName: "eu.exelban.Stats.widgets") public init() { super.init( diff --git a/Modules/GPU/main.swift b/Modules/GPU/main.swift index 0bcb59e1..1ef5213d 100644 --- a/Modules/GPU/main.swift +++ b/Modules/GPU/main.swift @@ -11,6 +11,7 @@ import Cocoa import Kit +import WidgetKit public typealias GPU_type = String public enum GPU_types: GPU_type { @@ -40,13 +41,16 @@ public struct GPU_Info: Codable { public var renderUtilization: Double? = nil public var tilerUtilization: Double? = nil - init(id: String, type: GPU_type, IOClass: String, vendor: String? = nil, model: String, cores: Int?) { + init(id: String, type: GPU_type, IOClass: String, vendor: String? = nil, model: String, cores: Int?, utilization: Double? = nil, render: Double? = nil, tiler: Double? = nil) { self.id = id self.type = type self.IOClass = IOClass self.vendor = vendor self.model = model self.cores = cores + self.utilization = utilization + self.renderUtilization = render + self.tilerUtilization = tiler } } @@ -143,5 +147,11 @@ public class GPU: Module { default: break } } + + if #available(macOS 11.0, *) { + guard let blobData = try? JSONEncoder().encode(selectedGPU) else { return } + self.userDefaults?.set(blobData, forKey: "GPU@InfoReader") + WidgetCenter.shared.reloadTimelines(ofKind: GPU_entry.kind) + } } } diff --git a/Modules/GPU/widget.swift b/Modules/GPU/widget.swift index 361ebaf2..bb454efe 100644 --- a/Modules/GPU/widget.swift +++ b/Modules/GPU/widget.swift @@ -2,11 +2,108 @@ // widget.swift // GPU // -// Created by Serhiy Mytrovtsiy on 16/07/2024 +// Created by Serhiy Mytrovtsiy on 17/07/2024 // Using Swift 5.0 // Running on macOS 14.5 // // Copyright © 2024 Serhiy Mytrovtsiy. All rights reserved. // -import Foundation +import SwiftUI +import WidgetKit +import Charts +import Kit + +public struct GPU_entry: TimelineEntry { + public static let kind = "GPUWidget" + public static var snapshot: GPU_entry = GPU_entry(value: GPU_Info(id: "", type: "", IOClass: "", model: "", cores: nil, utilization: 0.11, render: 0.11, tiler: 0.11)) + + public var date: Date { + Calendar.current.date(byAdding: .second, value: 5, to: Date())! + } + public var value: GPU_Info? = nil +} + +@available(macOS 11.0, *) +public struct Provider: TimelineProvider { + public typealias Entry = GPU_entry + + private let userDefaults: UserDefaults? = UserDefaults(suiteName: "eu.exelban.Stats.widgets") + + public func placeholder(in context: Context) -> GPU_entry { + GPU_entry() + } + + public func getSnapshot(in context: Context, completion: @escaping (GPU_entry) -> Void) { + completion(GPU_entry.snapshot) + } + + public func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + var entry = GPU_entry() + if let raw = userDefaults?.data(forKey: "GPU@InfoReader"), let load = try? JSONDecoder().decode(GPU_Info.self, from: raw) { + entry.value = load + } + let entries: [GPU_entry] = [entry] + completion(Timeline(entries: entries, policy: .atEnd)) + } +} + +@available(macOS 14.0, *) +public struct GPUWidget: Widget { + var usedColor: Color = Color(nsColor: NSColor.systemBlue) + var freeColor: Color = Color(nsColor: NSColor.lightGray) + + public init() {} + + public var body: some WidgetConfiguration { + StaticConfiguration(kind: GPU_entry.kind, provider: Provider()) { entry in + VStack(spacing: 10) { + if let value = entry.value { + HStack { + Chart { + SectorMark(angle: .value(localizedString("Used"), value.utilization ?? 0), innerRadius: .ratio(0.8)).foregroundStyle(self.usedColor) + SectorMark(angle: .value(localizedString("Free"), 1-(value.utilization ?? 0)), innerRadius: .ratio(0.8)).foregroundStyle(self.freeColor) + } + .frame(maxWidth: .infinity, maxHeight: 84) + .chartLegend(.hidden) + .chartBackground { chartProxy in + GeometryReader { geometry in + if let anchor = chartProxy.plotFrame { + let frame = geometry[anchor] + Text("\(Int((value.utilization ?? 0)*100))%") + .font(.system(size: 16, weight: .regular)) + .position(x: frame.midX, y: frame.midY) + } + } + } + } + VStack(spacing: 3) { + HStack { + Text(localizedString("Usage")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary) + Spacer() + Text("\(Int((value.utilization ?? 0)*100))%") + } + HStack { + Text(localizedString("Render")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary) + Spacer() + Text("\(Int((value.renderUtilization ?? 0)*100))%") + } + HStack { + Text(localizedString("Tiler")).font(.system(size: 12, weight: .regular)).foregroundColor(.secondary) + Spacer() + Text("\(Int((value.tilerUtilization ?? 0)*100))%") + } + } + } else { + Text("No data") + } + } + .containerBackground(for: .widget) { + Color.clear + } + } + .configurationDisplayName("GPU widget") + .description("Displays GPU stats") + .supportedFamilies([.systemSmall]) + } +} diff --git a/Modules/RAM/main.swift b/Modules/RAM/main.swift index a2f3ef0a..8f95d7b5 100644 --- a/Modules/RAM/main.swift +++ b/Modules/RAM/main.swift @@ -82,8 +82,6 @@ public class RAM: Module { return color.additional as! NSColor } - private var userDefaults: UserDefaults? = UserDefaults(suiteName: "eu.exelban.Stats.widgets") - public init() { self.settingsView = Settings(.RAM) self.popupView = Popup(.RAM) diff --git a/Stats.xcodeproj/project.pbxproj b/Stats.xcodeproj/project.pbxproj index ba6510e9..b2dcffe1 100644 --- a/Stats.xcodeproj/project.pbxproj +++ b/Stats.xcodeproj/project.pbxproj @@ -10,6 +10,11 @@ 5C044F7A2B3DE6F3005F6951 /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C044F792B3DE6F3005F6951 /* portal.swift */; }; 5C0A2A8A292A5B4D009B4C1F /* SMJobBlessUtil.py in Resources */ = {isa = PBXBuildFile; fileRef = 5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */; }; 5C0A9CA22C467AA300EE6A89 /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0A9CA12C467AA300EE6A89 /* widget.swift */; }; + 5C0A9CA42C467F7A00EE6A89 /* widget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0A9CA32C467F7A00EE6A89 /* widget.swift */; }; + 5C0A9CA52C46838300EE6A89 /* CPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A97CECA2537331B00742D8F /* CPU.framework */; }; + 5C0A9CAA2C46838A00EE6A89 /* GPU.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A90E18924EAD2BB00471E9A /* GPU.framework */; }; + 5C0A9CAF2C46838F00EE6A89 /* RAM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A81C7562449A41400825D92 /* RAM.framework */; }; + 5C0A9CB42C46839500EE6A89 /* Disk.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AF9EE0224648751005D2270 /* Disk.framework */; }; 5C21D80B296C7B81005BA16D /* CombinedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C21D80A296C7B81005BA16D /* CombinedView.swift */; }; 5C2229A329CCB3C400F00E69 /* Clock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; }; 5C2229A429CCB3C400F00E69 /* Clock.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5C22299D29CCB3C400F00E69 /* Clock.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -191,6 +196,34 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 5C0A9CA72C46838300EE6A89 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A1410ED229E721100D29793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A97CEC92537331B00742D8F; + remoteInfo = CPU; + }; + 5C0A9CAC2C46838A00EE6A89 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A1410ED229E721100D29793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A90E18824EAD2BB00471E9A; + remoteInfo = GPU; + }; + 5C0A9CB12C46838F00EE6A89 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A1410ED229E721100D29793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9A81C7552449A41400825D92; + remoteInfo = RAM; + }; + 5C0A9CB62C46839500EE6A89 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9A1410ED229E721100D29793 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9AF9EE0124648751005D2270; + remoteInfo = Disk; + }; 5C2229A129CCB3C400F00E69 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 9A1410ED229E721100D29793 /* Project object */; @@ -437,6 +470,7 @@ 5C044F792B3DE6F3005F6951 /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = ""; }; 5C0A2A89292A5B4D009B4C1F /* SMJobBlessUtil.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = SMJobBlessUtil.py; sourceTree = ""; }; 5C0A9CA12C467AA300EE6A89 /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; }; + 5C0A9CA32C467F7A00EE6A89 /* widget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = widget.swift; sourceTree = ""; }; 5C0E550A2B5D545A00FFF1FB /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; 5C21D80A296C7B81005BA16D /* CombinedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedView.swift; sourceTree = ""; }; 5C22299D29CCB3C400F00E69 /* Clock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Clock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -661,7 +695,11 @@ buildActionMask = 2147483647; files = ( 5CE7E78E2C318512006BC92C /* SwiftUI.framework in Frameworks */, + 5C0A9CAA2C46838A00EE6A89 /* GPU.framework in Frameworks */, 5CE7E78C2C318512006BC92C /* WidgetKit.framework in Frameworks */, + 5C0A9CB42C46839500EE6A89 /* Disk.framework in Frameworks */, + 5C0A9CA52C46838300EE6A89 /* CPU.framework in Frameworks */, + 5C0A9CAF2C46838F00EE6A89 /* RAM.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1070,6 +1108,7 @@ 5C23BC0929A0EDA300DBA990 /* portal.swift */, 9A53EBF824EAFA5200648841 /* settings.swift */, 5CF221142B1F4792006C583F /* notifications.swift */, + 5C0A9CA32C467F7A00EE6A89 /* widget.swift */, 9A90E18C24EAD2BB00471E9A /* Info.plist */, 9A90E19724EAD3B000471E9A /* config.plist */, ); @@ -1329,6 +1368,10 @@ buildRules = ( ); dependencies = ( + 5C0A9CA82C46838300EE6A89 /* PBXTargetDependency */, + 5C0A9CAD2C46838A00EE6A89 /* PBXTargetDependency */, + 5C0A9CB22C46838F00EE6A89 /* PBXTargetDependency */, + 5C0A9CB72C46839500EE6A89 /* PBXTargetDependency */, ); name = WidgetsExtension; productName = WidgetsExtension; @@ -2061,6 +2104,7 @@ 9A46C06B266D8602001A1117 /* smc.swift in Sources */, 9A90E1A324EAD66600471E9A /* reader.swift in Sources */, 9A90E19624EAD35F00471E9A /* main.swift in Sources */, + 5C0A9CA42C467F7A00EE6A89 /* widget.swift in Sources */, 5C23BC0A29A0EDA300DBA990 /* portal.swift in Sources */, 9A53EBFB24EB041E00648841 /* popup.swift in Sources */, 9A53EBF924EAFA5200648841 /* settings.swift in Sources */, @@ -2145,6 +2189,26 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 5C0A9CA82C46838300EE6A89 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A97CEC92537331B00742D8F /* CPU */; + targetProxy = 5C0A9CA72C46838300EE6A89 /* PBXContainerItemProxy */; + }; + 5C0A9CAD2C46838A00EE6A89 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A90E18824EAD2BB00471E9A /* GPU */; + targetProxy = 5C0A9CAC2C46838A00EE6A89 /* PBXContainerItemProxy */; + }; + 5C0A9CB22C46838F00EE6A89 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9A81C7552449A41400825D92 /* RAM */; + targetProxy = 5C0A9CB12C46838F00EE6A89 /* PBXContainerItemProxy */; + }; + 5C0A9CB72C46839500EE6A89 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9AF9EE0124648751005D2270 /* Disk */; + targetProxy = 5C0A9CB62C46839500EE6A89 /* PBXContainerItemProxy */; + }; 5C2229A229CCB3C400F00E69 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5C22299C29CCB3C400F00E69 /* Clock */; diff --git a/Widgets/widgets.swift b/Widgets/widgets.swift index 1939556a..2f81fac0 100644 --- a/Widgets/widgets.swift +++ b/Widgets/widgets.swift @@ -12,6 +12,7 @@ import SwiftUI import CPU +import GPU import RAM import Disk @@ -19,6 +20,7 @@ import Disk struct WidgetsBundle: WidgetBundle { var body: some Widget { CPUWidget() + GPUWidget() RAMWidget() DiskWidget() }