mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 23:54:32 +00:00
Merge pull request #104 from Ebullioscopic/feature/stats
Update Polling Interval and Timeout enable/disable for Stats
This commit is contained in:
commit
4ce1b0753d
9 changed files with 179 additions and 1061 deletions
|
|
@ -615,17 +615,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
KeyboardShortcuts.isEnabled = Defaults[.enableShortcuts]
|
KeyboardShortcuts.isEnabled = Defaults[.enableShortcuts]
|
||||||
|
|
||||||
_ = KeyboardShortcuts.onKeyDown(for: .statsPanel) {
|
|
||||||
guard Defaults[.enableShortcuts] else { return }
|
|
||||||
guard Defaults[.enableStatsFeature] && Defaults[.showStatsPanel] else { return }
|
|
||||||
|
|
||||||
if !StatsManager.shared.isMonitoring {
|
|
||||||
StatsManager.shared.startMonitoring()
|
|
||||||
}
|
|
||||||
|
|
||||||
StatsPanelManager.shared.toggleStatsPanel()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !Defaults[.showOnAllDisplays] {
|
if !Defaults[.showOnAllDisplays] {
|
||||||
let viewModel = self.vm
|
let viewModel = self.vm
|
||||||
let window = createDynamicIslandWindow(
|
let window = createDynamicIslandWindow(
|
||||||
|
|
|
||||||
|
|
@ -1321,6 +1321,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"• Updated %@" : {
|
"• Updated %@" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -5014,6 +5015,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Clear" : {
|
"Clear" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -5614,6 +5616,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Controls how often system metrics refresh while monitoring is active." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Copy event link" : {
|
"Copy event link" : {
|
||||||
"comment" : "A tooltip for a button that copies the link of a reminder event to the clipboard.",
|
"comment" : "A tooltip for a button that copies the link of a reminder event to the clipboard.",
|
||||||
|
|
@ -7857,9 +7862,6 @@
|
||||||
},
|
},
|
||||||
"Enable stats monitoring in Settings to view system performance data." : {
|
"Enable stats monitoring in Settings to view system performance data." : {
|
||||||
|
|
||||||
},
|
|
||||||
"Enable system stats monitoring in Settings to view detailed performance data." : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Enable Thinking Mode" : {
|
"Enable Thinking Mode" : {
|
||||||
|
|
||||||
|
|
@ -9617,6 +9619,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"High-frequency updates without a timeout can increase battery usage." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"History Size" : {
|
"History Size" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -10527,6 +10532,7 @@
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Live Monitoring" : {
|
"Live Monitoring" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -12581,6 +12587,9 @@
|
||||||
},
|
},
|
||||||
"Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history." : {
|
"Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Monitoring Behavior" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Monitoring Status" : {
|
"Monitoring Status" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -12593,6 +12602,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Monitoring Stopped" : {
|
"Monitoring Stopped" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -14512,9 +14522,6 @@
|
||||||
},
|
},
|
||||||
"Open Model Settings" : {
|
"Open Model Settings" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Open Settings" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Open System Settings" : {
|
"Open System Settings" : {
|
||||||
|
|
||||||
|
|
@ -14527,9 +14534,6 @@
|
||||||
},
|
},
|
||||||
"Opens the color picker panel for screen color capture. Default is Cmd+Shift+P. Only works when color picker feature is enabled." : {
|
"Opens the color picker panel for screen color capture. Default is Cmd+Shift+P. Only works when color picker feature is enabled." : {
|
||||||
|
|
||||||
},
|
|
||||||
"Opens the detailed system performance monitor panel. Default is Cmd+Shift+S. Only works when stats feature is enabled." : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Output devices" : {
|
"Output devices" : {
|
||||||
"comment" : "Section title for the list of available audio output devices in the media output selector.",
|
"comment" : "Section title for the list of available audio output devices in the media output selector.",
|
||||||
|
|
@ -15677,6 +15681,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Sampling can continue while the notch is closed when the timeout is disabled." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Save" : {
|
"Save" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -17733,6 +17740,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Start" : {
|
"Start" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17823,6 +17831,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Stats Feature Disabled" : {
|
"Stats Feature Disabled" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17833,6 +17842,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Stats feature is disabled" : {
|
"Stats feature is disabled" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17841,11 +17851,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Stats monitoring displays performance graphs in the Dynamic Island. The detailed panel (Cmd+Shift+S) shows Activity Monitor-style detailed graphs and metrics." : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Stats panel is disabled" : {
|
"Stats panel is disabled" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17856,6 +17864,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Stats Panel:" : {
|
"Stats Panel:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -17864,6 +17873,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Stats update interval" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Status & Actions" : {
|
"Status & Actions" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -18253,6 +18265,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"System Performance Monitor" : {
|
"System Performance Monitor" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -18289,6 +18302,7 @@
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Time Range" : {
|
"Time Range" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|
@ -19121,6 +19135,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Update interval" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Update Timer" : {
|
"Update Timer" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
|
|
@ -19955,6 +19972,9 @@
|
||||||
},
|
},
|
||||||
"When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : {
|
"When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"When enabled, stats monitoring stops a few seconds after the notch closes." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"When enabled, the Dynamic Island will expand horizontally to match the animation size" : {
|
"When enabled, the Dynamic Island will expand horizontally to match the animation size" : {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ extension KeyboardShortcuts.Name {
|
||||||
static let clipboardHistoryPanel = Self("clipboardHistoryPanel", default: .init(.c, modifiers: [.shift, .command]))
|
static let clipboardHistoryPanel = Self("clipboardHistoryPanel", default: .init(.c, modifiers: [.shift, .command]))
|
||||||
static let colorPickerPanel = Self("colorPickerPanel", default: .init(.p, modifiers: [.shift, .command]))
|
static let colorPickerPanel = Self("colorPickerPanel", default: .init(.p, modifiers: [.shift, .command]))
|
||||||
static let screenAssistantPanel = Self("screenAssistantPanel", default: .init(.a, modifiers: [.shift, .command]))
|
static let screenAssistantPanel = Self("screenAssistantPanel", default: .init(.a, modifiers: [.shift, .command]))
|
||||||
static let statsPanel = Self("statsPanel", default: .init(.s, modifiers: [.shift, .command]))
|
|
||||||
static let toggleMicrophone = Self("toggleMicrophone", default: .init(.f5, modifiers: [.function]))
|
static let toggleMicrophone = Self("toggleMicrophone", default: .init(.f5, modifiers: [.function]))
|
||||||
static let decreaseBacklight = Self("decreaseBacklight", default: .init(.f1, modifiers: [.command]))
|
static let decreaseBacklight = Self("decreaseBacklight", default: .init(.f1, modifiers: [.command]))
|
||||||
static let increaseBacklight = Self("increaseBacklight", default: .init(.f2, modifiers: [.command]))
|
static let increaseBacklight = Self("increaseBacklight", default: .init(.f2, modifiers: [.command]))
|
||||||
|
|
|
||||||
|
|
@ -1745,34 +1745,6 @@ struct Shortcuts: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
|
||||||
HStack {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
KeyboardShortcuts.Recorder("Stats Panel:", name: .statsPanel)
|
|
||||||
.disabled(!enableShortcuts || !enableStatsFeature || !Defaults[.showStatsPanel])
|
|
||||||
if !enableStatsFeature {
|
|
||||||
Text("Stats feature is disabled")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.padding(.top, 2)
|
|
||||||
} else if !Defaults[.showStatsPanel] {
|
|
||||||
Text("Stats panel is disabled")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.padding(.top, 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Stats")
|
|
||||||
} footer: {
|
|
||||||
Text("Opens the detailed system performance monitor panel. Default is Cmd+Shift+S. Only works when stats feature is enabled.")
|
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
|
|
@ -2260,16 +2232,32 @@ private struct TimerPresetComponentControl: View {
|
||||||
struct StatsSettings: View {
|
struct StatsSettings: View {
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
@ObservedObject var statsManager = StatsManager.shared
|
||||||
@Default(.enableStatsFeature) var enableStatsFeature
|
@Default(.enableStatsFeature) var enableStatsFeature
|
||||||
|
@Default(.statsStopWhenNotchCloses) var statsStopWhenNotchCloses
|
||||||
|
@Default(.statsUpdateInterval) var statsUpdateInterval
|
||||||
@Default(.showCpuGraph) var showCpuGraph
|
@Default(.showCpuGraph) var showCpuGraph
|
||||||
@Default(.showMemoryGraph) var showMemoryGraph
|
@Default(.showMemoryGraph) var showMemoryGraph
|
||||||
@Default(.showGpuGraph) var showGpuGraph
|
@Default(.showGpuGraph) var showGpuGraph
|
||||||
@Default(.showNetworkGraph) var showNetworkGraph
|
@Default(.showNetworkGraph) var showNetworkGraph
|
||||||
@Default(.showDiskGraph) var showDiskGraph
|
@Default(.showDiskGraph) var showDiskGraph
|
||||||
@Default(.showStatsPanel) var showStatsPanel
|
|
||||||
|
|
||||||
var enabledGraphsCount: Int {
|
var enabledGraphsCount: Int {
|
||||||
[showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count
|
[showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var formattedUpdateInterval: String {
|
||||||
|
let seconds = Int(statsUpdateInterval.rounded())
|
||||||
|
if seconds >= 60 {
|
||||||
|
return "60 s (1 min)"
|
||||||
|
} else if seconds == 1 {
|
||||||
|
return "1 s"
|
||||||
|
} else {
|
||||||
|
return "\(seconds) s"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var shouldShowStatsBatteryWarning: Bool {
|
||||||
|
!statsStopWhenNotchCloses && statsUpdateInterval <= 5
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|
@ -2282,26 +2270,55 @@ struct StatsSettings: View {
|
||||||
// Note: Smart monitoring will handle starting when switching to stats tab
|
// Note: Smart monitoring will handle starting when switching to stats tab
|
||||||
}
|
}
|
||||||
|
|
||||||
if enableStatsFeature {
|
|
||||||
Defaults.Toggle("Enable detailed stats panel", key: .showStatsPanel)
|
|
||||||
}
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("General")
|
Text("General")
|
||||||
} footer: {
|
} footer: {
|
||||||
if enableStatsFeature && showStatsPanel {
|
Text("When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery.")
|
||||||
Text("Stats monitoring displays performance graphs in the Dynamic Island. The detailed panel (Cmd+Shift+S) shows Activity Monitor-style detailed graphs and metrics.")
|
.multilineTextAlignment(.trailing)
|
||||||
.multilineTextAlignment(.trailing)
|
.foregroundStyle(.secondary)
|
||||||
.foregroundStyle(.secondary)
|
.font(.caption)
|
||||||
|
}
|
||||||
|
|
||||||
|
if enableStatsFeature {
|
||||||
|
Section {
|
||||||
|
Defaults.Toggle("Stop monitoring after closing the notch", key: .statsStopWhenNotchCloses)
|
||||||
|
.help("When enabled, stats monitoring stops a few seconds after the notch closes.")
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("Update interval")
|
||||||
|
Spacer()
|
||||||
|
Text(formattedUpdateInterval)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Slider(value: $statsUpdateInterval, in: 1...60, step: 1)
|
||||||
|
.accessibilityLabel("Stats update interval")
|
||||||
|
|
||||||
|
Text("Controls how often system metrics refresh while monitoring is active.")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldShowStatsBatteryWarning {
|
||||||
|
Label {
|
||||||
|
Text("High-frequency updates without a timeout can increase battery usage.")
|
||||||
|
} icon: {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
}
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
} else {
|
.foregroundStyle(.orange)
|
||||||
Text("When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery.")
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Monitoring Behavior")
|
||||||
|
} footer: {
|
||||||
|
Text("Sampling can continue while the notch is closed when the timeout is disabled.")
|
||||||
.multilineTextAlignment(.trailing)
|
.multilineTextAlignment(.trailing)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if enableStatsFeature {
|
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("CPU Usage", key: .showCpuGraph)
|
Defaults.Toggle("CPU Usage", key: .showCpuGraph)
|
||||||
Defaults.Toggle("Memory Usage", key: .showMemoryGraph)
|
Defaults.Toggle("Memory Usage", key: .showMemoryGraph)
|
||||||
|
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
//
|
|
||||||
// StatsPanel.swift
|
|
||||||
// DynamicIsland
|
|
||||||
//
|
|
||||||
// Created by Ebullioscopic on 13/08/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppKit
|
|
||||||
import SwiftUI
|
|
||||||
import Defaults
|
|
||||||
|
|
||||||
class StatsPanel: NSPanel {
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(
|
|
||||||
contentRect: .zero,
|
|
||||||
styleMask: [.borderless, .nonactivatingPanel],
|
|
||||||
backing: .buffered,
|
|
||||||
defer: true
|
|
||||||
)
|
|
||||||
|
|
||||||
setupWindow()
|
|
||||||
setupContentView()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override to allow the panel to become key window (required for user interaction)
|
|
||||||
override var canBecomeKey: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override to allow the panel to become main window (required for text input and controls)
|
|
||||||
override var canBecomeMain: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupWindow() {
|
|
||||||
backgroundColor = .clear
|
|
||||||
isOpaque = false
|
|
||||||
hasShadow = true
|
|
||||||
level = .floating
|
|
||||||
isMovableByWindowBackground = true // Enable dragging
|
|
||||||
titlebarAppearsTransparent = true
|
|
||||||
titleVisibility = .hidden
|
|
||||||
isFloatingPanel = true // Mark as floating panel for proper behavior
|
|
||||||
|
|
||||||
// Allow dragging from any part of the window
|
|
||||||
styleMask.insert(.fullSizeContentView)
|
|
||||||
|
|
||||||
collectionBehavior = [
|
|
||||||
.canJoinAllSpaces,
|
|
||||||
.stationary,
|
|
||||||
.fullScreenAuxiliary // Float above full-screen apps
|
|
||||||
]
|
|
||||||
|
|
||||||
// Apply screen capture hiding setting
|
|
||||||
updateScreenCaptureVisibility()
|
|
||||||
setupScreenCaptureObserver()
|
|
||||||
|
|
||||||
// Accept mouse moved events for proper hover behavior
|
|
||||||
acceptsMouseMovedEvents = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupContentView() {
|
|
||||||
let contentView = StatsPanelView {
|
|
||||||
self.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
let hostingView = NSHostingView(rootView: contentView)
|
|
||||||
self.contentView = hostingView
|
|
||||||
|
|
||||||
// Set initial size
|
|
||||||
let preferredSize = CGSize(width: 600, height: 400)
|
|
||||||
hostingView.setFrameSize(preferredSize)
|
|
||||||
setContentSize(preferredSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
func positionNearNotch() {
|
|
||||||
guard let screen = NSScreen.main else { return }
|
|
||||||
|
|
||||||
let screenFrame = screen.visibleFrame
|
|
||||||
let panelFrame = frame
|
|
||||||
|
|
||||||
// Position at top center of screen (near where the notch would be)
|
|
||||||
let xPosition = (screenFrame.width - panelFrame.width) / 2 + screenFrame.minX
|
|
||||||
let yPosition = screenFrame.maxY - panelFrame.height - 10 // 10px from top
|
|
||||||
|
|
||||||
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
|
||||||
}
|
|
||||||
|
|
||||||
func positionNearMouse() {
|
|
||||||
let mouseLocation = NSEvent.mouseLocation
|
|
||||||
let panelFrame = frame
|
|
||||||
|
|
||||||
// Position near mouse but ensure it stays on screen
|
|
||||||
guard let screen = NSScreen.main else { return }
|
|
||||||
let screenFrame = screen.visibleFrame
|
|
||||||
|
|
||||||
var xPosition = mouseLocation.x - panelFrame.width / 2
|
|
||||||
var yPosition = mouseLocation.y - panelFrame.height - 20
|
|
||||||
|
|
||||||
// Keep within screen bounds
|
|
||||||
xPosition = max(screenFrame.minX + 10, min(xPosition, screenFrame.maxX - panelFrame.width - 10))
|
|
||||||
yPosition = max(screenFrame.minY + 10, min(yPosition, screenFrame.maxY - panelFrame.height - 10))
|
|
||||||
|
|
||||||
setFrameOrigin(NSPoint(x: xPosition, y: yPosition))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupScreenCaptureObserver() {
|
|
||||||
// Observe changes to hidePanelsFromScreenCapture setting
|
|
||||||
Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.updateScreenCaptureVisibility()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateScreenCaptureVisibility() {
|
|
||||||
let shouldHide = Defaults[.hidePanelsFromScreenCapture]
|
|
||||||
|
|
||||||
if shouldHide {
|
|
||||||
// Hide from screen capture and recording
|
|
||||||
self.sharingType = .none
|
|
||||||
print("🙈 StatsPanel: Hidden from screen capture and recordings")
|
|
||||||
} else {
|
|
||||||
// Allow normal screen capture
|
|
||||||
self.sharingType = .readOnly
|
|
||||||
print("👁️ StatsPanel: Visible in screen capture and recordings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,798 +0,0 @@
|
||||||
//
|
|
||||||
// StatsPanelView.swift
|
|
||||||
// DynamicIsland
|
|
||||||
//
|
|
||||||
// Created by Ebullioscopic on 13/08/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Defaults
|
|
||||||
|
|
||||||
struct StatsPanelView: View {
|
|
||||||
let onClose: () -> Void
|
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
|
||||||
@Default(.enableStatsFeature) var enableStatsFeature
|
|
||||||
@State private var selectedTimeRange: TimeRange = .last30
|
|
||||||
@State private var refreshTimer: Timer?
|
|
||||||
|
|
||||||
enum TimeRange: String, CaseIterable {
|
|
||||||
case last30 = "Last 30 sec"
|
|
||||||
case last60 = "Last 1 min"
|
|
||||||
case last300 = "Last 5 min"
|
|
||||||
|
|
||||||
var dataPoints: Int {
|
|
||||||
switch self {
|
|
||||||
case .last30: return 30
|
|
||||||
case .last60: return 60
|
|
||||||
case .last300: return 300
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
// Header with controls
|
|
||||||
StatsPanelHeader(
|
|
||||||
selectedTimeRange: $selectedTimeRange,
|
|
||||||
onClose: onClose
|
|
||||||
)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Main content
|
|
||||||
if !enableStatsFeature {
|
|
||||||
// Disabled state
|
|
||||||
VStack(spacing: 16) {
|
|
||||||
Image(systemName: "chart.line.uptrend.xyaxis")
|
|
||||||
.font(.system(size: 48))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text("Stats Feature Disabled")
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Text("Enable system stats monitoring in Settings to view detailed performance data.")
|
|
||||||
.font(.body)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.padding(.horizontal, 40)
|
|
||||||
|
|
||||||
Button("Open Settings") {
|
|
||||||
// TODO: Add settings navigation
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
||||||
} else {
|
|
||||||
// Stats content - Compact two-row layout
|
|
||||||
CPUStatsDetailView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.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)
|
|
||||||
.onAppear {
|
|
||||||
// Note: Smart monitoring will handle starting/stopping based on UI state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Header Component
|
|
||||||
struct StatsPanelHeader: View {
|
|
||||||
@Binding var selectedTimeRange: StatsPanelView.TimeRange
|
|
||||||
let onClose: () -> Void
|
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack {
|
|
||||||
// Title and status
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
Image(systemName: "chart.line.uptrend.xyaxis")
|
|
||||||
.foregroundColor(.blue)
|
|
||||||
.font(.system(size: 18, weight: .medium))
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text("System Performance Monitor")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Circle()
|
|
||||||
.fill(statsManager.isMonitoring ? .green : .red)
|
|
||||||
.frame(width: 6, height: 6)
|
|
||||||
|
|
||||||
Text(statsManager.isMonitoring ? "Live Monitoring" : "Monitoring Stopped")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
if statsManager.isMonitoring {
|
|
||||||
Text("• Updated \(formatLastUpdated(statsManager.lastUpdated))")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
// Controls
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
// Time range picker
|
|
||||||
Picker("Time Range", selection: $selectedTimeRange) {
|
|
||||||
ForEach(StatsPanelView.TimeRange.allCases, id: \.self) { range in
|
|
||||||
Text(range.rawValue).tag(range)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.menu)
|
|
||||||
.frame(width: 120)
|
|
||||||
|
|
||||||
// Monitoring controls
|
|
||||||
if statsManager.isMonitoring {
|
|
||||||
Button("Stop") {
|
|
||||||
statsManager.stopMonitoring()
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
} else {
|
|
||||||
Button("Start") {
|
|
||||||
statsManager.startMonitoring()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("Clear") {
|
|
||||||
statsManager.clearHistory()
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.disabled(statsManager.isMonitoring)
|
|
||||||
|
|
||||||
// Close button
|
|
||||||
Button(action: onClose) {
|
|
||||||
Image(systemName: "xmark")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.font(.system(size: 12, weight: .medium))
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
.padding(.vertical, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func formatLastUpdated(_ date: Date) -> String {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.timeStyle = .medium
|
|
||||||
return formatter.string(from: date)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Section Container
|
|
||||||
struct StatsPanelSection<Content: View>: View {
|
|
||||||
let title: String
|
|
||||||
@ViewBuilder let content: Content
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text(title)
|
|
||||||
.font(.title3)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
content
|
|
||||||
.padding(16)
|
|
||||||
.background(Color.gray.opacity(0.1))
|
|
||||||
.cornerRadius(12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - System Overview
|
|
||||||
struct SystemOverviewGrid: View {
|
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
|
|
||||||
OverviewCard(
|
|
||||||
title: "CPU",
|
|
||||||
value: statsManager.cpuUsageString,
|
|
||||||
color: .blue,
|
|
||||||
icon: "cpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewCard(
|
|
||||||
title: "Memory",
|
|
||||||
value: statsManager.memoryUsageString,
|
|
||||||
color: .green,
|
|
||||||
icon: "memorychip"
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewCard(
|
|
||||||
title: "GPU",
|
|
||||||
value: statsManager.gpuUsageString,
|
|
||||||
color: .purple,
|
|
||||||
icon: "display"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OverviewCard: View {
|
|
||||||
let title: String
|
|
||||||
let value: String
|
|
||||||
let color: Color
|
|
||||||
let icon: String
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.foregroundColor(color)
|
|
||||||
.font(.title3)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text(value)
|
|
||||||
.font(.title2)
|
|
||||||
.fontWeight(.bold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(12)
|
|
||||||
.background(Color.gray.opacity(0.05))
|
|
||||||
.cornerRadius(8)
|
|
||||||
.frame(height: 80)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Detailed Views (Placeholder implementations)
|
|
||||||
struct DetailedCPUView: View {
|
|
||||||
let timeRange: StatsPanelView.TimeRange
|
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
// Large timeline graph
|
|
||||||
DetailedTimelineGraph(
|
|
||||||
data: statsManager.cpuHistory,
|
|
||||||
color: .blue,
|
|
||||||
title: "CPU Usage Over Time",
|
|
||||||
unit: "%",
|
|
||||||
timeRange: timeRange
|
|
||||||
)
|
|
||||||
|
|
||||||
// CPU details
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 12) {
|
|
||||||
DetailMetricCard(title: "Current", value: statsManager.cpuUsageString, color: .blue)
|
|
||||||
DetailMetricCard(title: "Average", value: String(format: "%.1f%%", statsManager.avgCpuUsage), color: .blue)
|
|
||||||
DetailMetricCard(title: "Peak", value: String(format: "%.1f%%", statsManager.maxCpuUsage), color: .blue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DetailedMemoryView: View {
|
|
||||||
let timeRange: StatsPanelView.TimeRange
|
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
DetailedTimelineGraph(
|
|
||||||
data: statsManager.memoryHistory,
|
|
||||||
color: .green,
|
|
||||||
title: "Memory Usage Over Time",
|
|
||||||
unit: "%",
|
|
||||||
timeRange: timeRange
|
|
||||||
)
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 12) {
|
|
||||||
DetailMetricCard(title: "Current", value: statsManager.memoryUsageString, color: .green)
|
|
||||||
DetailMetricCard(title: "Average", value: String(format: "%.1f%%", statsManager.avgMemoryUsage), color: .green)
|
|
||||||
DetailMetricCard(title: "Peak", value: String(format: "%.1f%%", statsManager.maxMemoryUsage), color: .green)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DetailedNetworkView: View {
|
|
||||||
let timeRange: StatsPanelView.TimeRange
|
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
DualTimelineGraph(
|
|
||||||
positiveData: statsManager.networkDownloadHistory,
|
|
||||||
negativeData: statsManager.networkUploadHistory,
|
|
||||||
positiveColor: .orange,
|
|
||||||
negativeColor: .red,
|
|
||||||
title: "Network Activity Over Time",
|
|
||||||
positiveLabel: "Download",
|
|
||||||
negativeLabel: "Upload",
|
|
||||||
unit: "MB/s",
|
|
||||||
timeRange: timeRange
|
|
||||||
)
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
|
|
||||||
DetailMetricCard(title: "Download", value: statsManager.networkDownloadString, color: .orange)
|
|
||||||
DetailMetricCard(title: "Upload", value: statsManager.networkUploadString, color: .red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DetailedDiskView: View {
|
|
||||||
let timeRange: StatsPanelView.TimeRange
|
|
||||||
@ObservedObject var statsManager = StatsManager.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
DualTimelineGraph(
|
|
||||||
positiveData: statsManager.diskReadHistory,
|
|
||||||
negativeData: statsManager.diskWriteHistory,
|
|
||||||
positiveColor: .cyan,
|
|
||||||
negativeColor: .yellow,
|
|
||||||
title: "Disk Activity Over Time",
|
|
||||||
positiveLabel: "Read",
|
|
||||||
negativeLabel: "Write",
|
|
||||||
unit: "MB/s",
|
|
||||||
timeRange: timeRange
|
|
||||||
)
|
|
||||||
|
|
||||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), spacing: 12) {
|
|
||||||
DetailMetricCard(title: "Read", value: statsManager.diskReadString, color: .cyan)
|
|
||||||
DetailMetricCard(title: "Write", value: statsManager.diskWriteString, color: .yellow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DetailMetricCard: View {
|
|
||||||
let title: String
|
|
||||||
let value: String
|
|
||||||
let color: Color
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text(value)
|
|
||||||
.font(.title3)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(color)
|
|
||||||
}
|
|
||||||
.padding(8)
|
|
||||||
.background(Color.gray.opacity(0.05))
|
|
||||||
.cornerRadius(6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Enhanced Graph Components
|
|
||||||
struct DetailedTimelineGraph: View {
|
|
||||||
let data: [Double]
|
|
||||||
let color: Color
|
|
||||||
let title: String
|
|
||||||
let unit: String
|
|
||||||
let timeRange: StatsPanelView.TimeRange
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
GeometryReader { geometry in
|
|
||||||
let maxValue = data.max() ?? 1.0
|
|
||||||
let normalizedData = maxValue > 0 ? data.map { $0 / maxValue } : data
|
|
||||||
|
|
||||||
ZStack {
|
|
||||||
// Grid lines
|
|
||||||
Path { path in
|
|
||||||
let horizontalLines = 5
|
|
||||||
for i in 0...horizontalLines {
|
|
||||||
let y = geometry.size.height * CGFloat(i) / CGFloat(horizontalLines)
|
|
||||||
path.move(to: CGPoint(x: 0, y: y))
|
|
||||||
path.addLine(to: CGPoint(x: geometry.size.width, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stroke(Color.gray.opacity(0.2), lineWidth: 0.5)
|
|
||||||
|
|
||||||
// Data line
|
|
||||||
Path { path in
|
|
||||||
guard !normalizedData.isEmpty else { return }
|
|
||||||
|
|
||||||
let stepX = geometry.size.width / CGFloat(max(1, normalizedData.count - 1))
|
|
||||||
|
|
||||||
for (index, value) in normalizedData.enumerated() {
|
|
||||||
let x = CGFloat(index) * stepX
|
|
||||||
let y = geometry.size.height * (1 - CGFloat(value))
|
|
||||||
|
|
||||||
if index == 0 {
|
|
||||||
path.move(to: CGPoint(x: x, y: y))
|
|
||||||
} else {
|
|
||||||
path.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stroke(color, lineWidth: 2)
|
|
||||||
|
|
||||||
// Fill area
|
|
||||||
Path { path in
|
|
||||||
guard !normalizedData.isEmpty else { return }
|
|
||||||
|
|
||||||
let stepX = geometry.size.width / CGFloat(max(1, normalizedData.count - 1))
|
|
||||||
|
|
||||||
path.move(to: CGPoint(x: 0, y: geometry.size.height))
|
|
||||||
|
|
||||||
for (index, value) in normalizedData.enumerated() {
|
|
||||||
let x = CGFloat(index) * stepX
|
|
||||||
let y = geometry.size.height * (1 - CGFloat(value))
|
|
||||||
path.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
}
|
|
||||||
|
|
||||||
path.addLine(to: CGPoint(x: geometry.size.width, y: geometry.size.height))
|
|
||||||
path.closeSubpath()
|
|
||||||
}
|
|
||||||
.fill(
|
|
||||||
LinearGradient(
|
|
||||||
gradient: Gradient(colors: [color.opacity(0.3), color.opacity(0.1)]),
|
|
||||||
startPoint: .top,
|
|
||||||
endPoint: .bottom
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 120)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DualTimelineGraph: View {
|
|
||||||
let positiveData: [Double]
|
|
||||||
let negativeData: [Double]
|
|
||||||
let positiveColor: Color
|
|
||||||
let negativeColor: Color
|
|
||||||
let title: String
|
|
||||||
let positiveLabel: String
|
|
||||||
let negativeLabel: String
|
|
||||||
let unit: String
|
|
||||||
let timeRange: StatsPanelView.TimeRange
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
HStack {
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Circle()
|
|
||||||
.fill(positiveColor)
|
|
||||||
.frame(width: 8, height: 8)
|
|
||||||
Text(positiveLabel)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Circle()
|
|
||||||
.fill(negativeColor)
|
|
||||||
.frame(width: 8, height: 8)
|
|
||||||
Text(negativeLabel)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GeometryReader { geometry in
|
|
||||||
let maxPositive = positiveData.max() ?? 1.0
|
|
||||||
let maxNegative = negativeData.max() ?? 1.0
|
|
||||||
let maxValue = max(maxPositive, maxNegative)
|
|
||||||
|
|
||||||
let normalizedPositive = maxValue > 0 ? positiveData.map { $0 / maxValue } : positiveData
|
|
||||||
let normalizedNegative = maxValue > 0 ? negativeData.map { $0 / maxValue } : negativeData
|
|
||||||
|
|
||||||
let centerY = geometry.size.height / 2
|
|
||||||
|
|
||||||
ZStack {
|
|
||||||
// Center line
|
|
||||||
Path { path in
|
|
||||||
path.move(to: CGPoint(x: 0, y: centerY))
|
|
||||||
path.addLine(to: CGPoint(x: geometry.size.width, y: centerY))
|
|
||||||
}
|
|
||||||
.stroke(Color.gray.opacity(0.3), lineWidth: 1)
|
|
||||||
|
|
||||||
// Positive line and fill
|
|
||||||
Path { path in
|
|
||||||
guard !normalizedPositive.isEmpty else { return }
|
|
||||||
|
|
||||||
let stepX = geometry.size.width / CGFloat(max(1, normalizedPositive.count - 1))
|
|
||||||
|
|
||||||
for (index, value) in normalizedPositive.enumerated() {
|
|
||||||
let x = CGFloat(index) * stepX
|
|
||||||
let y = centerY - (centerY * CGFloat(value))
|
|
||||||
|
|
||||||
if index == 0 {
|
|
||||||
path.move(to: CGPoint(x: x, y: y))
|
|
||||||
} else {
|
|
||||||
path.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stroke(positiveColor, lineWidth: 2)
|
|
||||||
|
|
||||||
// Negative line and fill
|
|
||||||
Path { path in
|
|
||||||
guard !normalizedNegative.isEmpty else { return }
|
|
||||||
|
|
||||||
let stepX = geometry.size.width / CGFloat(max(1, normalizedNegative.count - 1))
|
|
||||||
|
|
||||||
for (index, value) in normalizedNegative.enumerated() {
|
|
||||||
let x = CGFloat(index) * stepX
|
|
||||||
let y = centerY + (centerY * CGFloat(value))
|
|
||||||
|
|
||||||
if index == 0 {
|
|
||||||
path.move(to: CGPoint(x: x, y: y))
|
|
||||||
} else {
|
|
||||||
path.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stroke(negativeColor, lineWidth: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 120)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Compact Components for Two-Row Layout
|
|
||||||
|
|
||||||
struct CompactOverviewCard: View {
|
|
||||||
let title: String
|
|
||||||
let value: String
|
|
||||||
let color: Color
|
|
||||||
let icon: String
|
|
||||||
let data: [Double]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
// Header
|
|
||||||
HStack {
|
|
||||||
Image(systemName: icon)
|
|
||||||
.foregroundColor(color)
|
|
||||||
.font(.title3)
|
|
||||||
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text(value)
|
|
||||||
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
|
||||||
.foregroundColor(color)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mini graph
|
|
||||||
CompactGraph(data: data, color: color)
|
|
||||||
.frame(height: 40)
|
|
||||||
}
|
|
||||||
.padding(12)
|
|
||||||
.background(Color.gray.opacity(0.05))
|
|
||||||
.cornerRadius(8)
|
|
||||||
.frame(height: 80)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CompactGraph: View {
|
|
||||||
let data: [Double]
|
|
||||||
let color: Color
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Canvas { context, size in
|
|
||||||
guard data.count > 1 else { return }
|
|
||||||
|
|
||||||
let maxValue = data.max() ?? 1.0
|
|
||||||
let path = Path { path in
|
|
||||||
let dataToUse = Array(data.suffix(30)) // Last 30 points
|
|
||||||
let stepX = size.width / CGFloat(max(1, dataToUse.count - 1))
|
|
||||||
|
|
||||||
for (index, value) in dataToUse.enumerated() {
|
|
||||||
let x = CGFloat(index) * stepX
|
|
||||||
let normalizedValue = maxValue > 0 ? min(value / maxValue, 1.0) : 0
|
|
||||||
let y = size.height - (CGFloat(normalizedValue) * size.height)
|
|
||||||
|
|
||||||
if index == 0 {
|
|
||||||
path.move(to: CGPoint(x: x, y: y))
|
|
||||||
} else {
|
|
||||||
path.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill area
|
|
||||||
let fillPath = Path { fillPath in
|
|
||||||
let dataToUse = Array(data.suffix(30))
|
|
||||||
let stepX = size.width / CGFloat(max(1, dataToUse.count - 1))
|
|
||||||
|
|
||||||
fillPath.move(to: CGPoint(x: 0, y: size.height))
|
|
||||||
|
|
||||||
for (index, value) in dataToUse.enumerated() {
|
|
||||||
let x = CGFloat(index) * stepX
|
|
||||||
let normalizedValue = maxValue > 0 ? min(value / maxValue, 1.0) : 0
|
|
||||||
let y = size.height - (CGFloat(normalizedValue) * size.height)
|
|
||||||
|
|
||||||
if index == 0 {
|
|
||||||
fillPath.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
} else {
|
|
||||||
fillPath.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !dataToUse.isEmpty {
|
|
||||||
fillPath.addLine(to: CGPoint(x: size.width, y: size.height))
|
|
||||||
}
|
|
||||||
fillPath.closeSubpath()
|
|
||||||
}
|
|
||||||
|
|
||||||
context.fill(
|
|
||||||
fillPath,
|
|
||||||
with: .linearGradient(
|
|
||||||
Gradient(colors: [color.opacity(0.3), color.opacity(0.1), Color.clear]),
|
|
||||||
startPoint: CGPoint(x: 0, y: 0),
|
|
||||||
endPoint: CGPoint(x: 0, y: size.height)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
context.stroke(path, with: .color(color), lineWidth: 1.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CompactDualGraph: View {
|
|
||||||
let title: String
|
|
||||||
let primaryLabel: String
|
|
||||||
let primaryValue: String
|
|
||||||
let primaryData: [Double]
|
|
||||||
let primaryColor: Color
|
|
||||||
let secondaryLabel: String
|
|
||||||
let secondaryValue: String
|
|
||||||
let secondaryData: [Double]
|
|
||||||
let secondaryColor: Color
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
// Header
|
|
||||||
HStack {
|
|
||||||
Text(title)
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legend and values
|
|
||||||
HStack {
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Circle()
|
|
||||||
.fill(primaryColor)
|
|
||||||
.frame(width: 6, height: 6)
|
|
||||||
Text(primaryLabel)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text(primaryValue)
|
|
||||||
.font(.system(size: 10, weight: .medium, design: .monospaced))
|
|
||||||
.foregroundColor(primaryColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Circle()
|
|
||||||
.fill(secondaryColor)
|
|
||||||
.frame(width: 6, height: 6)
|
|
||||||
Text(secondaryLabel)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
Text(secondaryValue)
|
|
||||||
.font(.system(size: 10, weight: .medium, design: .monospaced))
|
|
||||||
.foregroundColor(secondaryColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dual graph
|
|
||||||
CompactDualLineGraph(
|
|
||||||
primaryData: primaryData,
|
|
||||||
primaryColor: primaryColor,
|
|
||||||
secondaryData: secondaryData,
|
|
||||||
secondaryColor: secondaryColor
|
|
||||||
)
|
|
||||||
.frame(height: 50)
|
|
||||||
}
|
|
||||||
.padding(12)
|
|
||||||
.background(Color.gray.opacity(0.05))
|
|
||||||
.cornerRadius(8)
|
|
||||||
.frame(height: 100)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CompactDualLineGraph: View {
|
|
||||||
let primaryData: [Double]
|
|
||||||
let primaryColor: Color
|
|
||||||
let secondaryData: [Double]
|
|
||||||
let secondaryColor: Color
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Canvas { context, size in
|
|
||||||
let maxPrimary = primaryData.max() ?? 1.0
|
|
||||||
let maxSecondary = secondaryData.max() ?? 1.0
|
|
||||||
let maxValue = max(maxPrimary, maxSecondary)
|
|
||||||
|
|
||||||
// Primary line
|
|
||||||
if primaryData.count > 1 {
|
|
||||||
let path = Path { path in
|
|
||||||
let dataToUse = Array(primaryData.suffix(30))
|
|
||||||
let stepX = size.width / CGFloat(max(1, dataToUse.count - 1))
|
|
||||||
|
|
||||||
for (index, value) in dataToUse.enumerated() {
|
|
||||||
let x = CGFloat(index) * stepX
|
|
||||||
let normalizedValue = maxValue > 0 ? min(value / maxValue, 1.0) : 0
|
|
||||||
let y = size.height - (CGFloat(normalizedValue) * size.height)
|
|
||||||
|
|
||||||
if index == 0 {
|
|
||||||
path.move(to: CGPoint(x: x, y: y))
|
|
||||||
} else {
|
|
||||||
path.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.stroke(path, with: .color(primaryColor), lineWidth: 1.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Secondary line
|
|
||||||
if secondaryData.count > 1 {
|
|
||||||
let path = Path { path in
|
|
||||||
let dataToUse = Array(secondaryData.suffix(30))
|
|
||||||
let stepX = size.width / CGFloat(max(1, dataToUse.count - 1))
|
|
||||||
|
|
||||||
for (index, value) in dataToUse.enumerated() {
|
|
||||||
let x = CGFloat(index) * stepX
|
|
||||||
let normalizedValue = maxValue > 0 ? min(value / maxValue, 1.0) : 0
|
|
||||||
let y = size.height - (CGFloat(normalizedValue) * size.height)
|
|
||||||
|
|
||||||
if index == 0 {
|
|
||||||
path.move(to: CGPoint(x: x, y: y))
|
|
||||||
} else {
|
|
||||||
path.addLine(to: CGPoint(x: x, y: y))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.stroke(path, with: .color(secondaryColor), lineWidth: 1.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
StatsPanelView {
|
|
||||||
print("Close panel")
|
|
||||||
}
|
|
||||||
.frame(width: 800, height: 600)
|
|
||||||
}
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import IOKit
|
import IOKit
|
||||||
import IOKit.ps
|
import IOKit.ps
|
||||||
|
|
@ -442,6 +443,11 @@ class StatsManager: ObservableObject {
|
||||||
private var isProcessRefreshInFlight = false
|
private var isProcessRefreshInFlight = false
|
||||||
private let gpuCollector = GPUInfoCollector()
|
private let gpuCollector = GPUInfoCollector()
|
||||||
private let cpuSensorCollector = CPUSensorCollector()
|
private let cpuSensorCollector = CPUSensorCollector()
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private let minUpdateInterval: TimeInterval = 1.0
|
||||||
|
private let maxUpdateInterval: TimeInterval = 60.0
|
||||||
|
private let notchCloseStopDelay: TimeInterval = 3.0
|
||||||
|
private let tabSwitchStopDelay: TimeInterval = 0.1
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
private init() {
|
private init() {
|
||||||
|
|
@ -462,6 +468,23 @@ class StatsManager: ObservableObject {
|
||||||
// Initialize baseline disk stats
|
// Initialize baseline disk stats
|
||||||
let initialDiskStats = getDiskStats()
|
let initialDiskStats = getDiskStats()
|
||||||
previousDiskStats = initialDiskStats
|
previousDiskStats = initialDiskStats
|
||||||
|
|
||||||
|
Defaults.publisher(.statsUpdateInterval, options: []).sink { [weak self] change in
|
||||||
|
self?.handleUpdateIntervalChange(change.newValue)
|
||||||
|
}.store(in: &cancellables)
|
||||||
|
|
||||||
|
Defaults.publisher(.statsStopWhenNotchCloses, options: []).sink { [weak self] change in
|
||||||
|
guard let self else { return }
|
||||||
|
|
||||||
|
if change.newValue {
|
||||||
|
if self.isMonitoring && self.lastNotchState == "closed" {
|
||||||
|
self.scheduleDelayedStop(after: self.notchCloseStopDelay)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.delayedStopTimer?.invalidate()
|
||||||
|
self.delayedStopTimer = nil
|
||||||
|
}
|
||||||
|
}.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
|
@ -495,12 +518,14 @@ class StatsManager: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Stop monitoring after 3 seconds (when notch is closed or stats tab is not active)
|
if notchIsOpen && currentView != "stats" {
|
||||||
let delay = notchIsOpen ? 0.1 : 3.0 // Stop quickly when switching tabs, slowly when closing notch
|
scheduleDelayedStop(after: tabSwitchStopDelay)
|
||||||
delayedStopTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in
|
} else if !notchIsOpen {
|
||||||
DispatchQueue.main.async {
|
if Defaults[.statsStopWhenNotchCloses] {
|
||||||
self?.stopMonitoring()
|
scheduleDelayedStop(after: notchCloseStopDelay)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
scheduleDelayedStop(after: tabSwitchStopDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -525,13 +550,10 @@ class StatsManager: ObservableObject {
|
||||||
networkTotals = .zero
|
networkTotals = .zero
|
||||||
diskTotals = .zero
|
diskTotals = .zero
|
||||||
|
|
||||||
// Start periodic monitoring (every 1 second)
|
scheduleMonitoringTimer()
|
||||||
monitoringTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
|
||||||
guard let self = self else { return }
|
Task { @MainActor in
|
||||||
|
self.updateSystemStats()
|
||||||
Task { @MainActor in
|
|
||||||
self.updateSystemStats()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print("StatsManager: Monitoring started")
|
print("StatsManager: Monitoring started")
|
||||||
|
|
@ -568,6 +590,59 @@ class StatsManager: ObservableObject {
|
||||||
cpuTemperature = CPUTemperatureMetrics(celsius: nil)
|
cpuTemperature = CPUTemperatureMetrics(celsius: nil)
|
||||||
cpuFrequency = nil
|
cpuFrequency = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func scheduleMonitoringTimer() {
|
||||||
|
monitoringTimer?.invalidate()
|
||||||
|
|
||||||
|
let configuredInterval = Defaults[.statsUpdateInterval]
|
||||||
|
let interval = validatedUpdateInterval(configuredInterval)
|
||||||
|
|
||||||
|
if abs(interval - configuredInterval) > 0.0001 {
|
||||||
|
Defaults[.statsUpdateInterval] = interval
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoringTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
self.updateSystemStats()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleUpdateIntervalChange(_ newValue: Double) {
|
||||||
|
let interval = validatedUpdateInterval(newValue)
|
||||||
|
|
||||||
|
if abs(interval - newValue) > 0.0001 {
|
||||||
|
Defaults[.statsUpdateInterval] = interval
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard isMonitoring else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.scheduleMonitoringTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validatedUpdateInterval(_ value: Double) -> TimeInterval {
|
||||||
|
min(max(value, minUpdateInterval), maxUpdateInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleDelayedStop(after delay: TimeInterval) {
|
||||||
|
guard delay > 0 else {
|
||||||
|
stopMonitoring()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
delayedStopTimer?.invalidate()
|
||||||
|
|
||||||
|
delayedStopTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self?.stopMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
//
|
|
||||||
// StatsPanelManager.swift
|
|
||||||
// DynamicIsland
|
|
||||||
//
|
|
||||||
// Created by DynamicIsland App on 2024.
|
|
||||||
//
|
|
||||||
|
|
||||||
import AppKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
class StatsPanelManager: ObservableObject {
|
|
||||||
static let shared = StatsPanelManager()
|
|
||||||
|
|
||||||
private var statsPanel: StatsPanel?
|
|
||||||
|
|
||||||
private init() {}
|
|
||||||
|
|
||||||
func showStatsPanel() {
|
|
||||||
hideStatsPanel() // Close any existing panel
|
|
||||||
|
|
||||||
let panel = StatsPanel()
|
|
||||||
panel.positionNearNotch()
|
|
||||||
|
|
||||||
self.statsPanel = panel
|
|
||||||
|
|
||||||
// Make the panel key and order front to ensure it can receive focus
|
|
||||||
panel.makeKeyAndOrderFront(nil)
|
|
||||||
panel.orderFrontRegardless()
|
|
||||||
|
|
||||||
// Activate the app to ensure proper focus handling
|
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
|
|
||||||
// Ensure the panel becomes the key window for input
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
||||||
panel.makeKey()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hideStatsPanel() {
|
|
||||||
statsPanel?.close()
|
|
||||||
statsPanel = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toggleStatsPanel() {
|
|
||||||
if let panel = statsPanel, panel.isVisible {
|
|
||||||
hideStatsPanel()
|
|
||||||
} else {
|
|
||||||
showStatsPanel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isPanelVisible: Bool {
|
|
||||||
return statsPanel?.isVisible ?? false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -446,12 +446,13 @@ extension Defaults.Keys {
|
||||||
// MARK: Stats Feature
|
// MARK: Stats Feature
|
||||||
static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false)
|
static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false)
|
||||||
static let autoStartStatsMonitoring = Key<Bool>("autoStartStatsMonitoring", default: true)
|
static let autoStartStatsMonitoring = Key<Bool>("autoStartStatsMonitoring", default: true)
|
||||||
|
static let statsStopWhenNotchCloses = Key<Bool>("statsStopWhenNotchCloses", default: false)
|
||||||
|
static let statsUpdateInterval = Key<Double>("statsUpdateInterval", default: 1.0)
|
||||||
static let showCpuGraph = Key<Bool>("showCpuGraph", default: true)
|
static let showCpuGraph = Key<Bool>("showCpuGraph", default: true)
|
||||||
static let showMemoryGraph = Key<Bool>("showMemoryGraph", default: true)
|
static let showMemoryGraph = Key<Bool>("showMemoryGraph", default: true)
|
||||||
static let showGpuGraph = Key<Bool>("showGpuGraph", default: true)
|
static let showGpuGraph = Key<Bool>("showGpuGraph", default: true)
|
||||||
static let showNetworkGraph = Key<Bool>("showNetworkGraph", default: false)
|
static let showNetworkGraph = Key<Bool>("showNetworkGraph", default: false)
|
||||||
static let showDiskGraph = Key<Bool>("showDiskGraph", default: false)
|
static let showDiskGraph = Key<Bool>("showDiskGraph", default: false)
|
||||||
static let showStatsPanel = Key<Bool>("showStatsPanel", default: true)
|
|
||||||
|
|
||||||
// MARK: Timer Feature
|
// MARK: Timer Feature
|
||||||
static let enableTimerFeature = Key<Bool>("enableTimerFeature", default: true)
|
static let enableTimerFeature = Key<Bool>("enableTimerFeature", default: true)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue