Merge pull request #50 from Ebullioscopic/feature/stats

Each and every stats is working
This commit is contained in:
Hariharan Mudaliar 2025-10-19 16:31:15 +05:30 • committed by GitHub
commit 6595266f4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 4274 additions and 381 deletions

3
.gitignore vendored
View file

@ -142,6 +142,7 @@ SHEET_PRESENTATION_FIX.md
lock_mode_reference.md
liquid_glass.md
huds.md
borrow_migrations.md
borrowing/
MoveToSky.xcodeproj
/MoveToSky/

View file

@ -40,11 +40,8 @@
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<PathRunnable
runnableDebuggingMode = "0"
FilePath = "/Applications/DynamicIsland.app">
</PathRunnable>
<MacroExpansion>
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F54406E62CD4987A00C7AFD9"
@ -52,7 +49,7 @@
BlueprintName = "DynamicIsland"
ReferencedContainer = "container:DynamicIsland.xcodeproj">
</BuildableReference>
</MacroExpansion>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View file

@ -36,35 +36,20 @@ struct ContentView: View {
// Dynamic sizing based on view type and graph count with smooth transitions
var dynamicNotchSize: CGSize {
// Use minimalistic or normal size based on settings
let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
// Always return the target size for the current view to ensure smooth transitions
let targetSize: CGSize
if coordinator.currentView == .stats {
// Count enabled graphs using the same logic as NotchStatsView
var enabledGraphsCount = 0
if showCpuGraph { enabledGraphsCount += 1 }
if showMemoryGraph { enabledGraphsCount += 1 }
if showGpuGraph { enabledGraphsCount += 1 }
if showNetworkGraph { enabledGraphsCount += 1 }
if showDiskGraph { enabledGraphsCount += 1 }
if enabledGraphsCount > 3 {
// Expand height for 4+ graphs
targetSize = CGSize(
width: baseSize.width,
height: baseSize.height + 65 // Add extra height for second row
)
} else {
targetSize = baseSize
}
} else {
targetSize = baseSize
guard coordinator.currentView == .stats else {
return baseSize
}
return targetSize
let rows = statsRowCount()
if rows <= 1 {
return baseSize
}
let additionalRows = max(rows - 1, 0)
let extraHeight = CGFloat(additionalRows) * statsAdditionalRowHeight
return CGSize(width: baseSize.width, height: baseSize.height + extraHeight)
}
@ -96,6 +81,7 @@ struct ContentView: View {
private let extendedHoverPadding: CGFloat = 30
private let zeroHeightHoverPadding: CGFloat = 10
private let statsAdditionalRowHeight: CGFloat = 110
// Use minimalistic corner radius ONLY when opened, keep normal when closed
private var activeCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) {
@ -266,10 +252,12 @@ struct ContentView: View {
isViewTransitioning = false
// Check if this transition will cause a size change
let baseOpenSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
let expandedHeight = baseOpenSize.height + statsAdditionalRowHeight
let oldSize = oldValue == .stats && statsTabHasExpandedHeight() ?
CGSize(width: openNotchSize.width, height: openNotchSize.height + 65) : openNotchSize
CGSize(width: baseOpenSize.width, height: expandedHeight) : baseOpenSize
let newSize = newValue == .stats && statsTabHasExpandedHeight() ?
CGSize(width: openNotchSize.width, height: openNotchSize.height + 65) : openNotchSize
CGSize(width: baseOpenSize.width, height: expandedHeight) : baseOpenSize
let sizeWillChange = oldSize != newSize
// Set flags for transition tracking
@ -694,7 +682,7 @@ struct ContentView: View {
self.isHovering = false
}
if self.vm.notchState == .open && !self.vm.isBatteryPopoverActive {
if self.vm.notchState == .open && !self.hasAnyActivePopovers() {
self.vm.close()
}
}
@ -780,13 +768,23 @@ struct ContentView: View {
// Helper to check if stats tab has 4+ graphs (needs expanded height)
private func statsTabHasExpandedHeight() -> Bool {
return statsRowCount() > 1
}
private func enabledStatsGraphCount() -> Int {
var enabledCount = 0
if showCpuGraph { enabledCount += 1 }
if showMemoryGraph { enabledCount += 1 }
if showGpuGraph { enabledCount += 1 }
if showNetworkGraph { enabledCount += 1 }
if showDiskGraph { enabledCount += 1 }
return enabledCount > 3
return enabledCount
}
private func statsRowCount() -> Int {
let count = enabledStatsGraphCount()
if count == 0 { return 0 }
return count <= 3 ? 1 : 2
}
// MARK: - Gesture Handling

View file

@ -205,6 +205,10 @@
"comment" : "A label displaying the duration of the current song. The argument is the duration of the song in seconds.",
"isCommentAutoGenerated" : true
},
"—" : {
"comment" : "A placeholder text used when GPU usage is not available.",
"isCommentAutoGenerated" : true
},
"'Now Playing' was the only option on previous versions and works with all media apps." : {
"localizations" : {
"ar" : {
@ -817,10 +821,6 @@
},
"%lld colors" : {
},
"%lld processes" : {
"comment" : "A label at the bottom of the ranked processes popover showing the total number of processes being monitored.",
"isCommentAutoGenerated" : true
},
"%lld%%" : {
"localizations" : {
@ -3053,6 +3053,10 @@
}
}
},
"Averages" : {
"comment" : "A label displayed above the average usage and last update information for a GPU.",
"isCommentAutoGenerated" : true
},
"Battery" : {
"localizations" : {
"ar" : {
@ -3572,6 +3576,10 @@
},
"Bottom Pad:" : {
},
"Breakdown" : {
"comment" : "A label displayed above the breakdown of memory usage.",
"isCommentAutoGenerated" : true
},
"Brightness HUD" : {
@ -4828,6 +4836,10 @@
},
"Copy something to start" : {
},
"Core %lld" : {
"comment" : "A label showing the ID of a CPU core. The argument is the ID of the CPU core.",
"isCommentAutoGenerated" : true
},
"CPU Usage" : {
@ -6795,6 +6807,10 @@
},
"Enable timer live activity" : {
},
"Engine Utilization" : {
"comment" : "A label describing the utilization of the GPU's engine components.",
"isCommentAutoGenerated" : true
},
"Enhance your experience with HUDs" : {
"localizations" : {
@ -7312,6 +7328,10 @@
}
}
},
"External" : {
"comment" : "A label indicating an external storage device.",
"isCommentAutoGenerated" : true
},
"Fill Notch" : {
},
@ -7417,6 +7437,10 @@
},
"Fit to Notch" : {
},
"Free · %@" : {
"comment" : "A label displaying the amount of free storage on a disk device. The argument is the number of free bytes on the disk.",
"isCommentAutoGenerated" : true
},
"Gemini API Key" : {
@ -8749,6 +8773,10 @@
}
}
},
"Idle" : {
"comment" : "A label displayed alongside the status of a GPU device.",
"isCommentAutoGenerated" : true
},
"Idle Animation Style" : {
"comment" : "The header label for the section that lets users choose the style of idle animation.",
"isCommentAutoGenerated" : true
@ -9078,6 +9106,14 @@
}
}
},
"IPv4 · %@" : {
"comment" : "A label displaying an IP address for a network interface. The text inside the parentheses is the IP address of the interface.",
"isCommentAutoGenerated" : true
},
"IPv6 · %@" : {
"comment" : "A label displaying an IPv6 address associated with a network interface. The text inside the parentheses is replaced with the actual IPv6 address.",
"isCommentAutoGenerated" : true
},
"Keyboard Backlight HUD" : {
},
@ -9201,6 +9237,10 @@
},
"Live Performance Data" : {
},
"Load Average" : {
"comment" : "A label displayed above the load average values.",
"isCommentAutoGenerated" : true
},
"Lock Screen" : {
@ -11765,6 +11805,10 @@
}
}
},
"No active interfaces detected yet." : {
"comment" : "A message displayed when there are no active network interfaces to display.",
"isCommentAutoGenerated" : true
},
"No clipboard history" : {
},
@ -12198,11 +12242,19 @@
"comment" : "A label displayed when no graphs are enabled in the Stats tab.",
"isCommentAutoGenerated" : true
},
"No process data yet. Keep stats open for a moment." : {
"comment" : "A message displayed when there is no process data available to display in the CPU stats view. It encourages the user to keep the stats open for a moment to gather more data.",
"isCommentAutoGenerated" : true
},
"No results" : {
},
"No results found" : {
},
"No storage devices detected yet." : {
"comment" : "A message indicating that no storage devices have been detected yet.",
"isCommentAutoGenerated" : true
},
"Non-notch display height" : {
"localizations" : {
@ -12997,10 +13049,6 @@
},
"Picking Status" : {
},
"PID: %d" : {
"comment" : "A small label displaying the process ID of a particular app. The argument is the process ID of the app.",
"isCommentAutoGenerated" : true
},
"Pin items from history to save them here" : {
@ -13128,6 +13176,10 @@
},
"Position" : {
},
"Pressure" : {
"comment" : "A label describing the memory pressure status.",
"isCommentAutoGenerated" : true
},
"Preview" : {
@ -13470,6 +13522,10 @@
}
}
},
"Read" : {
"comment" : "A heading displayed for the user's disk read speed.",
"isCommentAutoGenerated" : true
},
"Ready" : {
},
@ -13488,10 +13544,6 @@
},
"Recording Status" : {
},
"Refresh" : {
"comment" : "A button to refresh the displayed data.",
"isCommentAutoGenerated" : true
},
"Release name" : {
"localizations" : {
@ -14298,6 +14350,18 @@
},
"Sensitivity" : {
},
"Session · ↓ %@ · ↑ %@" : {
"comment" : "A row displaying the total downloaded and uploaded bytes for the network session.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Session · ↓ %1$@ · ↑ %2$@"
}
}
}
},
"Set" : {
@ -16018,6 +16082,18 @@
},
"Supports reasoning mode" : {
},
"Swap" : {
"comment" : "A label for the section of the memory dashboard related to swap usage.",
"isCommentAutoGenerated" : true
},
"Swap is disabled on this system." : {
"comment" : "A message indicating that swap memory is disabled on the user's system.",
"isCommentAutoGenerated" : true
},
"System" : {
"comment" : "A label displayed above the system, load average, and uptime information.",
"isCommentAutoGenerated" : true
},
"System features" : {
"localizations" : {
@ -16452,8 +16528,8 @@
"Toggle the Dynamic Island open or closed from anywhere." : {
},
"Top Processes by %@" : {
"comment" : "A heading that announces the top processes by a specific metric. The text inside the parentheses is replaced with the actual metric name.",
"Total · %@" : {
"comment" : "A label displaying the total storage capacity of a disk device. The argument is the total storage capacity of the disk device.",
"isCommentAutoGenerated" : true
},
"Transform" : {
@ -16976,6 +17052,18 @@
}
}
},
"Upload" : {
"comment" : "A label displayed above the user's upload statistics.",
"isCommentAutoGenerated" : true
},
"Usage" : {
"comment" : "A label displayed above the CPU usage statistics.",
"isCommentAutoGenerated" : true
},
"Usage Breakdown" : {
"comment" : "A heading for the breakdown of CPU usage.",
"isCommentAutoGenerated" : true
},
"Use music visualizer spectrogram" : {
"localizations" : {
"ar" : {
@ -17076,8 +17164,20 @@
}
}
},
"Used" : {
"comment" : "A label describing the percentage of memory that is currently in use.",
"isCommentAutoGenerated" : true
},
"Used · %@" : {
"comment" : "A label showing the used space on a storage device, followed by the size of that space. The argument is the number of bytes used on the device.",
"isCommentAutoGenerated" : true
},
"Uses event-driven private API for real-time screen recording detection" : {
},
"Utilization" : {
"comment" : "A label displayed alongside the utilization percentage in the GPU detail view.",
"isCommentAutoGenerated" : true
},
"Version" : {
"localizations" : {
@ -17599,6 +17699,10 @@
},
"With %lld graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row." : {
},
"Write" : {
"comment" : "A label for the write section of the disk overview.",
"isCommentAutoGenerated" : true
},
"You" : {

View file

@ -56,10 +56,16 @@ struct NotchStatsView: View {
@State private var showingCPUPopover = false
@State private var showingMemoryPopover = false
@State private var showingGPUPopover = false
@State private var showingNetworkPopover = false
@State private var showingDiskPopover = false
@State private var isHoveringCPUPopover = false
@State private var isHoveringMemoryPopover = false
@State private var isHoveringGPUPopover = false
@State private var isHoveringNetworkPopover = false
@State private var isHoveringDiskPopover = false
@State private var isResizingForStats = false
@State private var statsHoverGraceActive = false
@State private var statsHoverGraceWorkItem: DispatchWorkItem?
@EnvironmentObject var vm: DynamicIslandViewModel
var availableGraphs: [GraphData] {
@ -181,8 +187,7 @@ struct NotchStatsView: View {
private func graphViewForIndex(_ index: Int) -> some View {
let graphData = availableGraphs[index]
// Only make CPU, Memory, GPU clickable for process popovers
if graphData.title == "CPU" || graphData.title == "Memory" || graphData.title == "GPU" {
if let rankingType = rankingTypeForGraph(graphData) {
Button(action: {
handleGraphClick(for: graphData)
}) {
@ -191,7 +196,7 @@ struct NotchStatsView: View {
.buttonStyle(PlainButtonStyle())
.popover(isPresented: bindingForGraph(graphData)) {
RankedProcessPopover(
rankingType: rankingTypeForGraph(graphData),
rankingType: rankingType,
onHoverChange: { hovering in
switch graphData.title {
case "CPU":
@ -200,13 +205,16 @@ struct NotchStatsView: View {
isHoveringMemoryPopover = hovering
case "GPU":
isHoveringGPUPopover = hovering
case "Network":
isHoveringNetworkPopover = hovering
case "Disk":
isHoveringDiskPopover = hovering
default:
break
}
}
)
.onDisappear {
// Reset hover states when popover disappears
switch graphData.title {
case "CPU":
isHoveringCPUPopover = false
@ -214,10 +222,13 @@ struct NotchStatsView: View {
isHoveringMemoryPopover = false
case "GPU":
isHoveringGPUPopover = false
case "Network":
isHoveringNetworkPopover = false
case "Disk":
isHoveringDiskPopover = false
default:
break
}
// Ensure popover state is updated when popover disappears
DispatchQueue.main.async {
updateStatsPopoverState()
}
@ -228,7 +239,6 @@ struct NotchStatsView: View {
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
))
} else {
// Network and Disk graphs are not clickable
UnifiedStatsCard(graphData: graphData)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
@ -245,6 +255,10 @@ struct NotchStatsView: View {
showingMemoryPopover = true
case "GPU":
showingGPUPopover = true
case "Network":
showingNetworkPopover = true
case "Disk":
showingDiskPopover = true
default:
break
}
@ -258,12 +272,16 @@ struct NotchStatsView: View {
return $showingMemoryPopover
case "GPU":
return $showingGPUPopover
case "Network":
return $showingNetworkPopover
case "Disk":
return $showingDiskPopover
default:
return .constant(false)
}
}
private func rankingTypeForGraph(_ graphData: GraphData) -> ProcessRankingType {
private func rankingTypeForGraph(_ graphData: GraphData) -> ProcessRankingType? {
switch graphData.title {
case "CPU":
return .cpu
@ -271,8 +289,12 @@ struct NotchStatsView: View {
return .memory
case "GPU":
return .gpu
case "Network":
return .network
case "Disk":
return .disk
default:
return .cpu
return nil
}
}
@ -323,7 +345,6 @@ struct NotchStatsView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
} else {
// Stats content with smart grid layout
VStack(spacing: 8) {
statsGridLayout
}
@ -345,24 +366,61 @@ struct NotchStatsView: View {
}
}
.onDisappear {
statsHoverGraceWorkItem?.cancel()
statsHoverGraceWorkItem = nil
if statsHoverGraceActive {
statsHoverGraceActive = false
}
// Keep monitoring running when tab is not visible
updateStatsPopoverState()
}
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
.onChange(of: availableGraphs.count) { _, newCount in
.onChange(of: availableGraphs.count) { _, _ in
// Protect against hover interference during dynamic sizing
isResizingForStats = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
isResizingForStats = false
}
}
.onChange(of: showingCPUPopover) { _, _ in
.onChange(of: showingCPUPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState()
}
.onChange(of: showingMemoryPopover) { _, _ in
.onChange(of: showingMemoryPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState()
}
.onChange(of: showingGPUPopover) { _, _ in
.onChange(of: showingGPUPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState()
}
.onChange(of: showingNetworkPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState()
}
.onChange(of: showingDiskPopover) { _, newValue in
if newValue {
activateStatsHoverGrace()
} else {
deactivateStatsHoverGrace()
}
updateStatsPopoverState()
}
.onChange(of: isHoveringCPUPopover) { _, _ in
@ -374,23 +432,48 @@ struct NotchStatsView: View {
.onChange(of: isHoveringGPUPopover) { _, _ in
updateStatsPopoverState()
}
.onChange(of: isHoveringNetworkPopover) { _, _ in
updateStatsPopoverState()
}
.onChange(of: isHoveringDiskPopover) { _, _ in
updateStatsPopoverState()
}
}
private func activateStatsHoverGrace(duration: TimeInterval = 0.45) {
statsHoverGraceWorkItem?.cancel()
statsHoverGraceActive = true
let workItem = DispatchWorkItem {
statsHoverGraceActive = false
statsHoverGraceWorkItem = nil
updateStatsPopoverState()
}
statsHoverGraceWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: workItem)
}
private func deactivateStatsHoverGrace() {
statsHoverGraceWorkItem?.cancel()
statsHoverGraceWorkItem = nil
if statsHoverGraceActive {
statsHoverGraceActive = false
}
}
private func updateStatsPopoverState() {
// Use the same logic as battery popover: active only when shown AND hovered
// Also consider resize protection to prevent closing during layout changes
let newState = (showingCPUPopover && isHoveringCPUPopover) ||
(showingMemoryPopover && isHoveringMemoryPopover) ||
(showingGPUPopover && isHoveringGPUPopover) ||
isResizingForStats
let anyPopoverOpen = showingCPUPopover || showingMemoryPopover || showingGPUPopover || showingNetworkPopover || showingDiskPopover
let newState = anyPopoverOpen || isResizingForStats || statsHoverGraceActive
if vm.isStatsPopoverActive != newState {
vm.isStatsPopoverActive = newState
#if DEBUG
print("📊 Stats popover state updated: \(newState)")
print(" CPU: shown=\(showingCPUPopover), hovering=\(isHoveringCPUPopover)")
print(" Memory: shown=\(showingMemoryPopover), hovering=\(isHoveringMemoryPopover)")
print(" GPU: shown=\(showingGPUPopover), hovering=\(isHoveringGPUPopover)")
print(" CPU open=\(showingCPUPopover)")
print(" Memory open=\(showingMemoryPopover)")
print(" GPU open=\(showingGPUPopover)")
print(" Network open=\(showingNetworkPopover)")
print(" Disk open=\(showingDiskPopover)")
print(" Resizing: \(isResizingForStats)")
print(" Hover grace: \(statsHoverGraceActive)")
#endif
}
}
@ -460,8 +543,8 @@ struct UnifiedStatsCard: View {
}
.frame(height: 36) // Match boring.notch exactly - reduced from 50
// Click hint - only for CPU, Memory, GPU
if graphData.title == "CPU" || graphData.title == "Memory" || graphData.title == "GPU" {
// Click hint - shown for graphs that open popovers
if ["CPU", "Memory", "GPU", "Network", "Disk"].contains(graphData.title) {
Text("Click for details")
.font(.caption2)
.foregroundColor(.secondary)

View file

@ -0,0 +1,57 @@
//
// CPUStatsDetailView.swift
// DynamicIsland
//
// SwiftUI port of the stats app CPU popup layout.
// Mirrors dashboard, history, detail rows, and top processes.
//
// Created by GitHub Copilot on 18/10/2025.
//
import SwiftUI
struct CPUStatsDetailView: View {
@ObservedObject private var statsManager = StatsManager.shared
@State private var topProcesses: [ProcessStats] = []
private let systemColor = Color(red: 0.94, green: 0.32, blue: 0.28)
private let userColor = Color(red: 0.27, green: 0.52, blue: 0.97)
private let idleColor = Color.gray.opacity(0.28)
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "CPU Overview") {
CPUUsageDashboard(
breakdown: statsManager.cpuBreakdown,
loadAverage: statsManager.cpuLoadAverage,
uptime: statsManager.cpuUptime,
coreCount: statsManager.cpuCoreUsage.count,
systemColor: systemColor,
userColor: userColor,
idleColor: idleColor,
temperature: statsManager.cpuTemperature,
frequency: statsManager.cpuFrequency
)
}
StatsCard(title: "Top Processes") {
CPUProcessList(processes: topProcesses, accentColor: userColor)
}
if !statsManager.cpuCoreUsage.isEmpty {
StatsCard(title: "Per-Core Usage") {
CPUCoreUsageGrid(cores: statsManager.cpuCoreUsage, accentColor: userColor)
}
}
}
.padding(16)
}
.onReceive(statsManager.$topCPUProcesses) { processes in
topProcesses = Array(processes.prefix(8))
}
.onAppear {
topProcesses = Array(statsManager.topCPUProcesses.prefix(8))
}
}
}

View file

@ -0,0 +1,545 @@
//
// CPUStatsSharedViews.swift
// DynamicIsland
//
// Shared CPU stats UI components reused across the notch stats tab
// and the legacy stats panel.
//
// Created by GitHub Copilot on 19/10/2025.
//
import SwiftUI
struct StatsCard<Content: View>: View {
let title: String
let padding: CGFloat
let background: Color
let cornerRadius: CGFloat
let content: Content
init(
title: String,
padding: CGFloat = 16,
background: Color = Color(nsColor: .windowBackgroundColor).opacity(0.65),
cornerRadius: CGFloat = 12,
@ViewBuilder content: () -> Content
) {
self.title = title
self.padding = padding
self.background = background
self.cornerRadius = cornerRadius
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
.foregroundStyle(.primary)
content
}
.padding(padding)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.fill(background)
)
.overlay(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
.stroke(Color.white.opacity(0.05), lineWidth: 0.5)
)
}
}
struct CPUUsageDashboard: View {
let breakdown: CPULoadBreakdown
let loadAverage: LoadAverage
let uptime: TimeInterval
let coreCount: Int
let systemColor: Color
let userColor: Color
let idleColor: Color
let temperature: CPUTemperatureMetrics
let frequency: CPUFrequencyMetrics?
var body: some View {
let usageSection = VStack(alignment: .leading, spacing: 12) {
Text("Usage")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: systemColor.opacity(0.95), label: "System", value: StatsFormatting.percentage(breakdown.system))
DetailRow(color: userColor.opacity(0.95), label: "User", value: StatsFormatting.percentage(breakdown.user))
DetailRow(color: idleColor.opacity(0.7), label: "Idle", value: StatsFormatting.percentage(breakdown.idle))
DetailRow(color: nil, label: "Active", value: StatsFormatting.percentage(breakdown.activeUsage))
}
.frame(maxWidth: .infinity, alignment: .leading)
let systemSection = VStack(alignment: .leading, spacing: 10) {
Text("System")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: nil, label: "Logical cores", value: "\(max(coreCount, 1))")
DetailRow(color: nil, label: "Uptime", value: StatsFormatting.abbreviatedDuration(uptime))
Divider().padding(.vertical, 4)
DetailRow(color: nil, label: "Load 1m", value: String(format: "%.2f", loadAverage.oneMinute))
DetailRow(color: nil, label: "Load 5m", value: String(format: "%.2f", loadAverage.fiveMinutes))
DetailRow(color: nil, label: "Load 15m", value: String(format: "%.2f", loadAverage.fifteenMinutes))
}
.frame(maxWidth: .infinity, alignment: .leading)
let primaryGauges = CPUSensorGaugeGroup(
breakdown: breakdown,
temperature: temperature,
frequency: frequency,
systemColor: systemColor,
userColor: userColor,
idleColor: idleColor,
usageTint: userColor,
temperatureTint: Color(red: 0.98, green: 0.53, blue: 0.18)
)
.frame(maxWidth: .infinity, alignment: .center)
return ViewThatFits(in: .horizontal) {
VStack(alignment: .leading, spacing: 20) {
primaryGauges
usageSection
Divider().padding(.vertical, 4)
systemSection
}
HStack(alignment: .top, spacing: 28) {
primaryGauges
.frame(maxWidth: .infinity, alignment: .center)
usageSection
.frame(maxWidth: .infinity, alignment: .leading)
systemSection
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
private struct CPUSensorGaugeGroup: View {
let breakdown: CPULoadBreakdown
let temperature: CPUTemperatureMetrics
let frequency: CPUFrequencyMetrics?
let systemColor: Color
let userColor: Color
let idleColor: Color
let usageTint: Color
let temperatureTint: Color
var body: some View {
HStack(alignment: .center, spacing: 18) {
CPUTemperatureGauge(temperature: temperature.celsius, tint: temperatureTint, size: 78, lineWidth: 8)
.frame(maxWidth: .infinity)
CPUSegmentDonut(
breakdown: breakdown,
systemColor: systemColor,
userColor: userColor,
idleColor: idleColor,
diameter: 118,
lineWidth: 13
)
.frame(width: 118, height: 118)
.frame(maxWidth: .infinity)
CPUFrequencyGauge(frequency: frequency, tint: usageTint, size: 78, lineWidth: 8)
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity, alignment: .center)
}
}
private struct CPUTemperatureGauge: View {
let temperature: Double?
let tint: Color
var size: CGFloat = 84
var lineWidth: CGFloat = 9
private let maxTemperature: Double = 110
var body: some View {
CircularGaugeView(
title: "Temperature",
value: normalizedValue,
tint: tint,
centerPrimaryText: centerPrimary,
subtitle: nil,
size: size,
lineWidth: lineWidth
)
}
private var normalizedValue: Double {
guard let temperature, maxTemperature > 0 else { return 0 }
return min(max(temperature / maxTemperature, 0), 1)
}
private var centerPrimary: String {
guard let temperature else { return "—" }
return String(format: "%.0f°", temperature)
}
}
private struct CPUFrequencyGauge: View {
let frequency: CPUFrequencyMetrics?
let tint: Color
var size: CGFloat = 84
var lineWidth: CGFloat = 9
var body: some View {
CircularGaugeView(
title: "Frequency",
value: normalizedValue,
tint: tint,
centerPrimaryText: centerPrimary,
centerSecondaryText: frequency != nil ? "GHz" : nil,
subtitle: subtitle,
size: size,
lineWidth: lineWidth
)
}
private var normalizedValue: Double {
guard let frequency else { return 0 }
let candidateMax = frequency.maxOverallGHz > 0 ? frequency.maxOverallGHz : frequency.overallGHz
let denominator = max(candidateMax, 1.0)
return min(max(frequency.overallGHz / denominator, 0), 1)
}
private var centerPrimary: String {
guard let frequency else { return "—" }
return String(format: "%.1f", frequency.overallGHz)
}
private var subtitle: String? {
guard let frequency else { return nil }
var components: [String] = []
if let eValue = frequency.eCoreGHz {
components.append("E \(String(format: "%.1f", eValue))")
}
if let pValue = frequency.pCoreGHz {
components.append("P \(String(format: "%.1f", pValue))")
}
return components.isEmpty ? nil : components.joined(separator: " • ")
}
}
private struct CPUSegmentDonut: View {
let breakdown: CPULoadBreakdown
let systemColor: Color
let userColor: Color
let idleColor: Color
var diameter: CGFloat = 132
var lineWidth: CGFloat = 14
var body: some View {
ZStack {
Circle()
.stroke(idleColor.opacity(0.3), lineWidth: lineWidth)
.frame(width: diameter, height: diameter)
CPUUsageRing(
breakdown: breakdown,
systemColor: systemColor,
userColor: userColor,
lineWidth: lineWidth
)
.frame(width: diameter, height: diameter)
VStack(spacing: 4) {
Text(StatsFormatting.percentage(breakdown.activeUsage))
.font(.system(size: 30, weight: .bold, design: .rounded))
Text("Active")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
}
struct CPUUsageRing: View {
let breakdown: CPULoadBreakdown
let systemColor: Color
let userColor: Color
var lineWidth: CGFloat = 14
var body: some View {
let segments = breakdown.normalizedSegments
ZStack {
if segments.system > 0 {
RingArc(start: 0, end: segments.system, color: systemColor, lineWidth: lineWidth)
}
if segments.user > 0 {
RingArc(start: segments.system, end: segments.system + segments.user, color: userColor, lineWidth: lineWidth)
}
}
}
}
struct RingArc: View {
let start: Double
let end: Double
let color: Color
var lineWidth: CGFloat = 14
var body: some View {
Circle()
.trim(from: CGFloat(start), to: CGFloat(end))
.stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
.rotationEffect(.degrees(-90))
}
}
struct LegendRow: View {
let color: Color
let title: String
let value: String
var body: some View {
HStack(spacing: 8) {
Circle()
.fill(color)
.frame(width: 10, height: 10)
Text(title)
.font(.system(size: 12, weight: .medium))
.foregroundColor(.secondary)
Spacer()
Text(value)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(.primary)
}
}
}
struct LoadAverageRow: View {
let loadAverage: LoadAverage
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Load Average")
.font(.caption)
.foregroundColor(.secondary)
HStack(spacing: 12) {
LoadAverageValue(label: "1m", value: loadAverage.oneMinute)
LoadAverageValue(label: "5m", value: loadAverage.fiveMinutes)
LoadAverageValue(label: "15m", value: loadAverage.fifteenMinutes)
}
}
}
}
struct LoadAverageValue: View {
let label: String
let value: Double
var body: some View {
VStack(spacing: 2) {
Text(label)
.font(.caption2)
.foregroundColor(.secondary)
Text(String(format: "%.2f", value))
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(.primary)
}
}
}
struct DetailRow: View {
let color: Color?
let label: String
let value: String
var body: some View {
HStack {
if let color {
Circle()
.fill(color)
.frame(width: 8, height: 8)
}
Text(label)
.font(.system(size: 12, weight: .medium))
.foregroundColor(.secondary)
.lineLimit(1)
.layoutPriority(0.5)
Spacer()
Text(value)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(.primary)
.lineLimit(1)
.minimumScaleFactor(0.85)
.layoutPriority(1)
}
}
}
struct CPUCoreUsageGrid: View {
let cores: [CPUCoreUsage]
let accentColor: Color
private var columns: [GridItem] {
[GridItem(.adaptive(minimum: 90), spacing: 14, alignment: .top)]
}
var body: some View {
LazyVGrid(columns: columns, spacing: 14) {
ForEach(cores) { core in
CPUCoreUsageCell(core: core, accentColor: accentColor)
}
}
}
}
private struct CPUCoreUsageCell: View {
let core: CPUCoreUsage
let accentColor: Color
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text("Core \(core.id + 1)")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text(StatsFormatting.percentage(core.usage))
.font(.system(size: 12, weight: .semibold, design: .monospaced))
.foregroundColor(accentColor)
}
GeometryReader { proxy in
let width = proxy.size.width
let progress = max(0, min(core.usage / 100, 1))
ZStack(alignment: .leading) {
Capsule()
.fill(Color.primary.opacity(0.08))
Capsule()
.fill(accentColor)
.frame(width: width * progress)
}
}
.frame(height: 8)
}
.padding(10)
.background(
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(Color(nsColor: .controlBackgroundColor).opacity(0.6))
)
}
}
struct CPUDetailsGrid: View {
let breakdown: CPULoadBreakdown
let loadAverage: LoadAverage
let uptime: TimeInterval
let coreCount: Int
let systemColor: Color
let userColor: Color
var showLoadAverage: Bool = true
var body: some View {
VStack(alignment: .leading, spacing: 14) {
VStack(alignment: .leading, spacing: 8) {
Text("Usage Breakdown")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: nil, label: "Total", value: StatsFormatting.percentage(breakdown.activeUsage))
DetailRow(color: systemColor, label: "System", value: StatsFormatting.percentage(breakdown.system))
DetailRow(color: userColor, label: "User", value: StatsFormatting.percentage(breakdown.user))
DetailRow(color: Color.gray.opacity(0.4), label: "Idle", value: StatsFormatting.percentage(breakdown.idle))
}
Divider().padding(.vertical, 4)
VStack(alignment: .leading, spacing: 8) {
Text("System")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: nil, label: "Logical cores", value: "\(max(coreCount, 1))")
DetailRow(color: nil, label: "Uptime", value: StatsFormatting.abbreviatedDuration(uptime))
}
if showLoadAverage {
Divider().padding(.vertical, 4)
LoadAverageRow(loadAverage: loadAverage)
}
}
}
}
struct CPUProcessList: View {
let processes: [ProcessStats]
let accentColor: Color
var displayLimit: Int? = nil
private var visibleProcesses: [ProcessStats] {
if let limit = displayLimit {
return Array(processes.prefix(limit))
}
return processes
}
var body: some View {
VStack(spacing: 8) {
if visibleProcesses.isEmpty {
Text("No process data yet. Keep stats open for a moment.")
.font(.footnote)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
ForEach(visibleProcesses) { process in
CPUProcessRow(process: process, accentColor: accentColor)
if process.id != visibleProcesses.last?.id {
Divider().padding(.leading, 32)
}
}
}
}
}
}
struct CPUProcessRow: View {
let process: ProcessStats
let accentColor: Color
var body: some View {
HStack(spacing: 12) {
iconView
.frame(width: 24, height: 24)
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.stroke(Color.white.opacity(0.08), lineWidth: 0.5)
)
VStack(alignment: .leading, spacing: 2) {
Text(process.name)
.font(.system(size: 13, weight: .semibold))
.lineLimit(1)
Text(process.memoryUsageString)
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
Text(process.cpuUsageString)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(accentColor)
}
}
private var iconView: some View {
Group {
if let icon = process.icon {
Image(nsImage: icon)
.resizable()
.scaledToFit()
} else {
ZStack {
accentColor.opacity(0.15)
Image(systemName: "app")
.font(.system(size: 12, weight: .medium))
.foregroundColor(accentColor)
}
}
}
}
}

View file

@ -0,0 +1,48 @@
import SwiftUI
struct CircularGaugeView: View {
let title: String
let value: Double
let tint: Color
let centerPrimaryText: String
var centerSecondaryText: String? = nil
var subtitle: String? = nil
var size: CGFloat = 72
var lineWidth: CGFloat = 8
var backgroundTint: Color = Color.primary.opacity(0.08)
var body: some View {
VStack(spacing: 8) {
ZStack {
Circle()
.stroke(backgroundTint, lineWidth: lineWidth)
.frame(width: size, height: size)
if value > 0 {
RingArc(start: 0, end: min(max(value, 0), 1), color: tint, lineWidth: lineWidth)
.frame(width: size, height: size)
}
VStack(spacing: 2) {
Text(centerPrimaryText)
.font(.system(size: 16, weight: .semibold, design: .rounded))
.foregroundColor(tint)
if let centerSecondaryText, !centerSecondaryText.isEmpty {
Text(centerSecondaryText)
.font(.system(size: 11, weight: .medium))
.foregroundColor(.secondary)
}
}
}
VStack(spacing: 2) {
Text(title)
.font(.caption)
.foregroundColor(.secondary)
if let subtitle, !subtitle.isEmpty {
Text(subtitle)
.font(.footnote)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
}
}
}
}

View file

@ -0,0 +1,220 @@
//
// DiskStatsDetailView.swift
// DynamicIsland
//
// Disk detail dashboard mirroring Stats app cards.
//
// Created by GitHub Copilot on 19/10/2025.
//
import SwiftUI
struct DiskStatsDetailView: View {
@ObservedObject private var statsManager = StatsManager.shared
private let readColor = Color.cyan
private let writeColor = Color.yellow
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "Disk Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
DiskOverview(
readSpeed: statsManager.diskRead,
writeSpeed: statsManager.diskWrite,
peakRead: statsManager.diskReadHistory.max() ?? 0,
peakWrite: statsManager.diskWriteHistory.max() ?? 0,
readColor: readColor,
writeColor: writeColor
)
}
StatsCard(title: "Storage Devices", padding: 16, background: cardBackground, cornerRadius: 12) {
DiskDevicesCard(devices: statsManager.diskDevices, accentColor: readColor)
}
StatsCard(title: "Session Totals", padding: 16, background: cardBackground, cornerRadius: 12) {
DiskTotalsView(totals: statsManager.diskTotals, readColor: readColor, writeColor: writeColor)
}
}
.padding(16)
}
.frame(minWidth: 360, minHeight: 340)
}
}
private struct DiskOverview: View {
let readSpeed: Double
let writeSpeed: Double
let peakRead: Double
let peakWrite: Double
let readColor: Color
let writeColor: Color
var body: some View {
HStack(alignment: .center, spacing: 24) {
VStack(alignment: .leading, spacing: 10) {
Text("Read")
.font(.headline)
.foregroundColor(readColor)
Text(StatsFormatting.mbPerSecond(readSpeed))
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundColor(.primary)
DetailRow(color: readColor.opacity(0.6), label: "Peak", value: StatsFormatting.mbPerSecond(peakRead))
}
.frame(maxWidth: .infinity, alignment: .leading)
Divider()
.frame(height: 110)
.overlay(Color.white.opacity(0.06))
VStack(alignment: .leading, spacing: 10) {
Text("Write")
.font(.headline)
.foregroundColor(writeColor)
Text(StatsFormatting.mbPerSecond(writeSpeed))
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundColor(.primary)
DetailRow(color: writeColor.opacity(0.6), label: "Peak", value: StatsFormatting.mbPerSecond(peakWrite))
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
private struct DiskTotalsView: View {
let totals: DiskTotals
let readColor: Color
let writeColor: Color
var body: some View {
VStack(spacing: 10) {
DetailRow(color: readColor.opacity(0.75), label: "Read", value: formattedBytes(totals.readMB))
DetailRow(color: writeColor.opacity(0.75), label: "Written", value: formattedBytes(totals.writtenMB))
}
}
private func formattedBytes(_ megabytes: Double) -> String {
let bytes = UInt64(megabytes * 1_048_576)
return StatsFormatting.bytes(bytes)
}
}
private struct DiskDevicesCard: View {
let devices: [DiskDeviceMetrics]
let accentColor: Color
private var sortedDevices: [DiskDeviceMetrics] {
devices.sorted { lhs, rhs in
if lhs.isRoot != rhs.isRoot {
return lhs.isRoot && !rhs.isRoot
}
if lhs.isRemovable != rhs.isRemovable {
return !lhs.isRemovable && rhs.isRemovable
}
return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
}
}
var body: some View {
if sortedDevices.isEmpty {
Text("No storage devices detected yet.")
.font(.footnote)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
VStack(spacing: 12) {
ForEach(sortedDevices) { device in
DiskDeviceRow(device: device, accentColor: accentColor)
if device.id != sortedDevices.last?.id {
Divider().padding(.leading, 36)
}
}
}
}
}
}
private struct DiskDeviceRow: View {
let device: DiskDeviceMetrics
let accentColor: Color
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 10) {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(accentColor.opacity(0.16))
.overlay(
Image(systemName: device.isRemovable ? "externaldrive" : "internaldrive")
.font(.system(size: 16, weight: .semibold))
.foregroundColor(accentColor)
)
.frame(width: 32, height: 32)
VStack(alignment: .leading, spacing: 2) {
Text(device.name)
.font(.system(size: 13, weight: .semibold))
Text(device.path.path)
.font(.caption2)
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
if device.isRoot {
Text("System")
.font(.caption2)
.foregroundColor(accentColor)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(accentColor.opacity(0.16))
.clipShape(Capsule())
} else if device.isRemovable {
Text("External")
.font(.caption2)
.foregroundColor(accentColor)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(accentColor.opacity(0.16))
.clipShape(Capsule())
}
}
DiskUsageBar(usedBytes: device.usedBytes, totalBytes: device.totalBytes, tint: accentColor)
.frame(height: 8)
HStack {
Text("Used · \(StatsFormatting.bytes(device.usedBytes))")
.font(.caption2)
.foregroundColor(.secondary)
Spacer()
Text("Free · \(StatsFormatting.bytes(device.freeBytes))")
.font(.caption2)
.foregroundColor(.secondary)
}
Text("Total · \(StatsFormatting.bytes(device.totalBytes))")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
private struct DiskUsageBar: View {
let usedBytes: UInt64
let totalBytes: UInt64
let tint: Color
var body: some View {
GeometryReader { proxy in
let width = proxy.size.width
let progress = totalBytes > 0 ? min(max(Double(usedBytes) / Double(totalBytes), 0), 1) : 0
ZStack(alignment: .leading) {
Capsule()
.fill(Color.primary.opacity(0.08))
Capsule()
.fill(tint.opacity(0.8))
.frame(width: width * progress)
}
}
}
}

View file

@ -0,0 +1,305 @@
//
// GPUStatsDetailView.swift
// DynamicIsland
//
// GPU detail dashboard inspired by the Stats app layout.
//
// Created by GitHub Copilot on 19/10/2025.
//
import SwiftUI
struct GPUStatsDetailView: View {
@ObservedObject private var statsManager = StatsManager.shared
@State private var topProcesses: [ProcessStats] = []
private let accentColor = Color.purple
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
private let processDisplayLimit = 8
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "GPU Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
GPUUsageDashboard(
usage: statsManager.gpuUsage,
breakdown: statsManager.gpuBreakdown,
averageUsage: statsManager.avgGpuUsage,
lastUpdated: statsManager.lastUpdated,
accentColor: accentColor,
primaryDevice: statsManager.gpuDevices.first
)
}
StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) {
CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit)
}
if !statsManager.gpuDevices.isEmpty {
StatsCard(title: "Devices", padding: 16, background: cardBackground, cornerRadius: 12) {
GPUDeviceList(devices: statsManager.gpuDevices, accentColor: accentColor)
}
}
}
.padding(16)
}
.frame(minWidth: 360, minHeight: 380)
.onAppear(perform: refreshProcesses)
.onReceive(statsManager.$lastUpdated) { _ in
refreshProcesses()
}
}
private func refreshProcesses() {
let processes = statsManager.getProcessesRankedByGPU()
topProcesses = Array(processes.prefix(processDisplayLimit))
}
}
private struct GPUUsageDashboard: View {
let usage: Double
let breakdown: GPUBreakdown
let averageUsage: Double
let lastUpdated: Date
let accentColor: Color
let primaryDevice: GPUDeviceMetrics?
var body: some View {
ViewThatFits(in: .horizontal) {
HStack(alignment: .top, spacing: 20) {
leftColumn
breakdownAndMeta
}
VStack(alignment: .leading, spacing: 20) {
leftColumn
breakdownAndMeta
}
}
}
private func formattedTimestamp(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .medium
return formatter.string(from: date)
}
private var leftColumn: some View {
GPUEngineGauges(
usage: usage,
render: renderEngineUtilization,
tiler: tilerUtilization,
accentColor: accentColor
)
.frame(maxWidth: .infinity, alignment: .leading)
}
private var breakdownAndMeta: some View {
ViewThatFits(in: .horizontal) {
HStack(alignment: .top, spacing: 20) {
breakdownSection
metaSection
}
VStack(alignment: .leading, spacing: 16) {
breakdownSection
Divider().padding(.vertical, 4)
metaSection
}
}
}
private var breakdownSection: some View {
VStack(alignment: .leading, spacing: 12) {
DetailRow(color: accentColor.opacity(0.9), label: "Render", value: StatsFormatting.percentage(breakdown.render))
DetailRow(color: accentColor.opacity(0.7), label: "Compute", value: StatsFormatting.percentage(breakdown.compute))
DetailRow(color: accentColor.opacity(0.55), label: "Video", value: StatsFormatting.percentage(breakdown.video))
DetailRow(color: accentColor.opacity(0.45), label: "Other", value: StatsFormatting.percentage(breakdown.other))
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var metaSection: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Averages")
.font(.caption)
.foregroundColor(.secondary)
DetailRow(color: nil, label: "Session Avg", value: StatsFormatting.percentage(averageUsage))
DetailRow(color: nil, label: "Last Update", value: formattedTimestamp(lastUpdated))
if let device = primaryDevice {
Divider().padding(.vertical, 4)
DetailRow(color: nil, label: "Active GPU", value: device.formattedVendorModel)
DetailRow(color: nil, label: "Status", value: device.isActive ? "Active" : "Idle")
DetailRow(color: nil, label: "Temperature", value: device.temperatureText)
if let cores = device.cores {
DetailRow(color: nil, label: "Cores", value: "\(cores)")
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var renderEngineUtilization: Double? {
primaryDevice?.renderUtilization ?? breakdown.render
}
private var tilerUtilization: Double? {
primaryDevice?.tilerUtilization
}
}
private struct GPUEngineGauges: View {
let usage: Double
let render: Double?
let tiler: Double?
let accentColor: Color
private let ringDiameter: CGFloat = 112
private let ringLineWidth: CGFloat = 12
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Engine Utilization")
.font(.caption)
.foregroundColor(.secondary)
HStack(alignment: .center, spacing: 18) {
EngineGaugeView(title: "Render", value: render, tint: accentColor, size: 72)
GPUUsageRing(usage: usage, accentColor: accentColor, diameter: ringDiameter, lineWidth: ringLineWidth)
EngineGaugeView(title: "Tiler", value: tiler, tint: accentColor.opacity(0.75), size: 72)
}
.frame(maxWidth: .infinity, alignment: .center)
}
}
}
private struct GPUUsageRing: View {
let usage: Double
let accentColor: Color
var diameter: CGFloat = 108
var lineWidth: CGFloat = 12
var body: some View {
ZStack {
Circle()
.stroke(accentColor.opacity(0.25), lineWidth: lineWidth)
RingArc(start: 0, end: CGFloat(min(max(usage / 100, 0), 1)), color: accentColor, lineWidth: lineWidth)
VStack(spacing: 4) {
Text(StatsFormatting.percentage(usage))
.font(.system(size: 26, weight: .bold, design: .rounded))
Text("Active")
.font(.footnote)
.foregroundColor(.secondary)
}
}
.frame(width: diameter, height: diameter)
}
}
private struct EngineGaugeView: View {
let title: String
let value: Double?
let tint: Color
var size: CGFloat = 68
var body: some View {
if let value {
CircularGaugeView(
title: title,
value: min(max(value / 100, 0), 1),
tint: tint,
centerPrimaryText: StatsFormatting.percentage(value),
centerSecondaryText: nil,
subtitle: nil,
size: size,
lineWidth: 7,
backgroundTint: tint.opacity(0.18)
)
} else {
placeholder
}
}
private var placeholder: some View {
VStack(spacing: 8) {
ZStack {
Circle()
.stroke(Color.primary.opacity(0.08), lineWidth: 7)
.frame(width: size, height: size)
Text("—")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(.secondary)
}
Text(title)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
private struct GPUDeviceList: View {
let devices: [GPUDeviceMetrics]
let accentColor: Color
var body: some View {
VStack(spacing: 16) {
ForEach(Array(devices.enumerated()), id: \.element.id) { index, device in
GPUDeviceRow(device: device, accentColor: accentColor)
if index < devices.count - 1 {
Divider()
}
}
}
}
}
private struct GPUDeviceRow: View {
let device: GPUDeviceMetrics
let accentColor: Color
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 12) {
Circle()
.fill(device.isActive ? Color.green.opacity(0.85) : Color.red.opacity(0.7))
.frame(width: 8, height: 8)
VStack(alignment: .leading, spacing: 2) {
Text(device.formattedVendorModel)
.font(.system(size: 13, weight: .semibold))
Text(device.isActive ? "Active" : "Idle")
.font(.caption2)
.foregroundColor(device.isActive ? .green : .secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(device.utilizationText)
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.foregroundColor(accentColor)
Text("Utilization")
.font(.caption2)
.foregroundColor(.secondary)
}
}
VStack(spacing: 8) {
if let render = device.renderUtilization {
DetailRow(color: accentColor.opacity(0.9), label: "Renderer", value: StatsFormatting.percentage(render))
}
if let tiler = device.tilerUtilization {
DetailRow(color: accentColor.opacity(0.6), label: "Tiler", value: StatsFormatting.percentage(tiler))
}
if let temp = device.temperature {
DetailRow(color: nil, label: "Temperature", value: String(format: "%.0f°C", temp))
}
if let fan = device.fanSpeed {
DetailRow(color: nil, label: "Fan Speed", value: "\(fan)%")
}
if let coreClock = device.coreClock {
DetailRow(color: nil, label: "Core Clock", value: "\(coreClock) MHz")
}
if let memoryClock = device.memoryClock {
DetailRow(color: nil, label: "Memory Clock", value: "\(memoryClock) MHz")
}
if let cores = device.cores {
DetailRow(color: nil, label: "Cores", value: "\(cores)")
}
}
}
}
}

View file

@ -0,0 +1,246 @@
//
// MemoryStatsDetailView.swift
// DynamicIsland
//
// Detailed memory dashboard mirroring the Stats app layout.
//
// Created by GitHub Copilot on 19/10/2025.
//
import SwiftUI
struct MemoryStatsDetailView: View {
@ObservedObject private var statsManager = StatsManager.shared
@State private var topProcesses: [ProcessStats] = []
private let accentColor = Color.green
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
private let processDisplayLimit = 8
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "Memory Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
MemoryUsageDashboard(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
}
StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) {
CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit)
}
StatsCard(title: "Pressure & Swap", padding: 16, background: cardBackground, cornerRadius: 12) {
MemoryHealthView(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
}
}
.padding(16)
}
.frame(minWidth: 380, minHeight: 420)
.onAppear(perform: refreshProcesses)
.onReceive(statsManager.$lastUpdated) { _ in
refreshProcesses()
}
}
private func refreshProcesses() {
let processes = statsManager.getProcessesRankedByMemory()
topProcesses = Array(processes.prefix(processDisplayLimit))
}
}
private struct MemoryUsageDashboard: View {
let breakdown: MemoryBreakdown
let accentColor: Color
private let freeColor = Color.gray.opacity(0.28)
var body: some View {
let totalsSection = VStack(alignment: .leading, spacing: 12) {
DetailRow(color: accentColor.opacity(0.8), label: "Used", value: formattedBytes(breakdown.usedBytes))
DetailRow(color: freeColor.opacity(0.9), label: "Free", value: formattedBytes(breakdown.freeBytes))
DetailRow(color: nil, label: "Total", value: formattedBytes(breakdown.totalBytes))
}
.frame(maxWidth: .infinity, alignment: .leading)
let breakdownSection = VStack(alignment: .leading, spacing: 8) {
Text("Breakdown")
.font(.caption)
.foregroundColor(.secondary)
MemoryBreakdownRow(label: "App", value: breakdown.appBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.85))
MemoryBreakdownRow(label: "Cached", value: breakdown.cacheBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.65))
MemoryBreakdownRow(label: "Wired", value: breakdown.wiredBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.45))
MemoryBreakdownRow(label: "Compressed", value: breakdown.compressedBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.35))
}
.frame(maxWidth: .infinity, alignment: .leading)
return ViewThatFits(in: .horizontal) {
VStack(alignment: .leading, spacing: 20) {
usageRing
totalsSection
Divider().padding(.vertical, 4)
breakdownSection
}
HStack(alignment: .top, spacing: 28) {
usageRing
totalsSection
breakdownSection
}
}
}
private func formattedBytes(_ value: UInt64) -> String {
StatsFormatting.bytes(value)
}
private var usageRing: some View {
ZStack {
Circle()
.stroke(freeColor.opacity(0.35), lineWidth: 14)
.frame(width: 128, height: 128)
MemoryUsageRing(breakdown: breakdown, usedColor: accentColor, freeColor: freeColor)
.frame(width: 128, height: 128)
VStack(spacing: 4) {
Text(StatsFormatting.percentage(breakdown.usedPercentage))
.font(.system(size: 30, weight: .bold, design: .rounded))
Text("Used")
.font(.footnote)
.foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private struct MemoryUsageRing: View {
let breakdown: MemoryBreakdown
let usedColor: Color
let freeColor: Color
var body: some View {
ZStack {
let usedFraction = CGFloat(min(max(breakdown.usedPercentage / 100, 0), 1))
if usedFraction > 0 {
RingArc(start: 0, end: usedFraction, color: usedColor, lineWidth: 14)
}
if usedFraction < 1 {
RingArc(start: usedFraction, end: 1, color: freeColor.opacity(0.65), lineWidth: 14)
}
}
}
}
private struct MemoryBreakdownRow: View {
let label: String
let value: UInt64
let total: UInt64
let color: Color
var body: some View {
let percent = total > 0 ? Double(value) / Double(total) * 100 : 0
return DetailRow(color: color, label: label, value: "\(StatsFormatting.bytes(value)) · \(StatsFormatting.percentage(percent))")
}
}
private struct MemoryHealthView: View {
let breakdown: MemoryBreakdown
let accentColor: Color
var body: some View {
VStack(alignment: .leading, spacing: 16) {
MemoryPressureIndicator(pressure: breakdown.pressure)
Divider().padding(.vertical, 4)
SwapUsageView(swap: breakdown.swap, accentColor: accentColor)
}
}
}
private struct MemoryPressureIndicator: View {
let pressure: MemoryPressure
private var status: (title: String, color: Color, description: String) {
switch pressure.level {
case .normal:
return ("Normal", Color.green, "System memory pressure is nominal.")
case .warning:
return ("Warning", Color.orange, "Memory pressure is elevated. Closing apps may help.")
case .critical:
return ("Critical", Color.red, "Memory pressure is critical; the system may purge aggressively.")
}
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Pressure")
.font(.caption)
.foregroundColor(.secondary)
HStack {
Capsule()
.fill(status.color.opacity(0.16))
.overlay(
HStack(spacing: 8) {
Circle()
.fill(status.color)
.frame(width: 10, height: 10)
Text(status.title)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(status.color)
}
.padding(.horizontal, 12)
)
.frame(height: 28)
Spacer()
}
Text(status.description)
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
private struct SwapUsageView: View {
let swap: MemorySwap
let accentColor: Color
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Swap")
.font(.caption)
.foregroundColor(.secondary)
if swap.totalBytes > 0 {
DetailRow(color: accentColor.opacity(0.85), label: "Used", value: StatsFormatting.bytes(swap.usedBytes))
DetailRow(color: accentColor.opacity(0.55), label: "Free", value: StatsFormatting.bytes(swap.freeBytes))
DetailRow(color: nil, label: "Total", value: StatsFormatting.bytes(swap.totalBytes))
SwapUsageBar(usedBytes: swap.usedBytes, totalBytes: swap.totalBytes, tint: accentColor)
.frame(height: 8)
} else {
Text("Swap is disabled on this system.")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
}
private struct SwapUsageBar: View {
let usedBytes: UInt64
let totalBytes: UInt64
let tint: Color
var body: some View {
GeometryReader { proxy in
let width = proxy.size.width
let progress = totalBytes > 0 ? min(max(Double(usedBytes) / Double(totalBytes), 0), 1) : 0
ZStack(alignment: .leading) {
Capsule()
.fill(Color.primary.opacity(0.08))
Capsule()
.fill(tint.opacity(0.8))
.frame(width: width * progress)
}
}
}
}

View file

@ -0,0 +1,264 @@
//
// NetworkStatsDetailView.swift
// DynamicIsland
//
// Network detail dashboard with throughput history and totals.
//
// Created by GitHub Copilot on 19/10/2025.
//
import SwiftUI
struct NetworkStatsDetailView: View {
@ObservedObject private var statsManager = StatsManager.shared
private let downloadColor = Color.orange
private let uploadColor = Color.red
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
var body: some View {
ScrollView {
VStack(spacing: 16) {
StatsCard(title: "Network Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
NetworkOverview(
download: statsManager.networkDownload,
upload: statsManager.networkUpload,
avgDownload: average(statsManager.networkDownloadHistory),
avgUpload: average(statsManager.networkUploadHistory),
peakDownload: statsManager.networkDownloadHistory.max() ?? 0,
peakUpload: statsManager.networkUploadHistory.max() ?? 0,
downloadColor: downloadColor,
uploadColor: uploadColor
)
}
StatsCard(title: "Interfaces", padding: 16, background: cardBackground, cornerRadius: 12) {
NetworkInterfacesCard(
interfaces: statsManager.networkInterfaces,
downloadColor: downloadColor,
uploadColor: uploadColor
)
}
StatsCard(title: "Session Totals", padding: 16, background: cardBackground, cornerRadius: 12) {
NetworkTotalsView(totals: statsManager.networkTotals, downloadColor: downloadColor, uploadColor: uploadColor)
}
}
.padding(16)
}
.frame(minWidth: 360, minHeight: 340)
}
private func average(_ values: [Double]) -> Double {
guard !values.isEmpty else { return 0 }
let filtered = values.filter { $0 > 0 }
guard !filtered.isEmpty else { return 0 }
return filtered.reduce(0, +) / Double(filtered.count)
}
}
private struct NetworkOverview: View {
let download: Double
let upload: Double
let avgDownload: Double
let avgUpload: Double
let peakDownload: Double
let peakUpload: Double
let downloadColor: Color
let uploadColor: Color
var body: some View {
HStack(alignment: .center, spacing: 24) {
VStack(alignment: .leading, spacing: 10) {
Text("Download")
.font(.headline)
.foregroundColor(downloadColor)
Text(StatsFormatting.throughput(download))
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundColor(.primary)
DetailRow(color: downloadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgDownload))
DetailRow(color: downloadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakDownload))
}
.frame(maxWidth: .infinity, alignment: .leading)
Divider()
.frame(height: 110)
.overlay(Color.white.opacity(0.06))
VStack(alignment: .leading, spacing: 10) {
Text("Upload")
.font(.headline)
.foregroundColor(uploadColor)
Text(StatsFormatting.throughput(upload))
.font(.system(size: 26, weight: .bold, design: .rounded))
.foregroundColor(.primary)
DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.throughput(avgUpload))
DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.throughput(peakUpload))
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
private struct NetworkTotalsView: View {
let totals: NetworkTotals
let downloadColor: Color
let uploadColor: Color
var body: some View {
VStack(spacing: 10) {
DetailRow(color: downloadColor.opacity(0.75), label: "Downloaded", value: formattedBytes(totals.downloadedMB))
DetailRow(color: uploadColor.opacity(0.75), label: "Uploaded", value: formattedBytes(totals.uploadedMB))
}
}
private func formattedBytes(_ megabytes: Double) -> String {
let bytes = UInt64(megabytes * 1_048_576)
return StatsFormatting.bytes(bytes)
}
}
private struct NetworkInterfacesCard: View {
let interfaces: [NetworkInterfaceMetrics]
let downloadColor: Color
let uploadColor: Color
private var sortedInterfaces: [NetworkInterfaceMetrics] {
interfaces.sorted { lhs, rhs in
if lhs.isActive != rhs.isActive {
return lhs.isActive && !rhs.isActive
}
return interfacePriority(lhs.type) < interfacePriority(rhs.type)
}
}
private func interfacePriority(_ type: NetworkInterfaceType) -> Int {
switch type {
case .wifi:
return 0
case .ethernet:
return 1
case .cellular:
return 2
case .other:
return 3
case .loopback:
return 4
}
}
var body: some View {
if sortedInterfaces.isEmpty {
Text("No active interfaces detected yet.")
.font(.footnote)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
VStack(spacing: 12) {
ForEach(sortedInterfaces) { interface in
NetworkInterfaceRow(
interface: interface,
downloadColor: downloadColor,
uploadColor: uploadColor
)
if interface.id != sortedInterfaces.last?.id {
Divider().padding(.leading, 44)
}
}
}
}
}
}
private struct NetworkInterfaceRow: View {
let interface: NetworkInterfaceMetrics
let downloadColor: Color
let uploadColor: Color
var body: some View {
HStack(alignment: .top, spacing: 12) {
icon
VStack(alignment: .leading, spacing: 6) {
HStack(spacing: 8) {
Text(interface.displayName)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.primary)
if interface.isActive {
Text("Active")
.font(.caption2)
.foregroundColor(downloadColor)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(downloadColor.opacity(0.16))
.clipShape(Capsule())
}
}
if let ipv4 = interface.ipv4, !ipv4.isEmpty {
Text("IPv4 · \(ipv4)")
.font(.caption2)
.foregroundColor(.secondary)
}
if let ipv6 = interface.ipv6, !ipv6.isEmpty {
Text("IPv6 · \(ipv6)")
.font(.caption2)
.foregroundColor(.secondary)
}
Text("Session · ↓ \(formattedBytes(interface.totalDownloaded)) · ↑ \(formattedBytes(interface.totalUploaded))")
.font(.caption2)
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 4) {
metricRow(label: "Down", value: StatsFormatting.throughput(interface.currentDownload), color: downloadColor)
metricRow(label: "Up", value: StatsFormatting.throughput(interface.currentUpload), color: uploadColor)
}
}
}
private var icon: some View {
let isActive = interface.isActive
return RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(isActive ? downloadColor.opacity(0.18) : Color.primary.opacity(0.05))
.overlay(
Image(systemName: iconName)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(isActive ? downloadColor : .secondary)
)
.frame(width: 32, height: 32)
}
private var iconName: String {
switch interface.type {
case .wifi:
return "wifi"
case .ethernet:
return "cable.connector"
case .cellular:
return "antenna.radiowaves.left.and.right"
case .loopback:
return "arrow.triangle.2.circlepath"
case .other:
return "network"
}
}
private func metricRow(label: String, value: String, color: Color) -> some View {
HStack(spacing: 6) {
Text(label)
.font(.caption2)
.foregroundColor(.secondary)
Text(value)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(color)
}
}
private func formattedBytes(_ megabytes: Double) -> String {
let bytes = UInt64(max(megabytes, 0) * 1_048_576)
return StatsFormatting.bytes(bytes)
}
}

View file

@ -34,12 +34,16 @@ enum ProcessRankingType {
case cpu
case memory
case gpu
case network
case disk
var title: String {
switch self {
case .cpu: return "CPU Usage"
case .memory: return "Memory Usage"
case .gpu: return "GPU Usage"
case .network: return "Network Activity"
case .disk: return "Disk Activity"
}
}
@ -48,6 +52,8 @@ enum ProcessRankingType {
case .cpu: return "cpu"
case .memory: return "memorychip"
case .gpu: return "display"
case .network: return "network"
case .disk: return "internaldrive"
}
}
@ -56,6 +62,8 @@ enum ProcessRankingType {
case .cpu: return .blue
case .memory: return .green
case .gpu: return .purple
case .network: return .orange
case .disk: return .cyan
}
}
}

View file

@ -6,191 +6,116 @@
// Adapted from boring.notch implementation
import SwiftUI
#if os(macOS)
import AppKit
#endif
struct RankedProcessPopover: View {
let rankingType: ProcessRankingType
@ObservedObject var statsManager = StatsManager.shared
@Environment(\.dismiss) private var dismiss
@State private var isHovering = false
// Callback to notify parent about hover state
var onHoverChange: ((Bool) -> Void)?
private var rankedProcesses: [ProcessStats] {
private var configuration: (width: CGFloat, minHeight: CGFloat, padding: CGFloat) {
switch rankingType {
case .cpu:
return statsManager.getProcessesRankedByCPU()
return (420, 420, 0)
case .memory:
return statsManager.getProcessesRankedByMemory()
return (400, 380, 0)
case .gpu:
return statsManager.getProcessesRankedByGPU()
return (380, 360, 0)
case .network, .disk:
return (360, 320, 0)
}
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Header
HStack {
Image(systemName: rankingType.icon)
.font(.title3)
.foregroundColor(rankingType.color)
Text("Top Processes by \(rankingType.title)")
.font(.headline)
.fontWeight(.semibold)
Spacer()
popoverContent
.padding(configuration.padding)
.frame(width: configuration.width)
.frame(minHeight: configuration.minHeight)
.background(Color(NSColor.controlBackgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.shadow(color: .black.opacity(0.28), radius: 12, x: 0, y: 8)
.overlay(alignment: .topTrailing) {
Button(action: { dismiss() }) {
Image(systemName: "xmark.circle.fill")
.font(.title3)
.foregroundColor(.secondary)
.padding(10)
}
.buttonStyle(PlainButtonStyle())
}
.padding(.bottom, 4)
Divider()
// Process list
ScrollView {
LazyVStack(spacing: 6) {
ForEach(Array(rankedProcesses.prefix(15).enumerated()), id: \.element.id) { index, process in
RankedProcessRow(
process: process,
rank: index + 1,
rankingType: rankingType
)
}
}
.padding(.vertical, 4)
}
.frame(maxHeight: 300)
// Footer with refresh button
HStack {
Text("\(rankedProcesses.count) processes")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Button("Refresh") {
// Trigger stats update
if statsManager.isMonitoring {
// Stats will auto-refresh
}
}
.font(.caption)
.foregroundColor(rankingType.color)
}
}
.padding()
.frame(width: 320)
.frame(minHeight: 200)
.background(Color(NSColor.controlBackgroundColor))
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
.onHover { hovering in
isHovering = hovering
.overlay(alignment: .center) {
PopoverHoverSensor { hovering in
onHoverChange?(hovering)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
}
}
}
struct RankedProcessRow: View {
let process: ProcessStats
let rank: Int
let rankingType: ProcessRankingType
@State private var isHovered = false
private var primaryValue: String {
@ViewBuilder
private var popoverContent: some View {
switch rankingType {
case .cpu:
return process.cpuUsageString
CPUStatsDetailView()
case .memory:
return process.memoryUsageString
MemoryStatsDetailView()
case .gpu:
return process.cpuUsageString // Using CPU as proxy for GPU
}
}
private var secondaryValue: String {
switch rankingType {
case .cpu:
return process.memoryUsageString
case .memory:
return process.cpuUsageString
case .gpu:
return process.memoryUsageString
}
}
var body: some View {
HStack(spacing: 8) {
// Rank
Text("\(rank)")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(rankingType.color)
.frame(width: 24, alignment: .center)
// App icon
Group {
if let icon = process.icon {
Image(nsImage: icon)
.resizable()
.aspectRatio(contentMode: .fit)
} else {
Image(systemName: "app")
.foregroundColor(.secondary)
}
}
.frame(width: 20, height: 20)
// Process info
VStack(alignment: .leading, spacing: 2) {
Text(process.name)
.font(.system(size: 12, weight: .medium))
.foregroundColor(.primary)
.lineLimit(1)
.truncationMode(.tail)
HStack(spacing: 8) {
Text(primaryValue)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(rankingType.color)
Text(secondaryValue)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
// PID
Text("PID: \(process.pid)")
.font(.caption2)
.foregroundColor(.secondary)
.opacity(0.7)
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(isHovered ? Color(NSColor.selectedControlColor).opacity(0.2) : Color.clear)
)
.onHover { hovering in
withAnimation(.easeInOut(duration: 0.15)) {
isHovered = hovering
}
GPUStatsDetailView()
case .network:
NetworkStatsDetailView()
case .disk:
DiskStatsDetailView()
}
}
}
#Preview {
RankedProcessPopover(rankingType: .cpu)
.background(Color.black)
#if os(macOS)
private struct PopoverHoverSensor: NSViewRepresentable {
var onHoverChange: ((Bool) -> Void)?
func makeNSView(context: Context) -> TrackingView {
let view = TrackingView()
view.onHoverChange = onHoverChange
return view
}
func updateNSView(_ nsView: TrackingView, context: Context) {
nsView.onHoverChange = onHoverChange
}
final class TrackingView: NSView {
var onHoverChange: ((Bool) -> Void)?
private var trackingArea: NSTrackingArea?
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea {
removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways, .inVisibleRect]
let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
addTrackingArea(area)
trackingArea = area
}
override func mouseEntered(with event: NSEvent) {
onHoverChange?(true)
}
override func mouseExited(with event: NSEvent) {
onHoverChange?(false)
}
deinit {
onHoverChange?(false)
if let trackingArea {
removeTrackingArea(trackingArea)
}
}
}
}
#endif

View file

@ -0,0 +1,78 @@
//
// StatsFormatting.swift
// DynamicIsland
//
// Lightweight formatters shared across stats detail popovers.
//
// Created by GitHub Copilot on 19/10/2025.
//
import Foundation
enum StatsFormatting {
static func bytes(_ value: UInt64) -> String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useMB, .useGB]
formatter.countStyle = .memory
formatter.includesUnit = true
return formatter.string(fromByteCount: Int64(value))
}
static func mbPerSecond(_ value: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = value >= 10 ? 1 : 2
let number = NSNumber(value: value)
let formatted = formatter.string(from: number) ?? String(format: "%.2f", value)
return "\(formatted) MB/s"
}
static func throughput(_ valueInMegabytesPerSecond: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
if valueInMegabytesPerSecond >= 1 {
formatter.maximumFractionDigits = valueInMegabytesPerSecond >= 10 ? 1 : 2
let formatted = formatter.string(from: NSNumber(value: valueInMegabytesPerSecond))
?? String(format: "%.2f", valueInMegabytesPerSecond)
return "\(formatted) MB/s"
}
let valueInKilobytesPerSecond = valueInMegabytesPerSecond * 1024
if valueInKilobytesPerSecond >= 1 {
formatter.maximumFractionDigits = valueInKilobytesPerSecond >= 10 ? 0 : 1
let formatted = formatter.string(from: NSNumber(value: valueInKilobytesPerSecond))
?? String(format: valueInKilobytesPerSecond >= 10 ? "%.0f" : "%.1f", valueInKilobytesPerSecond)
return "\(formatted) KB/s"
}
let valueInBytesPerSecond = valueInMegabytesPerSecond * 1_048_576
if valueInBytesPerSecond >= 1 {
formatter.maximumFractionDigits = 0
let formatted = formatter.string(from: NSNumber(value: valueInBytesPerSecond))
?? String(format: "%.0f", valueInBytesPerSecond)
return "\(formatted) B/s"
}
return "0 B/s"
}
static func percentage(_ value: Double) -> String {
String(format: "%.1f%%", value)
}
static func gigabytes(_ value: Double) -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = value >= 10 ? 1 : 2
return (formatter.string(from: NSNumber(value: value)) ?? String(format: "%.2f", value)) + " GB"
}
static func abbreviatedDuration(_ value: TimeInterval) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.day, .hour, .minute]
formatter.maximumUnitCount = 2
formatter.unitsStyle = .abbreviated
return formatter.string(from: value) ?? "—"
}
}

View file

@ -67,65 +67,10 @@ struct StatsPanelView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
// Stats content - Compact two-row layout
VStack(spacing: 16) {
// Top row: System overview cards
HStack(spacing: 12) {
CompactOverviewCard(
title: "CPU",
value: statsManager.cpuUsageString,
color: .blue,
icon: "cpu",
data: statsManager.cpuHistory
)
CompactOverviewCard(
title: "Memory",
value: statsManager.memoryUsageString,
color: .green,
icon: "memorychip",
data: statsManager.memoryHistory
)
CompactOverviewCard(
title: "GPU",
value: statsManager.gpuUsageString,
color: .purple,
icon: "display",
data: statsManager.gpuHistory
)
}
// Bottom row: Network and Disk graphs
HStack(spacing: 12) {
CompactDualGraph(
title: "Network",
primaryLabel: "Down",
primaryValue: "\(String(format: "%.1f", statsManager.networkDownload / 1024 / 1024)) MB/s",
primaryData: statsManager.networkDownloadHistory,
primaryColor: .green,
secondaryLabel: "Up",
secondaryValue: "\(String(format: "%.1f", statsManager.networkUpload / 1024 / 1024)) MB/s",
secondaryData: statsManager.networkUploadHistory,
secondaryColor: .orange
)
CompactDualGraph(
title: "Disk",
primaryLabel: "Read",
primaryValue: "\(String(format: "%.1f", statsManager.diskRead / 1024 / 1024)) MB/s",
primaryData: statsManager.diskReadHistory,
primaryColor: .cyan,
secondaryLabel: "Write",
secondaryValue: "\(String(format: "%.1f", statsManager.diskWrite / 1024 / 1024)) MB/s",
secondaryData: statsManager.diskWriteHistory,
secondaryColor: .pink
)
CPUStatsDetailView()
}
}
.padding(16)
}
}
.frame(width: 650, height: 300)
.frame(width: 520, height: 620)
.background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow))
.cornerRadius(12)
.shadow(color: .black.opacity(0.4), radius: 20, x: 0, y: 10)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,39 @@
//
// CPUMetrics.swift
// DynamicIsland
//
// Lightweight CPU metric models derived from StatsManager sampling.
//
// Created by GitHub Copilot on 18/10/2025.
//
import Foundation
struct CPULoadBreakdown: Equatable {
var user: Double
var system: Double
var idle: Double
static let zero = CPULoadBreakdown(user: 0, system: 0, idle: 100)
var activeUsage: Double {
let active = user + system
return min(max(active, 0), 100)
}
var normalizedSegments: (user: Double, system: Double, idle: Double) {
let total = max(user + system + idle, 0.001)
let userFraction = min(max(user / total, 0), 1)
let systemFraction = min(max(system / total, 0), 1)
let idleFraction = min(max(idle / total, 0), 1)
return (userFraction, systemFraction, idleFraction)
}
}
struct LoadAverage: Equatable {
var oneMinute: Double
var fiveMinutes: Double
var fifteenMinutes: Double
static let zero = LoadAverage(oneMinute: 0, fiveMinutes: 0, fifteenMinutes: 0)
}

View file

@ -0,0 +1,162 @@
//
// AppleHardwareInfo.swift
// DynamicIsland
//
// Minimal system introspection helpers derived from the Stats project.
//
import Foundation
import IOKit
import Darwin
enum ApplePlatform: String {
case intel
case m1
case m1Pro
case m1Max
case m1Ultra
case m2
case m2Pro
case m2Max
case m2Ultra
case m3
case m3Pro
case m3Max
case m3Ultra
case m4
case m4Pro
case m4Max
case m4Ultra
}
struct CPUClusterFrequencies {
let eCoreFrequencies: [Int32]
let pCoreFrequencies: [Int32]
}
struct CPUClusterCounts {
let eCores: Int
let pCores: Int
}
final class AppleHardwareInfo {
static let shared = AppleHardwareInfo()
let cpuName: String?
let platform: ApplePlatform?
let logicalCoreCount: Int
let clusterCounts: CPUClusterCounts
let clusterFrequencies: CPUClusterFrequencies
private init() {
let name = AppleHardwareInfo.fetchCPUName()
cpuName = name
platform = AppleHardwareInfo.detectPlatform(from: name)
logicalCoreCount = AppleHardwareInfo.fetchLogicalCoreCount()
clusterCounts = AppleHardwareInfo.fetchClusterCounts()
clusterFrequencies = AppleHardwareInfo.fetchClusterFrequencies(cpuName: name ?? "")
}
private static func fetchCPUName() -> String? {
var size = 0
sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0)
guard size > 0 else { return nil }
var buffer = [CChar](repeating: 0, count: size)
let result = sysctlbyname("machdep.cpu.brand_string", &buffer, &size, nil, 0)
guard result == 0 else { return nil }
return String(cString: buffer)
}
private static func fetchLogicalCoreCount() -> Int {
var cores: Int32 = 0
var size = MemoryLayout<Int32>.size
let result = sysctlbyname("hw.logicalcpu", &cores, &size, nil, 0)
return result == 0 ? Int(cores) : ProcessInfo.processInfo.processorCount
}
private static func detectPlatform(from cpuName: String?) -> ApplePlatform? {
guard let name = cpuName?.lowercased() else { return nil }
if name.contains("intel") { return .intel }
if name.contains("m1") {
if name.contains("ultra") { return .m1Ultra }
if name.contains("max") { return .m1Max }
if name.contains("pro") { return .m1Pro }
return .m1
}
if name.contains("m2") {
if name.contains("ultra") { return .m2Ultra }
if name.contains("max") { return .m2Max }
if name.contains("pro") { return .m2Pro }
return .m2
}
if name.contains("m3") {
if name.contains("ultra") { return .m3Ultra }
if name.contains("max") { return .m3Max }
if name.contains("pro") { return .m3Pro }
return .m3
}
if name.contains("m4") {
if name.contains("ultra") { return .m4Ultra }
if name.contains("max") { return .m4Max }
if name.contains("pro") { return .m4Pro }
return .m4
}
return nil
}
private static func fetchClusterCounts() -> CPUClusterCounts {
var iterator = io_iterator_t()
guard IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("AppleARMPE"), &iterator) == KERN_SUCCESS else {
return CPUClusterCounts(eCores: 0, pCores: 0)
}
defer { IOObjectRelease(iterator) }
var eCoreCount: Int32 = 0
var pCoreCount: Int32 = 0
while case let service = IOIteratorNext(iterator), service != 0 {
defer { IOObjectRelease(service) }
var children = io_iterator_t()
guard IORegistryEntryGetChildIterator(service, kIOServicePlane, &children) == KERN_SUCCESS else { continue }
defer { IOObjectRelease(children) }
while case let child = IOIteratorNext(children), child != 0 {
defer { IOObjectRelease(child) }
guard let name = di_getIOName(child), name == "cpus", let props = di_getIOProperties(child) else { continue }
if let data = props.object(forKey: "e-core-count") as? Data {
eCoreCount = data.withUnsafeBytes { $0.load(as: Int32.self) }
}
if let data = props.object(forKey: "p-core-count") as? Data {
pCoreCount = data.withUnsafeBytes { $0.load(as: Int32.self) }
}
}
}
return CPUClusterCounts(eCores: Int(eCoreCount), pCores: Int(pCoreCount))
}
private static func fetchClusterFrequencies(cpuName: String) -> CPUClusterFrequencies {
var iterator = io_iterator_t()
guard IOServiceGetMatchingServices(kIOMasterPortDefault, IOServiceMatching("AppleARMIODevice"), &iterator) == KERN_SUCCESS else {
return CPUClusterFrequencies(eCoreFrequencies: [], pCoreFrequencies: [])
}
defer { IOObjectRelease(iterator) }
let lowerName = cpuName.lowercased()
let isM4 = lowerName.contains("m4")
var eFreq: [Int32] = []
var pFreq: [Int32] = []
while case let service = IOIteratorNext(iterator), service != 0 {
defer { IOObjectRelease(service) }
guard let name = di_getIOName(service), name == "pmgr", let props = di_getIOProperties(service) else { continue }
if let data = props["voltage-states1-sram"] as? Data {
eFreq = di_convertCFDataToFrequencies(data, isM4: isM4)
}
if let data = props["voltage-states5-sram"] as? Data {
pFreq = di_convertCFDataToFrequencies(data, isM4: isM4)
}
}
return CPUClusterFrequencies(eCoreFrequencies: eFreq, pCoreFrequencies: pFreq)
}
}

View file

@ -0,0 +1,237 @@
//
// CPUSensorCollector.swift
// DynamicIsland
//
// Ports the Stats project's CPU temperature and frequency readers.
//
import Foundation
import IOKit
struct CPUFrequencyMetrics: Equatable {
let overallGHz: Double
let eCoreGHz: Double?
let pCoreGHz: Double?
let maxOverallGHz: Double
let maxECoreGHz: Double?
let maxPCoreGHz: Double?
}
struct CPUTemperatureMetrics: Equatable {
let celsius: Double?
}
final class CPUSensorCollector {
private let hardware = AppleHardwareInfo.shared
private var channels: CFMutableDictionary?
private var subscription: IOReportSubscriptionRef?
private var previousSample: (samples: CFDictionary, time: TimeInterval)?
init() {
setupFrequencyChannel()
}
deinit {
if let subscription {
IOReportCreateSamples(subscription, nil, nil) // best-effort flush
}
}
func readTemperature() -> CPUTemperatureMetrics {
let platform = hardware.platform
if let value = primaryTemperatureKeyCandidates.compactMap({ SMC.shared.getValue($0) }).first(where: { $0 < 110 }) {
return CPUTemperatureMetrics(celsius: value)
}
let list = temperatureFallbackKeys(for: platform)
var total: Double = 0
var count: Double = 0
for key in list {
if let value = SMC.shared.getValue(key), value < 110 {
total += value
count += 1
}
}
guard total > 0, count > 0 else {
return CPUTemperatureMetrics(celsius: nil)
}
return CPUTemperatureMetrics(celsius: total / count)
}
func readFrequency() -> CPUFrequencyMetrics? {
guard let channels, let subscription else { return nil }
let timestamp = Date().timeIntervalSince1970
guard let currentSample = IOReportCreateSamples(subscription, channels, nil)?.takeRetainedValue() else {
return nil
}
defer {
previousSample = (currentSample, timestamp)
}
guard let previous = previousSample,
let diff = IOReportCreateSamplesDelta(previous.samples, currentSample, nil)?.takeRetainedValue() else {
return nil
}
let samples = collectIOSamples(from: diff)
let eFrequencies = hardware.clusterFrequencies.eCoreFrequencies
let pFrequencies = hardware.clusterFrequencies.pCoreFrequencies
let eCount = max(hardware.clusterCounts.eCores, 0)
let pCount = max(hardware.clusterCounts.pCores, 0)
var eValues: [Double] = []
var pValues: [Double] = []
for sample in samples where sample.group == "CPU Stats" {
if sample.channel.hasPrefix("ECPU") {
let value = calculateFrequency(from: sample.delta, freqs: eFrequencies)
if value > 0 { eValues.append(value) }
} else if sample.channel.hasPrefix("PCPU") {
let value = calculateFrequency(from: sample.delta, freqs: pFrequencies)
if value > 0 { pValues.append(value) }
}
}
let eAverage = eValues.isEmpty ? nil : (eValues.reduce(0, +) / Double(eValues.count))
let pAverage = pValues.isEmpty ? nil : (pValues.reduce(0, +) / Double(pValues.count))
guard eAverage != nil || pAverage != nil else {
return nil
}
let overall: Double
if let eAverage, let pAverage, (eCount + pCount) > 0 {
overall = ((eAverage * Double(eCount)) + (pAverage * Double(pCount))) / Double(eCount + pCount)
} else if let eAverage {
overall = eAverage
} else if let pAverage {
overall = pAverage
} else {
return nil
}
let maxE = eFrequencies.max().map { Double($0) / 1000 }
let maxP = pFrequencies.max().map { Double($0) / 1000 }
let maxOverall: Double
if let maxE, let maxP, (eCount + pCount) > 0 {
maxOverall = ((maxE * Double(eCount)) + (maxP * Double(pCount))) / Double(max(eCount + pCount, 1))
} else if let maxE {
maxOverall = maxE
} else if let maxP {
maxOverall = maxP
} else {
maxOverall = (overall / 1000)
}
return CPUFrequencyMetrics(
overallGHz: overall / 1000,
eCoreGHz: eAverage.map { $0 / 1000 },
pCoreGHz: pAverage.map { $0 / 1000 },
maxOverallGHz: maxOverall,
maxECoreGHz: maxE,
maxPCoreGHz: maxP
)
}
// MARK: - Private Helpers
private func setupFrequencyChannel() {
let names = [
("CPU Stats" as CFString, "CPU Complex Performance States" as CFString),
("CPU Stats" as CFString, "CPU Core Performance States" as CFString)
]
var mergedChannels: CFMutableDictionary?
var list: [CFDictionary] = []
for (group, subgroup) in names {
if let channel = IOReportCopyChannelsInGroup(group, subgroup, 0, 0, 0)?.takeRetainedValue() {
list.append(channel)
}
}
guard let first = list.first else { return }
for channel in list.dropFirst() {
IOReportMergeChannels(first, channel, nil)
}
guard let copy = CFDictionaryCreateMutableCopy(kCFAllocatorDefault, CFDictionaryGetCount(first), first) else {
return
}
mergedChannels = copy
channels = mergedChannels
var dictionary: Unmanaged<CFMutableDictionary>?
subscription = IOReportCreateSubscription(nil, mergedChannels, &dictionary, 0, nil)
dictionary?.release()
}
private struct IOSample {
let group: String
let subGroup: String
let channel: String
let unit: String
let delta: CFDictionary
}
private func collectIOSamples(from data: CFDictionary) -> [IOSample] {
let key = "IOReportChannels" as CFString
var rawValue: UnsafeRawPointer?
let found = withUnsafeMutablePointer(to: &rawValue) { pointer -> Bool in
CFDictionaryGetValueIfPresent(data, Unmanaged.passUnretained(key).toOpaque(), pointer)
}
guard found, let rawValue else { return [] }
let array = unsafeBitCast(rawValue, to: CFArray.self)
var result: [IOSample] = []
for index in 0..<CFArrayGetCount(array) {
let element = CFArrayGetValueAtIndex(array, index)
let entry = unsafeBitCast(element, to: CFDictionary.self)
let group = IOReportChannelGetGroup(entry)?.takeUnretainedValue() as String? ?? ""
let subGroup = IOReportChannelGetSubGroup(entry)?.takeUnretainedValue() as String? ?? ""
let channel = IOReportChannelGetChannelName(entry)?.takeUnretainedValue() as String? ?? ""
let unit = IOReportChannelGetUnitLabel(entry)?.takeUnretainedValue() as String? ?? ""
result.append(IOSample(group: group, subGroup: subGroup, channel: channel, unit: unit, delta: entry))
}
return result
}
private func getResidencies(dict: CFDictionary) -> [(label: String, value: Int64)] {
let count = IOReportStateGetCount(dict)
var result: [(String, Int64)] = []
for index in 0..<count {
let name = IOReportStateGetNameForIndex(dict, index)?.takeUnretainedValue() as String? ?? ""
let value = IOReportStateGetResidency(dict, index)
result.append((name, value))
}
return result
}
private func calculateFrequency(from dict: CFDictionary, freqs: [Int32]) -> Double {
guard !freqs.isEmpty else { return 0 }
let states = getResidencies(dict: dict)
guard let offset = states.firstIndex(where: { !$0.label.elementsEqual("IDLE") && !$0.label.elementsEqual("DOWN") && !$0.label.elementsEqual("OFF") }) else {
return 0
}
let usage = states.dropFirst(offset).reduce(0.0) { $0 + Double($1.value) }
guard usage > 0 else { return 0 }
var total: Double = 0
for (index, frequency) in freqs.enumerated() {
let stateIndex = offset + index
guard states.indices.contains(stateIndex) else { continue }
let fraction = Double(states[stateIndex].value) / usage
total += fraction * Double(frequency)
}
return total
}
private var primaryTemperatureKeyCandidates: [String] {
["TC0D", "TC0E", "TC0F", "TC0P", "TC0H"]
}
private func temperatureFallbackKeys(for platform: ApplePlatform?) -> [String] {
switch platform {
case .m1, .m1Pro, .m1Max, .m1Ultra:
return ["Tp09", "Tp0T", "Tp01", "Tp05", "Tp0D", "Tp0H", "Tp0L", "Tp0P", "Tp0X", "Tp0b"]
case .m2, .m2Pro, .m2Max, .m2Ultra:
return ["Tp1h", "Tp1t", "Tp1p", "Tp1l", "Tp01", "Tp05", "Tp09", "Tp0D", "Tp0X", "Tp0b", "Tp0f", "Tp0j"]
case .m3, .m3Pro, .m3Max, .m3Ultra:
return ["Te05", "Te0L", "Te0P", "Te0S", "Tf04", "Tf09", "Tf0A", "Tf0B", "Tf0D", "Tf0E", "Tf44", "Tf49", "Tf4A", "Tf4B", "Tf4D", "Tf4E"]
case .m4, .m4Pro, .m4Max, .m4Ultra:
return ["Te05", "Te09", "Te0H", "Te0S", "Tp01", "Tp05", "Tp09", "Tp0D", "Tp0V", "Tp0Y", "Tp0b", "Tp0e"]
default:
return ["TC0P", "TC0E", "TC0F", "TC0H", "TC0D"]
}
}
}

View file

@ -0,0 +1,52 @@
//
// IOKitHelpers.swift
// DynamicIsland
//
// Helper utilities borrowed from the Stats project for working with IORegistry and CFData blobs.
//
import Foundation
import IOKit
import Darwin
func di_getIOProperties(_ entry: io_registry_entry_t) -> NSDictionary? {
var properties: Unmanaged<CFMutableDictionary>? = nil
guard IORegistryEntryCreateCFProperties(entry, &properties, kCFAllocatorDefault, 0) == kIOReturnSuccess else {
return nil
}
defer { properties?.release() }
return properties?.takeUnretainedValue()
}
func di_getIOName(_ entry: io_registry_entry_t) -> String? {
let pointer = UnsafeMutablePointer<io_name_t>.allocate(capacity: 1)
defer { pointer.deallocate() }
let result = IORegistryEntryGetName(entry, pointer)
guard result == kIOReturnSuccess else {
if let message = String(validatingUTF8: mach_error_string(result)) {
print("IORegistryEntryGetName error: \(message)")
}
return nil
}
return String(cString: UnsafeRawPointer(pointer).assumingMemoryBound(to: CChar.self))
}
func di_convertCFDataToFrequencies(_ data: Data, isM4: Bool = false) -> [Int32] {
guard !data.isEmpty else { return [] }
let bytes = [UInt8](data)
let multiplier: UInt32 = isM4 ? 1_000 : 1_000_000
var frequencies: [Int32] = []
for chunkStart in stride(from: 0, to: bytes.count, by: 8) {
let upperBound = min(chunkStart + 8, bytes.count)
let chunk = bytes[chunkStart..<upperBound]
if chunk.count < 4 { continue }
let value = chunk.prefix(4).enumerated().reduce(UInt32(0)) { partial, element in
partial | (UInt32(element.element) << (UInt32(element.offset) * 8))
}
frequencies.append(Int32(value / multiplier))
}
return frequencies
}

View file

@ -0,0 +1,112 @@
//
// IOReportBridging.swift
// DynamicIsland
//
// Lightweight runtime loader for private IOReport symbols used by the GPU/CPU collectors.
// Avoids static linking against the private framework by resolving symbols via dlopen/dlsym.
//
import Foundation
import Darwin
public typealias IOReportSubscriptionRef = OpaquePointer
private enum IOReportLoader {
private static let libraryHandle: UnsafeMutableRawPointer? = {
let paths = [
"/System/Library/PrivateFrameworks/IOReport.framework/IOReport",
"/usr/lib/libIOReport.dylib"
]
for path in paths {
if let handle = dlopen(path, RTLD_LAZY | RTLD_LOCAL) {
return handle
}
}
return nil
}()
static func symbol<T>(_ name: String, as type: T.Type) -> T? {
guard let handle = libraryHandle, let pointer = dlsym(handle, name) else {
return nil
}
return unsafeBitCast(pointer, to: T.self)
}
}
func IOReportCopyChannelsInGroup(_ group: CFString?, _ subGroup: CFString?, _ a: UInt64, _ b: UInt64, _ c: UInt64) -> Unmanaged<CFDictionary>? {
typealias Fn = @convention(c) (CFString?, CFString?, UInt64, UInt64, UInt64) -> Unmanaged<CFDictionary>?
guard let fn: Fn = IOReportLoader.symbol("IOReportCopyChannelsInGroup", as: Fn.self) else { return nil }
return fn(group, subGroup, a, b, c)
}
func IOReportMergeChannels(_ a: CFDictionary, _ b: CFDictionary, _ c: CFTypeRef?) {
typealias Fn = @convention(c) (CFDictionary, CFDictionary, CFTypeRef?) -> Void
guard let fn: Fn = IOReportLoader.symbol("IOReportMergeChannels", as: Fn.self) else { return }
fn(a, b, c)
}
func IOReportCreateSubscription(_ a: UnsafeMutableRawPointer?, _ b: CFMutableDictionary?, _ c: UnsafeMutablePointer<Unmanaged<CFMutableDictionary>?>?, _ d: UInt64, _ e: CFTypeRef?) -> IOReportSubscriptionRef? {
typealias Fn = @convention(c) (UnsafeMutableRawPointer?, CFMutableDictionary?, UnsafeMutablePointer<Unmanaged<CFMutableDictionary>?>?, UInt64, CFTypeRef?) -> IOReportSubscriptionRef?
guard let fn: Fn = IOReportLoader.symbol("IOReportCreateSubscription", as: Fn.self) else { return nil }
return fn(a, b, c, d, e)
}
func IOReportCreateSamples(_ subscription: IOReportSubscriptionRef?, _ channels: CFMutableDictionary?, _ client: CFTypeRef?) -> Unmanaged<CFDictionary>? {
typealias Fn = @convention(c) (IOReportSubscriptionRef?, CFMutableDictionary?, CFTypeRef?) -> Unmanaged<CFDictionary>?
guard let fn: Fn = IOReportLoader.symbol("IOReportCreateSamples", as: Fn.self) else { return nil }
return fn(subscription, channels, client)
}
func IOReportCreateSamplesDelta(_ previous: CFDictionary, _ current: CFDictionary, _ zone: CFTypeRef?) -> Unmanaged<CFDictionary>? {
typealias Fn = @convention(c) (CFDictionary, CFDictionary, CFTypeRef?) -> Unmanaged<CFDictionary>?
guard let fn: Fn = IOReportLoader.symbol("IOReportCreateSamplesDelta", as: Fn.self) else { return nil }
return fn(previous, current, zone)
}
func IOReportChannelGetGroup(_ dictionary: CFDictionary) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportChannelGetGroup", as: Fn.self) else { return nil }
return fn(dictionary)
}
func IOReportChannelGetSubGroup(_ dictionary: CFDictionary) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportChannelGetSubGroup", as: Fn.self) else { return nil }
return fn(dictionary)
}
func IOReportChannelGetChannelName(_ dictionary: CFDictionary) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportChannelGetChannelName", as: Fn.self) else { return nil }
return fn(dictionary)
}
func IOReportChannelGetUnitLabel(_ dictionary: CFDictionary) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportChannelGetUnitLabel", as: Fn.self) else { return nil }
return fn(dictionary)
}
func IOReportStateGetCount(_ dictionary: CFDictionary) -> Int32 {
typealias Fn = @convention(c) (CFDictionary) -> Int32
guard let fn: Fn = IOReportLoader.symbol("IOReportStateGetCount", as: Fn.self) else { return 0 }
return fn(dictionary)
}
func IOReportStateGetNameForIndex(_ dictionary: CFDictionary, _ index: Int32) -> Unmanaged<CFString>? {
typealias Fn = @convention(c) (CFDictionary, Int32) -> Unmanaged<CFString>?
guard let fn: Fn = IOReportLoader.symbol("IOReportStateGetNameForIndex", as: Fn.self) else { return nil }
return fn(dictionary, index)
}
func IOReportStateGetResidency(_ dictionary: CFDictionary, _ index: Int32) -> Int64 {
typealias Fn = @convention(c) (CFDictionary, Int32) -> Int64
guard let fn: Fn = IOReportLoader.symbol("IOReportStateGetResidency", as: Fn.self) else { return 0 }
return fn(dictionary, index)
}
func IOReportSimpleGetIntegerValue(_ dictionary: CFDictionary, _ index: Int32) -> Int64 {
typealias Fn = @convention(c) (CFDictionary, Int32) -> Int64
guard let fn: Fn = IOReportLoader.symbol("IOReportSimpleGetIntegerValue", as: Fn.self) else { return 0 }
return fn(dictionary, index)
}

