mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
commit
74b9d2afad
7 changed files with 480 additions and 159 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -71,4 +71,6 @@ boringnotch + meteo + airpods + traduzione modificabile
|
||||||
segmentedhud.swift.ref
|
segmentedhud.swift.ref
|
||||||
gemini-apireference.md
|
gemini-apireference.md
|
||||||
|
|
||||||
ScreenshotApp-main
|
ScreenshotApp-main
|
||||||
|
stats
|
||||||
|
boring.notch
|
||||||
|
|
@ -30,25 +30,34 @@ struct ContentView: View {
|
||||||
@Default(.showNetworkGraph) var showNetworkGraph
|
@Default(.showNetworkGraph) var showNetworkGraph
|
||||||
@Default(.showDiskGraph) var showDiskGraph
|
@Default(.showDiskGraph) var showDiskGraph
|
||||||
|
|
||||||
|
// Dynamic sizing based on view type and graph count with smooth transitions
|
||||||
var dynamicNotchSize: CGSize {
|
var dynamicNotchSize: CGSize {
|
||||||
// Only apply dynamic sizing when on stats tab and stats are enabled
|
// Always return the target size for the current view to ensure smooth transitions
|
||||||
guard coordinator.currentView == .stats && enableStatsFeature else {
|
let targetSize: CGSize
|
||||||
return openNotchSize
|
|
||||||
|
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: openNotchSize.width,
|
||||||
|
height: openNotchSize.height + 65 // Add extra height for second row
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
targetSize = openNotchSize
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
targetSize = openNotchSize
|
||||||
}
|
}
|
||||||
|
|
||||||
let enabledGraphsCount = [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count
|
return targetSize
|
||||||
|
|
||||||
// Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows
|
|
||||||
var requiredHeight = openNotchSize.height
|
|
||||||
|
|
||||||
if enabledGraphsCount >= 4 {
|
|
||||||
// Two rows needed - add height for second row plus spacing
|
|
||||||
let extraHeight: CGFloat = 120 + 12 // Graph height + spacing
|
|
||||||
requiredHeight = openNotchSize.height + extraHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Width stays constant - no horizontal expansion
|
|
||||||
return CGSize(width: openNotchSize.width, height: requiredHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -57,6 +66,9 @@ struct ContentView: View {
|
||||||
@State private var debounceWorkItem: DispatchWorkItem?
|
@State private var debounceWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
@State private var isHoverStateChanging: Bool = false
|
@State private var isHoverStateChanging: Bool = false
|
||||||
|
@State private var isStatsTransitioning: Bool = false
|
||||||
|
@State private var isSwitchingToStats: Bool = false
|
||||||
|
@State private var lastHapticTime: Date = Date()
|
||||||
|
|
||||||
@State private var gestureProgress: CGFloat = .zero
|
@State private var gestureProgress: CGFloat = .zero
|
||||||
|
|
||||||
|
|
@ -68,7 +80,7 @@ struct ContentView: View {
|
||||||
|
|
||||||
@Default(.showNotHumanFace) var showNotHumanFace
|
@Default(.showNotHumanFace) var showNotHumanFace
|
||||||
@Default(.useModernCloseAnimation) var useModernCloseAnimation
|
@Default(.useModernCloseAnimation) var useModernCloseAnimation
|
||||||
|
|
||||||
private let extendedHoverPadding: CGFloat = 30
|
private let extendedHoverPadding: CGFloat = 30
|
||||||
private let zeroHeightHoverPadding: CGFloat = 10
|
private let zeroHeightHoverPadding: CGFloat = 10
|
||||||
|
|
||||||
|
|
@ -98,18 +110,22 @@ struct ContentView: View {
|
||||||
.conditionalModifier(!useModernCloseAnimation) { view in
|
.conditionalModifier(!useModernCloseAnimation) { view in
|
||||||
let hoverAnimationAnimation = Animation.bouncy.speed(1.2)
|
let hoverAnimationAnimation = Animation.bouncy.speed(1.2)
|
||||||
let notchStateAnimation = Animation.spring.speed(1.2)
|
let notchStateAnimation = Animation.spring.speed(1.2)
|
||||||
|
let viewTransitionAnimation = Animation.easeInOut(duration: 0.4)
|
||||||
return view
|
return view
|
||||||
.animation(hoverAnimationAnimation, value: isHovering)
|
.animation(hoverAnimationAnimation, value: isHovering)
|
||||||
.animation(notchStateAnimation, value: vm.notchState)
|
.animation(notchStateAnimation, value: vm.notchState)
|
||||||
|
.animation(viewTransitionAnimation, value: coordinator.currentView)
|
||||||
.animation(.smooth, value: gestureProgress)
|
.animation(.smooth, value: gestureProgress)
|
||||||
.transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2)))
|
.transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2)))
|
||||||
}
|
}
|
||||||
.conditionalModifier(useModernCloseAnimation) { view in
|
.conditionalModifier(useModernCloseAnimation) { view in
|
||||||
let hoverAnimationAnimation = Animation.bouncy.speed(1.2)
|
let hoverAnimationAnimation = Animation.bouncy.speed(1.2)
|
||||||
let notchStateAnimation = Animation.spring.speed(1.2)
|
let notchStateAnimation = Animation.spring.speed(1.2)
|
||||||
|
let viewTransitionAnimation = Animation.easeInOut(duration: 0.4)
|
||||||
return view
|
return view
|
||||||
.animation(hoverAnimationAnimation, value: isHovering)
|
.animation(hoverAnimationAnimation, value: isHovering)
|
||||||
.animation(notchStateAnimation, value: vm.notchState)
|
.animation(notchStateAnimation, value: vm.notchState)
|
||||||
|
.animation(viewTransitionAnimation, value: coordinator.currentView)
|
||||||
}
|
}
|
||||||
.conditionalModifier(Defaults[.openNotchOnHover]) { view in
|
.conditionalModifier(Defaults[.openNotchOnHover]) { view in
|
||||||
view.onHover { hovering in
|
view.onHover { hovering in
|
||||||
|
|
@ -119,26 +135,11 @@ struct ContentView: View {
|
||||||
.conditionalModifier(!Defaults[.openNotchOnHover]) { view in
|
.conditionalModifier(!Defaults[.openNotchOnHover]) { view in
|
||||||
view
|
view
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
withAnimation(vm.animation) {
|
handleSimpleHover(hovering)
|
||||||
isHovering = hovering
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only close if mouse leaves and the notch is open and no popovers are active
|
|
||||||
if !hovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive {
|
|
||||||
// Add slight delay for stats tab to prevent accidental closure during layout changes
|
|
||||||
let delay: Double = (coordinator.currentView == .stats) ? 0.2 : 0.0
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
|
|
||||||
// Double-check conditions after delay
|
|
||||||
if !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive {
|
|
||||||
vm.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if (vm.notchState == .closed) && Defaults[.enableHaptics] {
|
if (vm.notchState == .closed) && Defaults[.enableHaptics] {
|
||||||
haptics.toggle()
|
triggerHapticIfAllowed()
|
||||||
}
|
}
|
||||||
doOpen()
|
doOpen()
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +181,14 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
.onChange(of: vm.isBatteryPopoverActive) { _, newPopoverState in
|
.onChange(of: vm.isBatteryPopoverActive) { _, newPopoverState in
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
if !newPopoverState && !isHovering && vm.notchState == .open {
|
if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isStatsPopoverActive {
|
||||||
|
vm.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: vm.isStatsPopoverActive) { _, newPopoverState in
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
if !newPopoverState && !isHovering && vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive {
|
||||||
vm.close()
|
vm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +196,7 @@ struct ContentView: View {
|
||||||
.onChange(of: vm.shouldRecheckHover) { _, _ in
|
.onChange(of: vm.shouldRecheckHover) { _, _ in
|
||||||
// Recheck hover state when popovers are closed
|
// Recheck hover state when popovers are closed
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
if vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !isHovering {
|
if vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive && !vm.isStatsPopoverActive && !isHovering {
|
||||||
vm.close()
|
vm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,6 +211,35 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onChange(of: coordinator.currentView) { oldValue, newValue in
|
||||||
|
// Cancel any pending close actions during view transitions
|
||||||
|
hoverWorkItem?.cancel()
|
||||||
|
|
||||||
|
// Set flags for transition tracking
|
||||||
|
isSwitchingToStats = (oldValue != .stats && newValue == .stats)
|
||||||
|
|
||||||
|
// Provide protection when switching to stats tab
|
||||||
|
if newValue == .stats {
|
||||||
|
isStatsTransitioning = true
|
||||||
|
|
||||||
|
// Longer delay if stats tab has expanded height (4+ graphs)
|
||||||
|
let hasExpandedHeight = statsTabHasExpandedHeight()
|
||||||
|
let protectionDelay: Double = hasExpandedHeight ? 1.5 : 0.8
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + protectionDelay) {
|
||||||
|
isStatsTransitioning = false
|
||||||
|
isSwitchingToStats = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also provide brief protection when switching FROM stats to prevent premature closure
|
||||||
|
if oldValue == .stats && newValue != .stats {
|
||||||
|
isStatsTransitioning = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
|
||||||
|
isStatsTransitioning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.sensoryFeedback(.alignment, trigger: haptics)
|
.sensoryFeedback(.alignment, trigger: haptics)
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
SettingsLink(label: {
|
SettingsLink(label: {
|
||||||
|
|
@ -514,7 +551,7 @@ struct ContentView: View {
|
||||||
|
|
||||||
// Only provide haptic feedback if notch is closed
|
// Only provide haptic feedback if notch is closed
|
||||||
if vm.notchState == .closed && Defaults[.enableHaptics] {
|
if vm.notchState == .closed && Defaults[.enableHaptics] {
|
||||||
haptics.toggle()
|
triggerHapticIfAllowed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay opening the notch
|
// Delay opening the notch
|
||||||
|
|
@ -553,7 +590,9 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the notch if it's open and no popovers are active
|
// Close the notch if it's open and no popovers are active
|
||||||
if vm.notchState == .open && !vm.isBatteryPopoverActive && !vm.isClipboardPopoverActive && !vm.isColorPickerPopoverActive {
|
let hasActivePopovers = vm.isBatteryPopoverActive || vm.isClipboardPopoverActive || vm.isColorPickerPopoverActive || vm.isStatsPopoverActive
|
||||||
|
|
||||||
|
if vm.notchState == .open && !hasActivePopovers {
|
||||||
vm.close()
|
vm.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -564,6 +603,92 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simple hover handler for non-openNotchOnHover mode
|
||||||
|
private func handleSimpleHover(_ hovering: Bool) {
|
||||||
|
// Cancel any existing work items first
|
||||||
|
hoverWorkItem?.cancel()
|
||||||
|
debounceWorkItem?.cancel()
|
||||||
|
|
||||||
|
if hovering {
|
||||||
|
// Mouse entered - always update visual state immediately
|
||||||
|
withAnimation(vm.animation) {
|
||||||
|
isHovering = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse exited - always update visual state immediately
|
||||||
|
withAnimation(vm.animation) {
|
||||||
|
isHovering = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply stronger protection when switching to stats, especially with 4+ graphs
|
||||||
|
if isStatsTransitioning || (isSwitchingToStats && statsTabHasExpandedHeight()) {
|
||||||
|
return // Skip closing during transitions, but allow visual updates
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should close after a delay
|
||||||
|
let hasPopovers = hasAnyActivePopovers()
|
||||||
|
|
||||||
|
if vm.notchState == .open && !hasPopovers {
|
||||||
|
let isStatsTab = coordinator.currentView == .stats
|
||||||
|
let hasExpandedHeight = isStatsTab && statsTabHasExpandedHeight()
|
||||||
|
|
||||||
|
// Longer delays for stats tab, especially when it has expanded height
|
||||||
|
let delay: Double
|
||||||
|
if hasExpandedHeight {
|
||||||
|
delay = 1.0 // Extra long for stats with 4+ graphs
|
||||||
|
} else if isStatsTab {
|
||||||
|
delay = 0.6 // Medium for stats with 1-3 graphs
|
||||||
|
} else {
|
||||||
|
delay = 0.2 // Short for other tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
let closeTask = DispatchWorkItem {
|
||||||
|
// Triple-check conditions including transition state
|
||||||
|
let stillNotHovering = !isHovering
|
||||||
|
let stillOpen = vm.notchState == .open
|
||||||
|
let stillNoPopovers = !hasAnyActivePopovers()
|
||||||
|
let notTransitioning = !isStatsTransitioning && !isSwitchingToStats
|
||||||
|
|
||||||
|
if stillNotHovering && stillOpen && stillNoPopovers && notTransitioning {
|
||||||
|
vm.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverWorkItem = closeTask
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: closeTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if any popovers are active
|
||||||
|
private func hasAnyActivePopovers() -> Bool {
|
||||||
|
return vm.isBatteryPopoverActive ||
|
||||||
|
vm.isClipboardPopoverActive ||
|
||||||
|
vm.isColorPickerPopoverActive ||
|
||||||
|
vm.isStatsPopoverActive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to prevent rapid haptic feedback
|
||||||
|
private func triggerHapticIfAllowed() {
|
||||||
|
let now = Date()
|
||||||
|
if now.timeIntervalSince(lastHapticTime) > 0.3 { // Minimum 300ms between haptics
|
||||||
|
haptics.toggle()
|
||||||
|
lastHapticTime = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check if stats tab has 4+ graphs (needs expanded height)
|
||||||
|
private func statsTabHasExpandedHeight() -> Bool {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Gesture Handling
|
// MARK: - Gesture Handling
|
||||||
|
|
||||||
private func handleDownGesture(translation: CGFloat, phase: NSEvent.Phase) {
|
private func handleDownGesture(translation: CGFloat, phase: NSEvent.Phase) {
|
||||||
|
|
@ -581,7 +706,7 @@ struct ContentView: View {
|
||||||
|
|
||||||
if translation > Defaults[.gestureSensitivity] {
|
if translation > Defaults[.gestureSensitivity] {
|
||||||
if Defaults[.enableHaptics] {
|
if Defaults[.enableHaptics] {
|
||||||
haptics.toggle()
|
triggerHapticIfAllowed()
|
||||||
}
|
}
|
||||||
withAnimation(.smooth) {
|
withAnimation(.smooth) {
|
||||||
gestureProgress = .zero
|
gestureProgress = .zero
|
||||||
|
|
@ -610,7 +735,7 @@ struct ContentView: View {
|
||||||
vm.close()
|
vm.close()
|
||||||
|
|
||||||
if Defaults[.enableHaptics] {
|
if Defaults[.enableHaptics] {
|
||||||
haptics.toggle()
|
triggerHapticIfAllowed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,17 @@
|
||||||
},
|
},
|
||||||
"%lld colors" : {
|
"%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%%" : {
|
"%lld%%" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"•" : {
|
"•" : {
|
||||||
|
"comment" : "A period used to separate two related items.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"• Bug fixes" : {
|
"• Bug fixes" : {
|
||||||
|
|
||||||
|
|
@ -332,6 +337,10 @@
|
||||||
},
|
},
|
||||||
"Click 'Pick Color' or use Cmd+Shift+P to start picking colors" : {
|
"Click 'Pick Color' or use Cmd+Shift+P to start picking colors" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Click for details" : {
|
||||||
|
"comment" : "A hint text displayed below the graph in the Stats Card, indicating that tapping it will reveal more details.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Clipboard" : {
|
"Clipboard" : {
|
||||||
|
|
||||||
|
|
@ -564,7 +573,8 @@
|
||||||
|
|
||||||
},
|
},
|
||||||
"Enable graph visibility in Settings → Stats to view performance data." : {
|
"Enable graph visibility in Settings → Stats to view performance data." : {
|
||||||
|
"comment" : "A message displayed when no graphs are enabled in the Stats tab.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Enable haptics" : {
|
"Enable haptics" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
|
|
@ -749,9 +759,6 @@
|
||||||
},
|
},
|
||||||
"LinkedIn" : {
|
"LinkedIn" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Live" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Live Monitoring" : {
|
"Live Monitoring" : {
|
||||||
|
|
||||||
|
|
@ -943,7 +950,8 @@
|
||||||
|
|
||||||
},
|
},
|
||||||
"No Graphs Enabled" : {
|
"No Graphs Enabled" : {
|
||||||
|
"comment" : "A label displayed when no graphs are enabled in the Stats tab.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"No results" : {
|
"No results" : {
|
||||||
|
|
||||||
|
|
@ -968,9 +976,6 @@
|
||||||
},
|
},
|
||||||
"Notch Height" : {
|
"Notch Height" : {
|
||||||
|
|
||||||
},
|
|
||||||
"Off" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"OK" : {
|
"OK" : {
|
||||||
|
|
||||||
|
|
@ -1058,6 +1063,10 @@
|
||||||
},
|
},
|
||||||
"Picking Status" : {
|
"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" : {
|
"Pin items from history to save them here" : {
|
||||||
|
|
||||||
|
|
@ -1140,6 +1149,10 @@
|
||||||
},
|
},
|
||||||
"Recording Status" : {
|
"Recording Status" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Refresh" : {
|
||||||
|
"comment" : "A button to refresh the displayed data.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Release name" : {
|
"Release name" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -1461,6 +1474,10 @@
|
||||||
},
|
},
|
||||||
"Toggle the Dynamic Island open or closed from anywhere." : {
|
"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.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Transform your notch truly yours" : {
|
"Transform your notch truly yours" : {
|
||||||
|
|
||||||
|
|
@ -1546,5 +1563,5 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.1"
|
||||||
}
|
}
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
// NotchStatsView.swift
|
// NotchStatsView.swift
|
||||||
// DynamicIsland
|
// DynamicIsland
|
||||||
//
|
//
|
||||||
// Stats tab view for system performance monitoring
|
// Adapted from boring.notch StatsView
|
||||||
// Created by Hariharan Mudaliar
|
// Stats tab view for system performance monitoring with clickable process popovers
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Defaults
|
import Defaults
|
||||||
|
|
@ -53,6 +53,14 @@ struct NotchStatsView: View {
|
||||||
@Default(.showGpuGraph) var showGpuGraph
|
@Default(.showGpuGraph) var showGpuGraph
|
||||||
@Default(.showNetworkGraph) var showNetworkGraph
|
@Default(.showNetworkGraph) var showNetworkGraph
|
||||||
@Default(.showDiskGraph) var showDiskGraph
|
@Default(.showDiskGraph) var showDiskGraph
|
||||||
|
@State private var showingCPUPopover = false
|
||||||
|
@State private var showingMemoryPopover = false
|
||||||
|
@State private var showingGPUPopover = false
|
||||||
|
@State private var isHoveringCPUPopover = false
|
||||||
|
@State private var isHoveringMemoryPopover = false
|
||||||
|
@State private var isHoveringGPUPopover = false
|
||||||
|
@State private var isResizingForStats = false
|
||||||
|
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||||
|
|
||||||
var availableGraphs: [GraphData] {
|
var availableGraphs: [GraphData] {
|
||||||
var graphs: [GraphData] = []
|
var graphs: [GraphData] = []
|
||||||
|
|
@ -130,11 +138,7 @@ struct NotchStatsView: View {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
) {
|
) {
|
||||||
ForEach(0..<graphCount, id: \.self) { index in
|
ForEach(0..<graphCount, id: \.self) { index in
|
||||||
UnifiedStatsCard(graphData: availableGraphs[index])
|
graphViewForIndex(index)
|
||||||
.transition(.asymmetric(
|
|
||||||
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
|
|
||||||
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if graphCount == 4 {
|
} else if graphCount == 4 {
|
||||||
|
|
@ -144,11 +148,7 @@ struct NotchStatsView: View {
|
||||||
spacing: 12
|
spacing: 12
|
||||||
) {
|
) {
|
||||||
ForEach(0..<graphCount, id: \.self) { index in
|
ForEach(0..<graphCount, id: \.self) { index in
|
||||||
UnifiedStatsCard(graphData: availableGraphs[index])
|
graphViewForIndex(index)
|
||||||
.transition(.asymmetric(
|
|
||||||
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
|
|
||||||
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -160,11 +160,7 @@ struct NotchStatsView: View {
|
||||||
spacing: 8
|
spacing: 8
|
||||||
) {
|
) {
|
||||||
ForEach(0..<3, id: \.self) { index in
|
ForEach(0..<3, id: \.self) { index in
|
||||||
UnifiedStatsCard(graphData: availableGraphs[index])
|
graphViewForIndex(index)
|
||||||
.transition(.asymmetric(
|
|
||||||
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
|
|
||||||
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,17 +170,112 @@ struct NotchStatsView: View {
|
||||||
spacing: 8
|
spacing: 8
|
||||||
) {
|
) {
|
||||||
ForEach(3..<graphCount, id: \.self) { index in
|
ForEach(3..<graphCount, id: \.self) { index in
|
||||||
UnifiedStatsCard(graphData: availableGraphs[index])
|
graphViewForIndex(index)
|
||||||
.transition(.asymmetric(
|
|
||||||
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
|
|
||||||
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
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" {
|
||||||
|
Button(action: {
|
||||||
|
handleGraphClick(for: graphData)
|
||||||
|
}) {
|
||||||
|
UnifiedStatsCard(graphData: graphData)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
.popover(isPresented: bindingForGraph(graphData)) {
|
||||||
|
RankedProcessPopover(
|
||||||
|
rankingType: rankingTypeForGraph(graphData),
|
||||||
|
onHoverChange: { hovering in
|
||||||
|
switch graphData.title {
|
||||||
|
case "CPU":
|
||||||
|
isHoveringCPUPopover = hovering
|
||||||
|
case "Memory":
|
||||||
|
isHoveringMemoryPopover = hovering
|
||||||
|
case "GPU":
|
||||||
|
isHoveringGPUPopover = hovering
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onDisappear {
|
||||||
|
// Reset hover states when popover disappears
|
||||||
|
switch graphData.title {
|
||||||
|
case "CPU":
|
||||||
|
isHoveringCPUPopover = false
|
||||||
|
case "Memory":
|
||||||
|
isHoveringMemoryPopover = false
|
||||||
|
case "GPU":
|
||||||
|
isHoveringGPUPopover = false
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Ensure popover state is updated when popover disappears
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
updateStatsPopoverState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
|
||||||
|
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)),
|
||||||
|
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleGraphClick(for graphData: GraphData) {
|
||||||
|
switch graphData.title {
|
||||||
|
case "CPU":
|
||||||
|
showingCPUPopover = true
|
||||||
|
case "Memory":
|
||||||
|
showingMemoryPopover = true
|
||||||
|
case "GPU":
|
||||||
|
showingGPUPopover = true
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindingForGraph(_ graphData: GraphData) -> Binding<Bool> {
|
||||||
|
switch graphData.title {
|
||||||
|
case "CPU":
|
||||||
|
return $showingCPUPopover
|
||||||
|
case "Memory":
|
||||||
|
return $showingMemoryPopover
|
||||||
|
case "GPU":
|
||||||
|
return $showingGPUPopover
|
||||||
|
default:
|
||||||
|
return .constant(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func rankingTypeForGraph(_ graphData: GraphData) -> ProcessRankingType {
|
||||||
|
switch graphData.title {
|
||||||
|
case "CPU":
|
||||||
|
return .cpu
|
||||||
|
case "Memory":
|
||||||
|
return .memory
|
||||||
|
case "GPU":
|
||||||
|
return .gpu
|
||||||
|
default:
|
||||||
|
return .cpu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to create graph views using unified component
|
// Helper function to create graph views using unified component
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
func graphView(for graphData: GraphData) -> some View {
|
func graphView(for graphData: GraphData) -> some View {
|
||||||
|
|
@ -232,125 +323,129 @@ struct NotchStatsView: View {
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.padding()
|
.padding()
|
||||||
} else {
|
} else {
|
||||||
// Stats content with improved animation coordination
|
// Stats content with smart grid layout
|
||||||
ZStack(alignment: .topTrailing) {
|
VStack(spacing: 8) {
|
||||||
VStack(spacing: 12) {
|
statsGridLayout
|
||||||
// Simplified consistent layout
|
|
||||||
statsGridLayout
|
|
||||||
}
|
|
||||||
.padding(16)
|
|
||||||
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
|
||||||
.transition(.asymmetric(
|
|
||||||
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
|
|
||||||
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
|
|
||||||
))
|
|
||||||
|
|
||||||
// Live indicator and controls in top-right corner
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
// Control buttons
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
if statsManager.isMonitoring {
|
|
||||||
Button("Stop") {
|
|
||||||
statsManager.stopMonitoring()
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
.controlSize(.mini)
|
|
||||||
} else {
|
|
||||||
Button("Start") {
|
|
||||||
statsManager.startMonitoring()
|
|
||||||
}
|
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.controlSize(.mini)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("Clear") {
|
|
||||||
statsManager.clearHistory()
|
|
||||||
}
|
|
||||||
.buttonStyle(.bordered)
|
|
||||||
.disabled(statsManager.isMonitoring)
|
|
||||||
.controlSize(.mini)
|
|
||||||
}
|
|
||||||
.font(.caption2)
|
|
||||||
|
|
||||||
// Live indicator
|
|
||||||
HStack(spacing: 4) {
|
|
||||||
Circle()
|
|
||||||
.fill(statsManager.isMonitoring ? .green : .red)
|
|
||||||
.frame(width: 6, height: 6)
|
|
||||||
|
|
||||||
Text(statsManager.isMonitoring ? "Live" : "Off")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, 8)
|
|
||||||
.padding(.trailing, 12)
|
|
||||||
}
|
}
|
||||||
|
.padding(12)
|
||||||
|
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
|
||||||
|
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.background(Color(NSColor.windowBackgroundColor).opacity(0.1))
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if enableStatsFeature && !statsManager.isMonitoring {
|
if enableStatsFeature && Defaults[.autoStartStatsMonitoring] && !statsManager.isMonitoring {
|
||||||
statsManager.startMonitoring()
|
statsManager.startMonitoring()
|
||||||
}
|
}
|
||||||
|
// Protect against hover interference during view transition
|
||||||
|
isResizingForStats = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||||
|
isResizingForStats = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
// Keep monitoring running when tab is not visible
|
// Keep monitoring running when tab is not visible
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
|
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
|
||||||
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
||||||
|
.onChange(of: availableGraphs.count) { _, newCount in
|
||||||
|
// Protect against hover interference during dynamic sizing
|
||||||
|
isResizingForStats = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||||
|
isResizingForStats = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: showingCPUPopover) { _, _ in
|
||||||
|
updateStatsPopoverState()
|
||||||
|
}
|
||||||
|
.onChange(of: showingMemoryPopover) { _, _ in
|
||||||
|
updateStatsPopoverState()
|
||||||
|
}
|
||||||
|
.onChange(of: showingGPUPopover) { _, _ in
|
||||||
|
updateStatsPopoverState()
|
||||||
|
}
|
||||||
|
.onChange(of: isHoveringCPUPopover) { _, _ in
|
||||||
|
updateStatsPopoverState()
|
||||||
|
}
|
||||||
|
.onChange(of: isHoveringMemoryPopover) { _, _ in
|
||||||
|
updateStatsPopoverState()
|
||||||
|
}
|
||||||
|
.onChange(of: isHoveringGPUPopover) { _, _ in
|
||||||
|
updateStatsPopoverState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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(" Resizing: \(isResizingForStats)")
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unified Stats Card Component - handles both single and dual data types
|
// Unified Stats Card Component - handles both single and dual data types, matches boring.notch sizing
|
||||||
struct UnifiedStatsCard: View {
|
struct UnifiedStatsCard: View {
|
||||||
let graphData: GraphData
|
let graphData: GraphData
|
||||||
|
@State private var isHovered = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 3) { // Match boring.notch spacing
|
||||||
// Header - consistent across all card types
|
// Header - consistent across all card types
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: graphData.icon)
|
Image(systemName: graphData.icon)
|
||||||
.foregroundColor(graphData.color)
|
.foregroundColor(graphData.color)
|
||||||
.font(.caption2)
|
.font(.caption) // Match boring.notch font size
|
||||||
|
|
||||||
Text(graphData.title)
|
Text(graphData.title)
|
||||||
.font(.caption2)
|
.font(.caption) // Match boring.notch font size
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
|
||||||
|
// Show value on right for single graphs like boring.notch
|
||||||
// Values section - adapts based on graph type
|
|
||||||
Group {
|
|
||||||
if let singleData = graphData as? SingleGraphData {
|
if let singleData = graphData as? SingleGraphData {
|
||||||
Text(singleData.value)
|
Text(singleData.value)
|
||||||
.font(.title3)
|
.font(.caption) // Match boring.notch font size
|
||||||
.fontWeight(.bold)
|
.fontWeight(.bold)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(.primary)
|
||||||
} else if let dualData = graphData as? DualGraphData {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
Text(dualData.positiveValue)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(dualData.positiveColor)
|
|
||||||
|
|
||||||
Text("•")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text(dualData.negativeValue)
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(dualData.negativeColor)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: 22) // Fixed height for consistent card sizing
|
|
||||||
|
// Values section for dual graphs only
|
||||||
|
if let dualData = graphData as? DualGraphData {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text(dualData.positiveValue)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(dualData.positiveColor)
|
||||||
|
|
||||||
|
Text("•")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text(dualData.negativeValue)
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(dualData.negativeColor)
|
||||||
|
}
|
||||||
|
.frame(height: 18) // Smaller for dual graphs
|
||||||
|
}
|
||||||
|
|
||||||
// Graph section - adapts based on graph type
|
// Graph section - adapts based on graph type
|
||||||
Group {
|
Group {
|
||||||
|
|
@ -365,12 +460,32 @@ struct UnifiedStatsCard: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: 50) // Fixed height for consistent card sizing
|
.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" {
|
||||||
|
Text("Click for details")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.opacity(isHovered ? 1.0 : 0.0)
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: isHovered)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(10)
|
.padding(8) // Match boring.notch padding - reduced from 10
|
||||||
.background(Color(NSColor.controlBackgroundColor).opacity(0.5))
|
.background(
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color(NSColor.controlBackgroundColor).opacity(0.7)) // Match boring.notch opacity
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.stroke(graphData.color.opacity(isHovered ? 0.5 : 0.2), lineWidth: 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.scaleEffect(isHovered ? 1.02 : 1.0)
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: isHovered)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.onHover { hovering in
|
||||||
|
isHovered = hovering
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,6 +544,8 @@ struct MiniGraph: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct DualQuadrantGraph: View {
|
struct DualQuadrantGraph: View {
|
||||||
let positiveData: [Double]
|
let positiveData: [Double]
|
||||||
let negativeData: [Double]
|
let negativeData: [Double]
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import SwiftUI
|
||||||
import IOKit
|
import IOKit
|
||||||
import IOKit.ps
|
import IOKit.ps
|
||||||
import Darwin
|
import Darwin
|
||||||
|
import AppKit
|
||||||
//import Network
|
//import Network
|
||||||
|
|
||||||
class StatsManager: ObservableObject {
|
class StatsManager: ObservableObject {
|
||||||
|
|
@ -403,4 +404,61 @@ class StatsManager: ObservableObject {
|
||||||
diskReadHistory = Array(repeating: 0.0, count: maxHistoryPoints)
|
diskReadHistory = Array(repeating: 0.0, count: maxHistoryPoints)
|
||||||
diskWriteHistory = Array(repeating: 0.0, count: maxHistoryPoints)
|
diskWriteHistory = Array(repeating: 0.0, count: maxHistoryPoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Process Monitoring Methods
|
||||||
|
func getProcessesRankedByCPU() -> [ProcessStats] {
|
||||||
|
return getRunningProcesses().sorted { $0.cpuUsage > $1.cpuUsage }
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProcessesRankedByMemory() -> [ProcessStats] {
|
||||||
|
return getRunningProcesses().sorted { $0.memoryUsage > $1.memoryUsage }
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProcessesRankedByGPU() -> [ProcessStats] {
|
||||||
|
// For now, rank by CPU usage as GPU per-process stats are complex to obtain
|
||||||
|
return getRunningProcesses().sorted { $0.cpuUsage > $1.cpuUsage }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getRunningProcesses() -> [ProcessStats] {
|
||||||
|
var processes: [ProcessStats] = []
|
||||||
|
|
||||||
|
// Get all running applications from NSWorkspace
|
||||||
|
let runningApps = NSWorkspace.shared.runningApplications
|
||||||
|
|
||||||
|
for app in runningApps {
|
||||||
|
// Skip system processes and processes without bundle identifier
|
||||||
|
guard let bundleId = app.bundleIdentifier,
|
||||||
|
!bundleId.hasPrefix("com.apple.") || bundleId == "com.apple.finder" else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let pid = app.processIdentifier
|
||||||
|
var cpuUsage: Double = 0.0
|
||||||
|
var memoryUsage: UInt64 = 0
|
||||||
|
|
||||||
|
// Get CPU and memory usage using proc_pidinfo
|
||||||
|
var taskInfo = proc_taskinfo()
|
||||||
|
let result = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, Int32(MemoryLayout<proc_taskinfo>.size))
|
||||||
|
|
||||||
|
if result == MemoryLayout<proc_taskinfo>.size {
|
||||||
|
// Calculate CPU usage (basic approximation)
|
||||||
|
cpuUsage = Double(taskInfo.pti_total_user + taskInfo.pti_total_system) / 1000000.0
|
||||||
|
|
||||||
|
// Get memory usage
|
||||||
|
memoryUsage = taskInfo.pti_resident_size
|
||||||
|
}
|
||||||
|
|
||||||
|
let processStats = ProcessStats(
|
||||||
|
pid: pid,
|
||||||
|
name: app.localizedName ?? bundleId,
|
||||||
|
cpuUsage: cpuUsage,
|
||||||
|
memoryUsage: memoryUsage,
|
||||||
|
icon: app.icon
|
||||||
|
)
|
||||||
|
|
||||||
|
processes.append(processStats)
|
||||||
|
}
|
||||||
|
|
||||||
|
return processes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,7 @@ 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 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)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
|
||||||
@Published var isBatteryPopoverActive: Bool = false
|
@Published var isBatteryPopoverActive: Bool = false
|
||||||
@Published var isClipboardPopoverActive: Bool = false
|
@Published var isClipboardPopoverActive: Bool = false
|
||||||
@Published var isColorPickerPopoverActive: Bool = false
|
@Published var isColorPickerPopoverActive: Bool = false
|
||||||
|
@Published var isStatsPopoverActive: Bool = false
|
||||||
@Published var shouldRecheckHover: Bool = false
|
@Published var shouldRecheckHover: Bool = false
|
||||||
|
|
||||||
let webcamManager = WebcamManager.shared
|
let webcamManager = WebcamManager.shared
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue