mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
feat: added a new United widget that visualizes: CPU, GPU, RAM, and disk utilization
This commit is contained in:
@@ -117,6 +117,7 @@ open class Module {
|
|||||||
self.menuBar = MenuBar(moduleName: self.config.name)
|
self.menuBar = MenuBar(moduleName: self.config.name)
|
||||||
self.available = self.isAvailable()
|
self.available = self.isAvailable()
|
||||||
self.enabled = Store.shared.bool(key: "\(self.config.name)_state", defaultValue: self.config.defaultState)
|
self.enabled = Store.shared.bool(key: "\(self.config.name)_state", defaultValue: self.config.defaultState)
|
||||||
|
self.userDefaults?.set(self.enabled, forKey: "\(self.config.name)_state")
|
||||||
|
|
||||||
if !self.available {
|
if !self.available {
|
||||||
debug("Module is not available", log: self.log)
|
debug("Module is not available", log: self.log)
|
||||||
@@ -195,6 +196,7 @@ open class Module {
|
|||||||
|
|
||||||
self.enabled = true
|
self.enabled = true
|
||||||
Store.shared.set(key: "\(self.config.name)_state", value: true)
|
Store.shared.set(key: "\(self.config.name)_state", value: true)
|
||||||
|
self.userDefaults?.set(true, forKey: "\(self.config.name)_state")
|
||||||
self.readers.forEach { (reader: Reader_p) in
|
self.readers.forEach { (reader: Reader_p) in
|
||||||
reader.initStoreValues(title: self.config.name)
|
reader.initStoreValues(title: self.config.name)
|
||||||
reader.start()
|
reader.start()
|
||||||
@@ -211,6 +213,7 @@ open class Module {
|
|||||||
self.enabled = false
|
self.enabled = false
|
||||||
if !self.pauseState { // omit saving the disable state when toggle by pause, need for resume state restoration
|
if !self.pauseState { // omit saving the disable state when toggle by pause, need for resume state restoration
|
||||||
Store.shared.set(key: "\(self.config.name)_state", value: false)
|
Store.shared.set(key: "\(self.config.name)_state", value: false)
|
||||||
|
self.userDefaults?.set(false, forKey: "\(self.config.name)_state")
|
||||||
}
|
}
|
||||||
self.readers.forEach{ $0.stop() }
|
self.readers.forEach{ $0.stop() }
|
||||||
self.menuBar.disable()
|
self.menuBar.disable()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Kit
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
|
|
||||||
public struct CPU_Load: Codable, RemoteType {
|
public struct CPU_Load: Codable, RemoteType {
|
||||||
var totalUsage: Double = 0
|
public var totalUsage: Double = 0
|
||||||
var usagePerCore: [Double] = []
|
var usagePerCore: [Double] = []
|
||||||
var usageECores: Double? = nil
|
var usageECores: Double? = nil
|
||||||
var usagePCores: Double? = nil
|
var usagePCores: Double? = nil
|
||||||
@@ -226,6 +226,7 @@ public class CPU: Module {
|
|||||||
guard let blobData = try? JSONEncoder().encode(value) else { return }
|
guard let blobData = try? JSONEncoder().encode(value) else { return }
|
||||||
self.userDefaults?.set(blobData, forKey: "CPU@LoadReader")
|
self.userDefaults?.set(blobData, forKey: "CPU@LoadReader")
|
||||||
WidgetCenter.shared.reloadTimelines(ofKind: CPU_entry.kind)
|
WidgetCenter.shared.reloadTimelines(ofKind: CPU_entry.kind)
|
||||||
|
WidgetCenter.shared.reloadTimelines(ofKind: "UnitedWidget")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public struct drive: Codable {
|
|||||||
var activity: stats = stats()
|
var activity: stats = stats()
|
||||||
var smart: smart_t? = nil
|
var smart: smart_t? = nil
|
||||||
|
|
||||||
var percentage: Double {
|
public var percentage: Double {
|
||||||
let total = self.size
|
let total = self.size
|
||||||
let free = self.free
|
let free = self.free
|
||||||
var usedSpace = total - free
|
var usedSpace = total - free
|
||||||
@@ -333,6 +333,7 @@ public class Disk: Module {
|
|||||||
guard let blobData = try? JSONEncoder().encode(d) else { return }
|
guard let blobData = try? JSONEncoder().encode(d) else { return }
|
||||||
self.userDefaults?.set(blobData, forKey: "Disk@CapacityReader")
|
self.userDefaults?.set(blobData, forKey: "Disk@CapacityReader")
|
||||||
WidgetCenter.shared.reloadTimelines(ofKind: Disk_entry.kind)
|
WidgetCenter.shared.reloadTimelines(ofKind: Disk_entry.kind)
|
||||||
|
WidgetCenter.shared.reloadTimelines(ofKind: "UnitedWidget")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ public class GPU: Module {
|
|||||||
guard let blobData = try? JSONEncoder().encode(selectedGPU) else { return }
|
guard let blobData = try? JSONEncoder().encode(selectedGPU) else { return }
|
||||||
self.userDefaults?.set(blobData, forKey: "GPU@InfoReader")
|
self.userDefaults?.set(blobData, forKey: "GPU@InfoReader")
|
||||||
WidgetCenter.shared.reloadTimelines(ofKind: GPU_entry.kind)
|
WidgetCenter.shared.reloadTimelines(ofKind: GPU_entry.kind)
|
||||||
|
WidgetCenter.shared.reloadTimelines(ofKind: "UnitedWidget")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ public class RAM: Module {
|
|||||||
guard let blobData = try? JSONEncoder().encode(value) else { return }
|
guard let blobData = try? JSONEncoder().encode(value) else { return }
|
||||||
self.userDefaults?.set(blobData, forKey: "RAM@UsageReader")
|
self.userDefaults?.set(blobData, forKey: "RAM@UsageReader")
|
||||||
WidgetCenter.shared.reloadTimelines(ofKind: RAM_entry.kind)
|
WidgetCenter.shared.reloadTimelines(ofKind: RAM_entry.kind)
|
||||||
|
WidgetCenter.shared.reloadTimelines(ofKind: "UnitedWidget")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
5CFE493D2926513E000F2856 /* eu.exelban.Stats.SMC.Helper in Copy Files */ = {isa = PBXBuildFile; fileRef = 5CFE492729264DF1000F2856 /* eu.exelban.Stats.SMC.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
5CFE493D2926513E000F2856 /* eu.exelban.Stats.SMC.Helper in Copy Files */ = {isa = PBXBuildFile; fileRef = 5CFE492729264DF1000F2856 /* eu.exelban.Stats.SMC.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
|
||||||
5CFE494229265418000F2856 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494129265418000F2856 /* uninstall.sh */; };
|
5CFE494229265418000F2856 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494129265418000F2856 /* uninstall.sh */; };
|
||||||
5CFE494429265421000F2856 /* changelog.py in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494329265421000F2856 /* changelog.py */; };
|
5CFE494429265421000F2856 /* changelog.py in Resources */ = {isa = PBXBuildFile; fileRef = 5CFE494329265421000F2856 /* changelog.py */; };
|
||||||
|
5EC1B2E52E2FEAFB007042A6 /* UnitedWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EC1B2E42E2FEAFB007042A6 /* UnitedWidget.swift */; };
|
||||||
5EE8037F29C36BDD0063D37D /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE8037E29C36BDD0063D37D /* portal.swift */; };
|
5EE8037F29C36BDD0063D37D /* portal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EE8037E29C36BDD0063D37D /* portal.swift */; };
|
||||||
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; };
|
9A045EB72594F8D100ED58F2 /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A045EB62594F8D100ED58F2 /* Dashboard.swift */; };
|
||||||
9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; };
|
9A11AAD6266FD77F000C1C05 /* Bluetooth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A11AACF266FD77F000C1C05 /* Bluetooth.framework */; };
|
||||||
@@ -570,6 +571,7 @@
|
|||||||
5CFE493B292650F8000F2856 /* Launchd.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Launchd.plist; sourceTree = "<group>"; };
|
5CFE493B292650F8000F2856 /* Launchd.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Launchd.plist; sourceTree = "<group>"; };
|
||||||
5CFE494129265418000F2856 /* uninstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = uninstall.sh; sourceTree = "<group>"; };
|
5CFE494129265418000F2856 /* uninstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = uninstall.sh; sourceTree = "<group>"; };
|
||||||
5CFE494329265421000F2856 /* changelog.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = changelog.py; sourceTree = "<group>"; };
|
5CFE494329265421000F2856 /* changelog.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = changelog.py; sourceTree = "<group>"; };
|
||||||
|
5EC1B2E42E2FEAFB007042A6 /* UnitedWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitedWidget.swift; sourceTree = "<group>"; };
|
||||||
5EE8037E29C36BDD0063D37D /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
|
5EE8037E29C36BDD0063D37D /* portal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = portal.swift; sourceTree = "<group>"; };
|
||||||
62BA5F74254810C8009D0AC2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
62BA5F74254810C8009D0AC2 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
63A07F97275018DF00352C46 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
|
63A07F97275018DF00352C46 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
@@ -923,6 +925,7 @@
|
|||||||
children = (
|
children = (
|
||||||
5C3068752C3351D800B05EFA /* Supporting Files */,
|
5C3068752C3351D800B05EFA /* Supporting Files */,
|
||||||
5CE7E7A32C318C33006BC92C /* widgets.swift */,
|
5CE7E7A32C318C33006BC92C /* widgets.swift */,
|
||||||
|
5EC1B2E42E2FEAFB007042A6 /* UnitedWidget.swift */,
|
||||||
);
|
);
|
||||||
path = Widgets;
|
path = Widgets;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -2030,6 +2033,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
5EC1B2E52E2FEAFB007042A6 /* UnitedWidget.swift in Sources */,
|
||||||
5CE7E7A42C318C33006BC92C /* widgets.swift in Sources */,
|
5CE7E7A42C318C33006BC92C /* widgets.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|||||||
147
Widgets/UnitedWidget.swift
Normal file
147
Widgets/UnitedWidget.swift
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
//
|
||||||
|
// UnitedWidget.swift
|
||||||
|
// WidgetsExtension
|
||||||
|
//
|
||||||
|
// Created by Serhiy Mytrovtsiy on 22/07/2025.
|
||||||
|
// Copyright © 2025 Serhiy Mytrovtsiy. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
|
import CPU
|
||||||
|
import GPU
|
||||||
|
import RAM
|
||||||
|
import Disk
|
||||||
|
|
||||||
|
public struct Value {
|
||||||
|
public var value: Double = 0
|
||||||
|
public var color: Color = Color(nsColor: .controlAccentColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct United_entry: TimelineEntry {
|
||||||
|
public static let kind = "UnitedWidget"
|
||||||
|
public static var snapshot: United_entry = United_entry()
|
||||||
|
|
||||||
|
public var date: Date {
|
||||||
|
Calendar.current.date(byAdding: .second, value: 5, to: Date())!
|
||||||
|
}
|
||||||
|
|
||||||
|
public var cpu: Value? = nil
|
||||||
|
public var gpu: Value? = nil
|
||||||
|
public var ram: Value? = nil
|
||||||
|
public var disk: Value? = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(macOS 11.0, *)
|
||||||
|
public struct Provider: TimelineProvider {
|
||||||
|
public typealias Entry = United_entry
|
||||||
|
|
||||||
|
private let userDefaults: UserDefaults? = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")
|
||||||
|
|
||||||
|
public func placeholder(in context: Context) -> United_entry {
|
||||||
|
United_entry()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getSnapshot(in context: Context, completion: @escaping (United_entry) -> Void) {
|
||||||
|
completion(United_entry.snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getTimeline(in context: Context, completion: @escaping (Timeline<United_entry>) -> Void) {
|
||||||
|
var entry = United_entry()
|
||||||
|
if let raw = userDefaults?.data(forKey: "CPU@LoadReader"), let value = try? JSONDecoder().decode(CPU_Load.self, from: raw) {
|
||||||
|
entry.cpu = Value(value: value.totalUsage)
|
||||||
|
}
|
||||||
|
if let raw = userDefaults?.bool(forKey: "CPU_state"), !raw {
|
||||||
|
entry.cpu = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let raw = userDefaults?.data(forKey: "GPU@InfoReader"), let value = try? JSONDecoder().decode(GPU_Info.self, from: raw) {
|
||||||
|
entry.gpu = Value(value: value.utilization ?? 0)
|
||||||
|
}
|
||||||
|
if let raw = userDefaults?.bool(forKey: "GPU_state"), !raw {
|
||||||
|
entry.gpu = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let raw = userDefaults?.data(forKey: "RAM@UsageReader"), let value = try? JSONDecoder().decode(RAM_Usage.self, from: raw) {
|
||||||
|
entry.ram = Value(value: value.usage)
|
||||||
|
}
|
||||||
|
if let raw = userDefaults?.bool(forKey: "RAM_state"), !raw {
|
||||||
|
entry.ram = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let raw = userDefaults?.data(forKey: "Disk@CapacityReader"), let value = try? JSONDecoder().decode(drive.self, from: raw) {
|
||||||
|
entry.disk = Value(value: value.percentage)
|
||||||
|
}
|
||||||
|
if let raw = userDefaults?.bool(forKey: "Disk_state"), !raw {
|
||||||
|
entry.disk = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries: [United_entry] = [entry]
|
||||||
|
completion(Timeline(entries: entries, policy: .atEnd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(macOS 14.0, *)
|
||||||
|
public struct UnitedWidget: Widget {
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
let columns = [GridItem(.flexible()), GridItem(.flexible())]
|
||||||
|
|
||||||
|
public var body: some WidgetConfiguration {
|
||||||
|
StaticConfiguration(kind: United_entry.kind, provider: Provider()) { entry in
|
||||||
|
let values: [(String, Double, Color)] = [
|
||||||
|
entry.cpu.map { ("CPU", $0.value, $0.color) },
|
||||||
|
entry.gpu.map { ("GPU", $0.value, $0.color) },
|
||||||
|
entry.ram.map { ("RAM", $0.value, $0.color) },
|
||||||
|
entry.disk.map { ("Disk", $0.value, $0.color) }
|
||||||
|
].compactMap { $0 }
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
if values.isEmpty {
|
||||||
|
Text("No data available")
|
||||||
|
} else {
|
||||||
|
LazyVGrid(columns: columns, alignment: .leading, spacing: 12) {
|
||||||
|
ForEach(values.indices, id: \.self) { index in
|
||||||
|
let item = values[index]
|
||||||
|
CircularGaugeView(title: item.0, progress: item.1, color: item.2)
|
||||||
|
}
|
||||||
|
ForEach(values.count..<4, id: \.self) { _ in
|
||||||
|
Color.clear
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.containerBackground(for: .widget) {
|
||||||
|
Color.clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.configurationDisplayName("United widget")
|
||||||
|
.description("Displays CPU/GPU/RAM/Disk stats")
|
||||||
|
.supportedFamilies([.systemSmall])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CircularGaugeView: View {
|
||||||
|
var title: String
|
||||||
|
var progress: Double
|
||||||
|
var color: Color
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Circle().stroke(Color.gray.opacity(0.2), lineWidth: 6)
|
||||||
|
Circle()
|
||||||
|
.trim(from: 0, to: self.progress)
|
||||||
|
.stroke(self.color, style: StrokeStyle(lineWidth: 6, lineCap: .round))
|
||||||
|
.rotationEffect(.degrees(-90))
|
||||||
|
.animation(.easeInOut(duration: 0.3), value: self.progress)
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Text(self.title).font(.system(size: 10))
|
||||||
|
Text("\(Int(self.progress * 100))%").font(.system(size: 12))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,5 +25,6 @@ struct WidgetsBundle: WidgetBundle {
|
|||||||
RAMWidget()
|
RAMWidget()
|
||||||
DiskWidget()
|
DiskWidget()
|
||||||
NetworkWidget()
|
NetworkWidget()
|
||||||
|
UnitedWidget()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user