View file

@ -0,0 +1,532 @@
//
// smc.swift
// SMC
//
// Created by Serhiy Mytrovtsiy on 25/05/2021.
// Using Swift 5.0.
// Running on macOS 10.15.
//
// Copyright © 2021 Serhiy Mytrovtsiy. All rights reserved.
//
import Foundation
import IOKit
internal enum SMCDataType: String {
case UI8 = "ui8 "
case UI16 = "ui16"
case UI32 = "ui32"
case SP1E = "sp1e"
case SP3C = "sp3c"
case SP4B = "sp5b"
case SP5A = "sp5a"
case SPA5 = "spa5"
case SP69 = "sp669"
case SP78 = "sp78"
case SP87 = "sp87"
case SP96 = "sp96"
case SPB4 = "spb4"
case SPF0 = "spf0"
case FLT = "flt "
case FPE2 = "fpe2"
case FP2E = "fp2e"
case FDS = "{fds"
}
internal enum SMCKeys: UInt8 {
case kernelIndex = 2
case readBytes = 5
case writeBytes = 6
case readIndex = 8
case readKeyInfo = 9
case readPLimit = 11
case readVers = 12
}
public enum FanMode: Int, Codable {
case automatic = 0
case forced = 1
}
internal struct SMCKeyData_t {
typealias SMCBytes_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8)
struct vers_t {
var major: CUnsignedChar = 0
var minor: CUnsignedChar = 0
var build: CUnsignedChar = 0
var reserved: CUnsignedChar = 0
var release: CUnsignedShort = 0
}
struct LimitData_t {
var version: UInt16 = 0
var length: UInt16 = 0
var cpuPLimit: UInt32 = 0
var gpuPLimit: UInt32 = 0
var memPLimit: UInt32 = 0
}
struct keyInfo_t {
var dataSize: IOByteCount32 = 0
var dataType: UInt32 = 0
var dataAttributes: UInt8 = 0
}
var key: UInt32 = 0
var vers = vers_t()
var pLimitData = LimitData_t()
var keyInfo = keyInfo_t()
var padding: UInt16 = 0
var result: UInt8 = 0
var status: UInt8 = 0
var data8: UInt8 = 0
var data32: UInt32 = 0
var bytes: SMCBytes_t = (UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0))
}
internal struct SMCVal_t {
var key: String
var dataSize: UInt32 = 0
var dataType: String = ""
var bytes: [UInt8] = Array(repeating: 0, count: 32)
init(_ key: String) {
self.key = key
}
}
extension FourCharCode {
init(fromString str: String) {
precondition(str.count == 4)
self = str.utf8.reduce(0) { sum, character in
return sum << 8 | UInt32(character)
}
}
func toString() -> String {
return String(describing: UnicodeScalar(self >> 24 & 0xff)!) +
String(describing: UnicodeScalar(self >> 16 & 0xff)!) +
String(describing: UnicodeScalar(self >> 8 & 0xff)!) +
String(describing: UnicodeScalar(self & 0xff)!)
}
}
extension UInt16 {
init(bytes: (UInt8, UInt8)) {
self = UInt16(bytes.0) << 8 | UInt16(bytes.1)
}
}
extension UInt32 {
init(bytes: (UInt8, UInt8, UInt8, UInt8)) {
self = UInt32(bytes.0) << 24 | UInt32(bytes.1) << 16 | UInt32(bytes.2) << 8 | UInt32(bytes.3)
}
}
extension Int {
init(fromFPE2 bytes: (UInt8, UInt8)) {
self = (Int(bytes.0) << 6) + (Int(bytes.1) >> 2)
}
}
extension Float {
init?(_ bytes: [UInt8]) {
self = bytes.withUnsafeBytes {
return $0.load(fromByteOffset: 0, as: Self.self)
}
}
var bytes: [UInt8] {
withUnsafeBytes(of: self, Array.init)
}
}
public class SMC {
public static let shared = SMC()
private var conn: io_connect_t = 0
public init() {
var result: kern_return_t
var iterator: io_iterator_t = 0
let device: io_object_t
let matchingDictionary: CFMutableDictionary = IOServiceMatching("AppleSMC")
result = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDictionary, &iterator)
if result != kIOReturnSuccess {
print("Error IOServiceGetMatchingServices(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
device = IOIteratorNext(iterator)
IOObjectRelease(iterator)
if device == 0 {
print("Error IOIteratorNext(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
result = IOServiceOpen(device, mach_task_self_, 0, &conn)
IOObjectRelease(device)
if result != kIOReturnSuccess {
print("Error IOServiceOpen(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
deinit {
let result = self.close()
if result != kIOReturnSuccess {
print("error close smc connection: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
public func close() -> kern_return_t {
return IOServiceClose(conn)
}
public func getValue(_ key: String) -> Double? {
var result: kern_return_t = 0
var val: SMCVal_t = SMCVal_t(key)
result = read(&val)
if result != kIOReturnSuccess {
print("Error read(\(key)): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
if val.dataSize > 0 {
if val.bytes.first(where: { $0 != 0 }) == nil && val.key != "FS! " && val.key != "F0Md" && val.key != "F1Md" {
return nil
}
switch val.dataType {
case SMCDataType.UI8.rawValue:
return Double(val.bytes[0])
case SMCDataType.UI16.rawValue:
return Double(UInt16(bytes: (val.bytes[0], val.bytes[1])))
case SMCDataType.UI32.rawValue:
return Double(UInt32(bytes: (val.bytes[0], val.bytes[1], val.bytes[2], val.bytes[3])))
case SMCDataType.SP1E.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 16384)
case SMCDataType.SP3C.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 4096)
case SMCDataType.SP4B.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 2048)
case SMCDataType.SP5A.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 1024)
case SMCDataType.SP69.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 512)
case SMCDataType.SP78.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 256)
case SMCDataType.SP87.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 128)
case SMCDataType.SP96.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 64)
case SMCDataType.SPA5.rawValue:
let result: Double = Double(UInt16(val.bytes[0]) * 256 + UInt16(val.bytes[1]))
return Double(result / 32)
case SMCDataType.SPB4.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return Double(intValue / 16)
case SMCDataType.SPF0.rawValue:
let intValue: Double = Double(Int(val.bytes[0]) * 256 + Int(val.bytes[1]))
return intValue
case SMCDataType.FLT.rawValue:
let value: Float? = Float(val.bytes)
if value != nil {
return Double(value!)
}
return nil
case SMCDataType.FPE2.rawValue:
return Double(Int(fromFPE2: (val.bytes[0], val.bytes[1])))
default:
return nil
}
}
return nil
}
public func getStringValue(_ key: String) -> String? {
var result: kern_return_t = 0
var val: SMCVal_t = SMCVal_t(key)
result = read(&val)
if result != kIOReturnSuccess {
print("Error read(): " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return nil
}
if val.dataSize > 0 {
if val.bytes.first(where: { $0 != 0}) == nil {
return nil
}
switch val.dataType {
case SMCDataType.FDS.rawValue:
let c1 = String(UnicodeScalar(val.bytes[4]))
let c2 = String(UnicodeScalar(val.bytes[5]))
let c3 = String(UnicodeScalar(val.bytes[6]))
let c4 = String(UnicodeScalar(val.bytes[7]))
let c5 = String(UnicodeScalar(val.bytes[8]))
let c6 = String(UnicodeScalar(val.bytes[9]))
let c7 = String(UnicodeScalar(val.bytes[10]))
let c8 = String(UnicodeScalar(val.bytes[11]))
let c9 = String(UnicodeScalar(val.bytes[12]))
let c10 = String(UnicodeScalar(val.bytes[13]))
let c11 = String(UnicodeScalar(val.bytes[14]))
let c12 = String(UnicodeScalar(val.bytes[15]))
return (c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9 + c10 + c11 + c12).trimmingCharacters(in: .whitespaces)
default:
print("unsupported data type \(val.dataType) for key: \(key)")
return nil
}
}
return nil
}
public func getAllKeys() -> [String] {
var list: [String] = []
let keysNum: Double? = self.getValue("#KEY")
if keysNum == nil {
print("ERROR no keys count found")
return list
}
var result: kern_return_t = 0
var input: SMCKeyData_t = SMCKeyData_t()
var output: SMCKeyData_t = SMCKeyData_t()
for i in 0...Int(keysNum!) {
input = SMCKeyData_t()
output = SMCKeyData_t()
input.data8 = SMCKeys.readIndex.rawValue
input.data32 = UInt32(i)
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
continue
}
list.append(output.key.toString())
}
return list
}
public func write(_ key: String, _ newValue: Int) -> kern_return_t {
var value = SMCVal_t(key)
value.dataSize = 2
value.bytes = [UInt8(newValue >> 6), UInt8((newValue << 2) ^ ((newValue >> 6) << 8)), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
return write(value)
}
// MARK: - fans
public func setFanMode(_ id: Int, mode: FanMode) {
if self.getValue("F\(id)Md") != nil {
var result: kern_return_t = 0
var value = SMCVal_t("F\(id)Md")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan mode: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
value.bytes = [UInt8(mode.rawValue), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
let fansMode = Int(self.getValue("FS! ") ?? 0)
var newMode: UInt8 = 0
if fansMode == 0 && id == 0 && mode == .forced {
newMode = 1
} else if fansMode == 0 && id == 1 && mode == .forced {
newMode = 2
} else if fansMode == 1 && id == 0 && mode == .automatic {
newMode = 0
} else if fansMode == 1 && id == 1 && mode == .forced {
newMode = 3
} else if fansMode == 2 && id == 1 && mode == .automatic {
newMode = 0
} else if fansMode == 2 && id == 0 && mode == .forced {
newMode = 3
} else if fansMode == 3 && id == 0 && mode == .automatic {
newMode = 2
} else if fansMode == 3 && id == 1 && mode == .automatic {
newMode = 1
}
if fansMode == newMode {
return
}
var result: kern_return_t = 0
var value = SMCVal_t("FS! ")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan mode: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
value.bytes = [0, newMode, UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0), UInt8(0),
UInt8(0), UInt8(0)]
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
public func setFanSpeed(_ id: Int, speed: Int) {
let maxSpeed = Int(self.getValue("F\(id)Mx") ?? 4000)
if speed > maxSpeed {
print("new fan speed (\(speed)) is more than maximum speed (\(maxSpeed))")
return
}
var result: kern_return_t = 0
var value = SMCVal_t("F\(id)Tg")
result = read(&value)
if result != kIOReturnSuccess {
print("Error read fan value: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
if value.dataType == "flt " {
let bytes = Float(speed).bytes
value.bytes[0] = bytes[0]
value.bytes[1] = bytes[1]
value.bytes[2] = bytes[2]
value.bytes[3] = bytes[3]
} else if value.dataType == "fpe2" {
value.bytes[0] = UInt8(speed >> 6)
value.bytes[1] = UInt8((speed << 2) ^ ((speed >> 6) << 8))
value.bytes[2] = UInt8(0)
value.bytes[3] = UInt8(0)
}
result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
return
}
}
public func resetFans() {
var value = SMCVal_t("FS! ")
value.dataSize = 2
let result = write(value)
if result != kIOReturnSuccess {
print("Error write: " + (String(cString: mach_error_string(result), encoding: String.Encoding.ascii) ?? "unknown error"))
}
}
// MARK: - internal functions
private func read(_ value: UnsafeMutablePointer<SMCVal_t>) -> kern_return_t {
var result: kern_return_t = 0
var input = SMCKeyData_t()
var output = SMCKeyData_t()
input.key = FourCharCode(fromString: value.pointee.key)
input.data8 = SMCKeys.readKeyInfo.rawValue
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
value.pointee.dataSize = UInt32(output.keyInfo.dataSize)
value.pointee.dataType = output.keyInfo.dataType.toString()
input.keyInfo.dataSize = output.keyInfo.dataSize
input.data8 = SMCKeys.readBytes.rawValue
result = call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
memcpy(&value.pointee.bytes, &output.bytes, Int(value.pointee.dataSize))
return kIOReturnSuccess
}
private func write(_ value: SMCVal_t) -> kern_return_t {
var input = SMCKeyData_t()
var output = SMCKeyData_t()
input.key = FourCharCode(fromString: value.key)
input.data8 = SMCKeys.writeBytes.rawValue
input.keyInfo.dataSize = IOByteCount32(value.dataSize)
input.bytes = (value.bytes[0], value.bytes[1], value.bytes[2], value.bytes[3], value.bytes[4], value.bytes[5],
value.bytes[6], value.bytes[7], value.bytes[8], value.bytes[9], value.bytes[10], value.bytes[11],
value.bytes[12], value.bytes[13], value.bytes[14], value.bytes[15], value.bytes[16], value.bytes[17],
value.bytes[18], value.bytes[19], value.bytes[20], value.bytes[21], value.bytes[22], value.bytes[23],
value.bytes[24], value.bytes[25], value.bytes[26], value.bytes[27], value.bytes[28], value.bytes[29],
value.bytes[30], value.bytes[31])
let result = self.call(SMCKeys.kernelIndex.rawValue, input: &input, output: &output)
if result != kIOReturnSuccess {
return result
}
return kIOReturnSuccess
}
private func call(_ index: UInt8, input: inout SMCKeyData_t, output: inout SMCKeyData_t) -> kern_return_t {
let inputSize = MemoryLayout<SMCKeyData_t>.stride
var outputSize = MemoryLayout<SMCKeyData_t>.stride
return IOConnectCallStructMethod(conn, UInt32(index), &input, inputSize, &output, &outputSize)
}
}