Commitok összehasonlítása

...

12 Commit-ok

Szerző SHA1 Üzenet Dátum
hariel1985
eb52891a8c Fix 440 MB leak from re-evaluated .tabItem labels
ContentView declared an @EnvironmentObject it never read, so every
SystemMonitor publish (3 s by default) re-evaluated its body. On macOS
the AppKit tab bridge permanently retains the AnyView(Label(...)) behind
each .tabItem, leaking ~6 KB per tick — 519 MB after three days of
uptime, >90% of the process footprint.

Keep the TabView shells free of observable dependencies:

- ContentView: drop the unused @EnvironmentObject, so the body is
  evaluated exactly once.
- SettingsView: split the shell from its two tabs, which now own the
  AppSettings observation (dragging a threshold slider leaked 2 labels
  per step).
- PerformanceView: drop the write-only selection binding (3 labels per
  sub-tab switch).

Two smaller wins while in here:

- GPUMonitor leaked one io_object per GPU poll: leaving a
  `defer { release; IOIteratorNext }` loop via break/return hands back an
  object nobody releases. Replaced with firstMatch(in:), and removed a
  never-released iterator in getAppleSiliconChipName.
- MetricsStore re-encoded the whole 20k-sample history to JSON every
  30 s; saveEvery 10 -> 40 (~2 min). flush() on quit and on background
  is unchanged.

Measured over 10 minutes (~200 refresh cycles): footprint 86-123 MB with
no upward trend, leaked Label views pinned at 4 — the tab items
themselves. Was 519 MB and climbing ~170 MB/day. 55/55 unit tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:45:51 +02:00
hariel1985
05139408a7 docs: update screenshots with new-feature UI (Energy/Disk I/O, health, trends)
Refresh processes, performance-cpu, performance-memory and power-storage shots to
show the new columns, System Health score, battery panel and time-range trends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:17:48 +02:00
hariel1985
50480c2a97 docs: add Download section with v1.0.0 release link
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:11:12 +02:00
hariel1985
186ae31d01 Fix: show alert banners while TopManager is foreground
macOS suppresses an app's own notifications while it is the frontmost app unless
the UNUserNotificationCenter delegate opts in. Set the delegate in AppDelegate and
return [.banner, .sound, .list] from willPresent so High-CPU/health alerts pop as
banners even with the window focused. (Also confirmed notifications require a real
code signature, not the ad-hoc CODE_SIGNING_ALLOWED=NO test build.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 21:00:57 +02:00
hariel1985
d4db0a7972 Increment 6: final verification + docs
- Full XCTest suite green (55 tests)
- Visual verification of all tabs (screenshots): Processes (Energy/Disk columns +
  live data), Performance (range picker + P/E core charts), Power & Storage
  (Health score 55/Fair + battery 97%/114 cycles + preserved System/GPU)
- Ground-truth cross-checks: memory 12.31GB vs 12.53GB reported (16KB pages),
  battery vs ioreg/pmset, per-process CPU vs ps, disk vs df — all consistent
- README updated to document new capabilities; testing docs referenced

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:35:09 +02:00
hariel1985
2e3574f6e3 Increment 5: Settings, launch-at-login & rich menu-bar HUD
- AppSettings (@MainActor, UserDefaults-persisted): refresh interval, menu-bar
  metric, notifications toggle, CPU/disk/low-battery alert thresholds,
  launch-at-login; pushes changes into live services; nonisolated clampInterval
- LoginItem via SMAppService (macOS 13+), register/unregister with error catch
- SystemMonitor: runtime-configurable refresh cadence (was hardcoded 3s)
- SettingsView (⌘,): General (login, interval, menu-bar metric) + Alerts
  (notifications + thresholds) tabs
- MenuBarLabel: user-selectable menu-bar metric (CPU/Memory/Health/Download)
- Menu-bar dropdown: Top CPU Consumers with one-click quit
- 3 new tests (55 total); smoke: runs clean, Settings… menu registered

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:25:43 +02:00
hariel1985
bc494af363 Increment 4: alerts engine, notifications & System Health score
- HealthModel (pure): 0-100 score + plain-language diagnosis + rating
- SystemAlert/AlertKind/AlertThresholds + pure AlertEvaluator (sustained-breach)
- AlertCenter (@MainActor): per-cycle health + alert evaluation, de-dup by kind,
  inbox (capped), native UserNotifications on transition into a condition
- Covers: sustained high CPU, critical memory pressure, disk full, thermal,
  runaway process, low battery
- Wire evaluate() into SystemMonitor; request notif auth on launch
- HealthCardView (score ring + diagnosis) + AlertsInboxView in Power & Storage;
  health score + alert badge in menu bar
- Renamed model Alert->SystemAlert to avoid SwiftUI.Alert shadowing
- 10 new tests incl. AlertCenter de-dup integration (52 total green)
- Visual smoke: Energy + Disk I/O columns render with live data under load

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:20:24 +02:00
hariel1985
9b92fb715a Increment 3: battery health + power monitoring (IOKit)
- PowerInfo model + BatteryMath (health %, condition, watts, time fmt) — unit-tested
- PowerMonitor reads AppleSmartBattery via IORegistry (no root/entitlements):
  charge, cycles, design/raw-max capacity, temperature, voltage, amperage,
  time-to-empty/full, adapter watts; graceful no-battery (desktop) path
- Wire powerInfo into SystemMonitor (every 3rd cycle)
- Battery & Power section in Power & Storage tab; battery in menu-bar dropdown
- 10 new tests (42 total); cross-checked vs pmset/ioreg: health 96.9%, 114 cycles,
  30.8C, matches ground truth exactly

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:12:20 +02:00
hariel1985
1390eda468 Increment 2: deep per-process data + process inspector
- ProcessItem: disk read/write rate + cumulative bytes, energy-impact score,
  executable path; equality now tracks energy/disk so the table refreshes
- ProcessMonitor: disk I/O + idle-wakeup rates from rusage_info_v4; full path
  capture; on-demand KERN_PROCARGS2 args + PROC_PIDLISTFDS open-file count
- EnergyModel heuristic + pure ProcArgs parser (both unit-tested)
- ProcessView: sortable Energy + Disk I/O columns (icon folded into Name to stay
  within SwiftUI's 10-column limit); Get Info (double-click / ⌘I) opens inspector
- ProcessInspectorSheet: live stats, path, args, open files, Reveal/Copy Path
- 10 new tests (32 total green); smoke: stable under CPU+disk load

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:08:15 +02:00
hariel1985
222638fc98 Increment 1: persistent metric history + extended time-range charts
- MetricsStore: thread-safe on-disk ring buffer (Application Support), throttled
  background saves, flush on quit/background, 24h/20k cap
- MetricsMath: pure trim + downsample (unit-tested)
- HistoryRange (Live/5m/30m/1h/24h) + HistoryRangePicker + MetricTrendChart
- Wire recording into SystemMonitor; range picker in CPU/Memory/Network tabs
- Fix durability: flush history on applicationWillTerminate + scenePhase.background
- 8 new tests (30 total green); runtime smoke: no crash, 7 samples persisted

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:59:05 +02:00
hariel1985
d59bf2b7b0 Increment 0: add XCTest target, testing process & 10x value plan
- Add TopManagerTests unit-test target (hosted, @testable import)
- 14 baseline tests for formatters and model math (all green)
- scripts/test.sh build+test loop driver
- docs/TESTING.md (5-gate process), docs/VALUE_PLAN.md (10x rubric)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 19:51:47 +02:00
hariel1985
5ec09d9279 Aláírás: céges Apple Team ID (P3663392R2) + entitlements javítás
- DEVELOPMENT_TEAM beállítva a céges fiókra (InstaSoft, P3663392R2).
- A project.yml-ből kivéve a target `entitlements:` blokk, amely
  `properties` nélkül üres entitlements fájlt generált és felülírta a
  kézzel karbantartott TopManager.entitlements-et (app-sandbox=false).
  A CODE_SIGN_ENTITLEMENTS beállítás marad. Release build ellenőrizve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:26:29 +02:00
38 fájl változott, egészen pontosan 2865 új sor hozzáadva és 183 régi sor törölve

Fájl megtekintése

@@ -5,6 +5,15 @@ A native macOS system monitor application built with SwiftUI. TopManager provide
![macOS](https://img.shields.io/badge/macOS-13.0+-blue) ![macOS](https://img.shields.io/badge/macOS-13.0+-blue)
![Swift](https://img.shields.io/badge/Swift-5.9-orange) ![Swift](https://img.shields.io/badge/Swift-5.9-orange)
![License](https://img.shields.io/badge/License-GPL--3.0-green) ![License](https://img.shields.io/badge/License-GPL--3.0-green)
![Release](https://img.shields.io/badge/release-v1.0.0-brightgreen)
## Download
**[⬇️ Download TopManager 1.0 (.dmg)](https://github.com/hariel1985/TopManager/releases/latest)**
Signed with a Developer ID and **notarized by Apple** — it opens without Gatekeeper
warnings. Open the DMG and drag TopManager to your Applications folder.
All [releases](https://github.com/hariel1985/TopManager/releases).
## Screenshots ## Screenshots
@@ -29,10 +38,13 @@ A native macOS system monitor application built with SwiftUI. TopManager provide
## Features ## Features
### Processes Tab ### Processes Tab
- View all running processes with CPU, memory, and thread information - View all running processes with CPU, memory, thread, **energy-impact**, and
- Sort by any column (name, PID, CPU%, memory, threads, user, state) **per-process disk I/O** information
- Sort by any column (name, PID, CPU/Core, CPU/Total, Energy, memory, Disk I/O, threads, user, state)
- Process states: Running, Sleeping, Stopped, Zombie - Process states: Running, Sleeping, Stopped, Zombie
- Context menu to terminate, force kill, suspend, or resume processes - Context menu to terminate, force kill, suspend, or resume processes
- **Deep-dive inspector** (double-click / ⌘I): full executable path, launch
arguments, open-file count, disk totals, energy, parent PID, Reveal in Finder
- Search processes by name or PID - Search processes by name or PID
### Apps Tab ### Apps Tab
@@ -43,17 +55,38 @@ A native macOS system monitor application built with SwiftUI. TopManager provide
### Performance Tab ### Performance Tab
- Real-time CPU usage graphs (global and per-core) - Real-time CPU usage graphs (global and per-core)
- **Selectable time range** (Live / 5m / 30m / 1h / 24h) backed by persistent history
- Memory usage visualization with donut chart - Memory usage visualization with donut chart
- Network throughput monitoring - Network throughput monitoring
- Support for Apple Silicon P-cores and E-cores - Support for Apple Silicon P-cores and E-cores
### Power & Storage Tab ### Power & Storage Tab
- **System Health score (0–100)** with plain-language diagnosis of what's wrong
- **Recent Alerts** inbox
- **Battery & Power**: charge, health (cycle-adjusted), cycle count, condition,
temperature, power draw, time remaining, adapter wattage (via IOKit)
- System status: macOS version, uptime, thermal state - System status: macOS version, uptime, thermal state
- CPU and GPU core counts - CPU and GPU core counts
- GPU memory/VRAM usage - GPU memory/VRAM usage
- Storage volumes with usage bars - Storage volumes with usage bars
- Network interface statistics - Network interface statistics
### Proactive monitoring
- **Alerts engine** with native macOS notifications on sustained high CPU,
critical memory pressure, disk almost full, thermal throttling, runaway
processes, and low battery — de-duplicated so you're never spammed
- **Persistent metric history** stored to disk; trends survive restarts
### Menu Bar
- Configurable menu-bar metric (CPU % / Memory % / Health / Download)
- Dropdown with live CPU/memory/network/GPU/battery, System Health score, and
**Top CPU Consumers** with one-click quit
### Settings (⌘,)
- **Launch at login** (SMAppService)
- Adjustable refresh interval (1–30 s)
- Notification toggle and alert thresholds (CPU, disk, low battery)
## Requirements ## Requirements
- macOS 13.0 or later - macOS 13.0 or later
@@ -74,10 +107,20 @@ A native macOS system monitor application built with SwiftUI. TopManager provide
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
## Testing
Logic is covered by an XCTest suite (`TopManagerTests`, 55 tests) run via
`scripts/test.sh test`. The full manual + automated process is documented in
[docs/TESTING.md](docs/TESTING.md), and the product direction in
[docs/VALUE_PLAN.md](docs/VALUE_PLAN.md).
## Acknowledgments ## Acknowledgments
Built with SwiftUI and native macOS APIs including: Built with SwiftUI and native macOS APIs including:
- `libproc` for process information - `libproc` / `rusage_info` for process information, disk I/O and energy signals
- `IOKit` for GPU and hardware monitoring - `IOKit` for GPU, battery (AppleSmartBattery) and hardware monitoring
- `Metal` for GPU detection - `Metal` for GPU detection
- `SystemConfiguration` for network monitoring - `SystemConfiguration` for network monitoring
- `UserNotifications` for proactive alerts
- `ServiceManagement` (SMAppService) for launch-at-login
- Swift `Charts` for time-series visualization

Binary file not shown.

Before

Width:  |  Height:  |  Méret: 229 KiB

After

Width:  |  Height:  |  Méret: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Méret: 227 KiB

After

Width:  |  Height:  |  Méret: 241 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Méret: 236 KiB

After

Width:  |  Height:  |  Méret: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Méret: 498 KiB

After

Width:  |  Height:  |  Méret: 616 KiB

Fájl megtekintése

@@ -8,88 +8,127 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
00EC5A90B7B8C6C5015D90D1 /* PerformanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80A817DCDA254349DEA6F06 /* PerformanceView.swift */; }; 00EC5A90B7B8C6C5015D90D1 /* PerformanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80A817DCDA254349DEA6F06 /* PerformanceView.swift */; };
2B3C4D5E6F7A8B9C0D1E2F3A /* AppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A2B3C4D5E6F7A8B9C0D1E2F /* AppsView.swift */; };
0AA7D351AEB05078FA9B4B93 /* ProcessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6F97E6EF65AA17A469F037 /* ProcessView.swift */; }; 0AA7D351AEB05078FA9B4B93 /* ProcessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B6F97E6EF65AA17A469F037 /* ProcessView.swift */; };
136DAD1FE90FD682DF666667 /* TopManager_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */; }; 136DAD1FE90FD682DF666667 /* TopManager_icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */; };
1F212CE158B7D14B29077B18 /* CPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44281079654C7E1C9E16806C /* CPUInfo.swift */; }; 1F212CE158B7D14B29077B18 /* CPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44281079654C7E1C9E16806C /* CPUInfo.swift */; };
1F25A84B560709AE71C27D23 /* GPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F481045B4FC6017F50849FF1 /* GPUInfo.swift */; }; 1F25A84B560709AE71C27D23 /* GPUInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F481045B4FC6017F50849FF1 /* GPUInfo.swift */; };
24F63686869A69C8B0FCD7FA /* MetricsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */; };
266B888ADE2FDDBCEC8D28A5 /* MemoryLineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */; }; 266B888ADE2FDDBCEC8D28A5 /* MemoryLineChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */; };
2B74C177F49B4358D9642712 /* ProcessDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 332290DABD5326C87258723C /* ProcessDataTests.swift */; };
2DE14B65EA1D4E9B4D9DA5A6 /* BatteryMathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */; };
341C12CED58388D32ED6C5BB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F1ACD0492CAB741595DEAA /* ContentView.swift */; }; 341C12CED58388D32ED6C5BB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F1ACD0492CAB741595DEAA /* ContentView.swift */; };
35852165B2DA76AB7F073723 /* MenuBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D71CC63FC2FEF52A069372E /* MenuBarView.swift */; }; 35852165B2DA76AB7F073723 /* MenuBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D71CC63FC2FEF52A069372E /* MenuBarView.swift */; };
4427B8406B25B8FEDF27CDBA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 334573CEB47D60570A40B55D /* Assets.xcassets */; }; 4427B8406B25B8FEDF27CDBA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 334573CEB47D60570A40B55D /* Assets.xcassets */; };
489E29F78A4093D3548673E6 /* HealthAlertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E539A84E57291B69D4EE0314 /* HealthAlertTests.swift */; };
49D85FF611927294159C89D1 /* FormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 301352EB5B362C732CE1A246 /* FormattingTests.swift */; };
501C13D57386C179C0B3B14B /* PowerStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6038CCDF3BDF0F8BC93C9D4 /* PowerStorageView.swift */; }; 501C13D57386C179C0B3B14B /* PowerStorageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6038CCDF3BDF0F8BC93C9D4 /* PowerStorageView.swift */; };
67F66E7EC7C01A6309DC060E /* AlertCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C96471F109FF1C20ABF59E /* AlertCenter.swift */; };
6A0810422D2EC8F9F5D191C4 /* HealthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 126A382E2755C5061E6D29F5 /* HealthView.swift */; };
744F52D036D1F3E081923382 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */; }; 744F52D036D1F3E081923382 /* ProcessInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */; };
79E837AE662021C521F7E74F /* HealthModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C70E54E350F7C8C22D342594 /* HealthModel.swift */; };
7A1144E43159D96564C13164 /* MemoryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3EDCDEDF4A0CE1921A3B25 /* MemoryInfo.swift */; }; 7A1144E43159D96564C13164 /* MemoryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3EDCDEDF4A0CE1921A3B25 /* MemoryInfo.swift */; };
7A6088469A2643D360B35B89 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892247B0F17363DFC146EDF8 /* AppSettings.swift */; };
833976FD8DC4CCC45078CF9A /* DiskMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */; }; 833976FD8DC4CCC45078CF9A /* DiskMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */; };
8789C5F4A1AF4B1E8E434D71 /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01C19ABF49697A97F0C40378 /* SettingsTests.swift */; };
8F9ED99AB6D8D90AE79B5179 /* DiskUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B3F62455B01B2E9C0D76A9 /* DiskUsageView.swift */; }; 8F9ED99AB6D8D90AE79B5179 /* DiskUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B3F62455B01B2E9C0D76A9 /* DiskUsageView.swift */; };
9146403BC02D18A234444DF6 /* ProcessMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295232B4C746511757DCF8FE /* ProcessMonitor.swift */; }; 9146403BC02D18A234444DF6 /* ProcessMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295232B4C746511757DCF8FE /* ProcessMonitor.swift */; };
9F3A8E658859AD9C325E52A8 /* TrendChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA15E30C4B694CB36DC0893 /* TrendChartView.swift */; };
A0649565A902BA48DB10F592 /* CPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19ED582C412C63A0A2E2103E /* CPUMonitor.swift */; }; A0649565A902BA48DB10F592 /* CPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19ED582C412C63A0A2E2103E /* CPUMonitor.swift */; };
A7EC9FB55D5088D669491C11 /* AppsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 813543165B39EBD213EB3BC7 /* AppsView.swift */; };
B0165358B483B0ECC28DB4DE /* PowerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C512B13DC0C1024CF39A5E /* PowerInfo.swift */; };
B62B18DF32A8734D5908B16B /* DiskInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */; }; B62B18DF32A8734D5908B16B /* DiskInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */; };
B89695749D32F394DB459C15 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AACBF14FFC5EC68C9E438040 /* SettingsView.swift */; };
BCBAC37351B9A3878CC3F01B /* MetricsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05CC33DC4E93F6ED2E52F4E1 /* MetricsStore.swift */; };
C077A279E20A90FD6038CCB0 /* ProcessDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110A314A63AE355A9E04990D /* ProcessDetailView.swift */; }; C077A279E20A90FD6038CCB0 /* ProcessDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110A314A63AE355A9E04990D /* ProcessDetailView.swift */; };
CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFADE83965AA863B775A910 /* NetworkChartView.swift */; }; CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFADE83965AA863B775A910 /* NetworkChartView.swift */; };
D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A404EBD588B7ACD36079A95D /* TopManagerApp.swift */; }; D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A404EBD588B7ACD36079A95D /* TopManagerApp.swift */; };
D9E882888E23C26B748E22CF /* PowerMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */; };
DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279B972C08A878053821285D /* SystemMonitor.swift */; }; DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279B972C08A878053821285D /* SystemMonitor.swift */; };
DE50ECC69FCE60B10364802C /* ByteFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C9B469F6A45F3CC21A7594 /* ByteFormatters.swift */; }; DE50ECC69FCE60B10364802C /* ByteFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C9B469F6A45F3CC21A7594 /* ByteFormatters.swift */; };
E379C349DC9617D6C505B74B /* GPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */; }; E379C349DC9617D6C505B74B /* GPUMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */; };
E40A4E80ED4092A3D3A9C219 /* MemoryDonutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F402CE13EF67DE550786B79 /* MemoryDonutView.swift */; }; E40A4E80ED4092A3D3A9C219 /* MemoryDonutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F402CE13EF67DE550786B79 /* MemoryDonutView.swift */; };
E4AF7F7CA1E1FE8638CA5A77 /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3010F605712E09175D9D8878 /* NetworkInfo.swift */; }; E4AF7F7CA1E1FE8638CA5A77 /* NetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3010F605712E09175D9D8878 /* NetworkInfo.swift */; };
EB0D84848F42134927F9C9B1 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */; }; EB0D84848F42134927F9C9B1 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */; };
EB7EAB13C0DC7865BB1A99D5 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5B918C9462144E9EE7880A /* Alert.swift */; };
F5722D2B18EF7ABF43847EE4 /* MemoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */; }; F5722D2B18EF7ABF43847EE4 /* MemoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */; };
FA252F9182ADDA26AD1E54DE /* CPUChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D5A13CC5B9CF1B90CC2214 /* CPUChartView.swift */; }; FA252F9182ADDA26AD1E54DE /* CPUChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D5A13CC5B9CF1B90CC2214 /* CPUChartView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
B01CE3A16D70795D608951EE /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 4BAFF5CBBA9A1302C6CCDCD2 /* Project object */;
proxyType = 1;
remoteGlobalIDString = F86DD5615146D759EB0D44EB;
remoteInfo = TopManager;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
01C19ABF49697A97F0C40378 /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = "<group>"; };
05CC33DC4E93F6ED2E52F4E1 /* MetricsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsStore.swift; sourceTree = "<group>"; };
0D5B918C9462144E9EE7880A /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsStoreTests.swift; sourceTree = "<group>"; };
0F5A69E0EE7FDD44157911D7 /* TopManagerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TopManagerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
110A314A63AE355A9E04990D /* ProcessDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessDetailView.swift; sourceTree = "<group>"; }; 110A314A63AE355A9E04990D /* ProcessDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessDetailView.swift; sourceTree = "<group>"; };
1A2B3C4D5E6F7A8B9C0D1E2F /* AppsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsView.swift; sourceTree = "<group>"; }; 126A382E2755C5061E6D29F5 /* HealthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthView.swift; sourceTree = "<group>"; };
162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskMonitor.swift; sourceTree = "<group>"; }; 162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskMonitor.swift; sourceTree = "<group>"; };
19ED582C412C63A0A2E2103E /* CPUMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUMonitor.swift; sourceTree = "<group>"; }; 19ED582C412C63A0A2E2103E /* CPUMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUMonitor.swift; sourceTree = "<group>"; };
279B972C08A878053821285D /* SystemMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMonitor.swift; sourceTree = "<group>"; }; 279B972C08A878053821285D /* SystemMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMonitor.swift; sourceTree = "<group>"; };
27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryMonitor.swift; sourceTree = "<group>"; }; 27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryMonitor.swift; sourceTree = "<group>"; };
295232B4C746511757DCF8FE /* ProcessMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessMonitor.swift; sourceTree = "<group>"; }; 295232B4C746511757DCF8FE /* ProcessMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessMonitor.swift; sourceTree = "<group>"; };
3010F605712E09175D9D8878 /* NetworkInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInfo.swift; sourceTree = "<group>"; }; 3010F605712E09175D9D8878 /* NetworkInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkInfo.swift; sourceTree = "<group>"; };
301352EB5B362C732CE1A246 /* FormattingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattingTests.swift; sourceTree = "<group>"; };
332290DABD5326C87258723C /* ProcessDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessDataTests.swift; sourceTree = "<group>"; };
334573CEB47D60570A40B55D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 334573CEB47D60570A40B55D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
33BCE156CB39EDADFFC938F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 33BCE156CB39EDADFFC938F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; }; 347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskInfo.swift; sourceTree = "<group>"; }; 352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskInfo.swift; sourceTree = "<group>"; };
44281079654C7E1C9E16806C /* CPUInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUInfo.swift; sourceTree = "<group>"; }; 44281079654C7E1C9E16806C /* CPUInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUInfo.swift; sourceTree = "<group>"; };
4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = "<group>"; }; 4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessInfo.swift; sourceTree = "<group>"; };
51C512B13DC0C1024CF39A5E /* PowerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerInfo.swift; sourceTree = "<group>"; };
5E1D2EA8E1AD11B42FF225EB /* TopManager.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TopManager.entitlements; sourceTree = "<group>"; }; 5E1D2EA8E1AD11B42FF225EB /* TopManager.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TopManager.entitlements; sourceTree = "<group>"; };
70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryMathTests.swift; sourceTree = "<group>"; };
7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryLineChart.swift; sourceTree = "<group>"; }; 7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryLineChart.swift; sourceTree = "<group>"; };
78C9B469F6A45F3CC21A7594 /* ByteFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteFormatters.swift; sourceTree = "<group>"; }; 78C9B469F6A45F3CC21A7594 /* ByteFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ByteFormatters.swift; sourceTree = "<group>"; };
7B6F97E6EF65AA17A469F037 /* ProcessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessView.swift; sourceTree = "<group>"; }; 7B6F97E6EF65AA17A469F037 /* ProcessView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessView.swift; sourceTree = "<group>"; };
813543165B39EBD213EB3BC7 /* AppsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsView.swift; sourceTree = "<group>"; };
892247B0F17363DFC146EDF8 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TopManager_icon.png; sourceTree = "<group>"; }; 8E4A6E8E2305CEDC119AC6BC /* TopManager_icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TopManager_icon.png; sourceTree = "<group>"; };
8F402CE13EF67DE550786B79 /* MemoryDonutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryDonutView.swift; sourceTree = "<group>"; }; 8F402CE13EF67DE550786B79 /* MemoryDonutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryDonutView.swift; sourceTree = "<group>"; };
94D5A13CC5B9CF1B90CC2214 /* CPUChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUChartView.swift; sourceTree = "<group>"; }; 94D5A13CC5B9CF1B90CC2214 /* CPUChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CPUChartView.swift; sourceTree = "<group>"; };
95B3F62455B01B2E9C0D76A9 /* DiskUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskUsageView.swift; sourceTree = "<group>"; }; 95B3F62455B01B2E9C0D76A9 /* DiskUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskUsageView.swift; sourceTree = "<group>"; };
95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerMonitor.swift; sourceTree = "<group>"; };
9AA15E30C4B694CB36DC0893 /* TrendChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendChartView.swift; sourceTree = "<group>"; };
9D71CC63FC2FEF52A069372E /* MenuBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = "<group>"; }; 9D71CC63FC2FEF52A069372E /* MenuBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarView.swift; sourceTree = "<group>"; };
A3A3D0B1EB522C9975F71DA8 /* TopManager.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TopManager.app; sourceTree = BUILT_PRODUCTS_DIR; }; A3A3D0B1EB522C9975F71DA8 /* TopManager.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TopManager.app; sourceTree = BUILT_PRODUCTS_DIR; };
A404EBD588B7ACD36079A95D /* TopManagerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopManagerApp.swift; sourceTree = "<group>"; }; A404EBD588B7ACD36079A95D /* TopManagerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopManagerApp.swift; sourceTree = "<group>"; };
AACBF14FFC5EC68C9E438040 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
B6038CCDF3BDF0F8BC93C9D4 /* PowerStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerStorageView.swift; sourceTree = "<group>"; }; B6038CCDF3BDF0F8BC93C9D4 /* PowerStorageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerStorageView.swift; sourceTree = "<group>"; };
BBFADE83965AA863B775A910 /* NetworkChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkChartView.swift; sourceTree = "<group>"; }; BBFADE83965AA863B775A910 /* NetworkChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkChartView.swift; sourceTree = "<group>"; };
BD3EDCDEDF4A0CE1921A3B25 /* MemoryInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryInfo.swift; sourceTree = "<group>"; }; BD3EDCDEDF4A0CE1921A3B25 /* MemoryInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryInfo.swift; sourceTree = "<group>"; };
BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPUMonitor.swift; sourceTree = "<group>"; }; BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPUMonitor.swift; sourceTree = "<group>"; };
C2C96471F109FF1C20ABF59E /* AlertCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCenter.swift; sourceTree = "<group>"; };
C4F1ACD0492CAB741595DEAA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; C4F1ACD0492CAB741595DEAA /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
C70E54E350F7C8C22D342594 /* HealthModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthModel.swift; sourceTree = "<group>"; };
E539A84E57291B69D4EE0314 /* HealthAlertTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthAlertTests.swift; sourceTree = "<group>"; };
F481045B4FC6017F50849FF1 /* GPUInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPUInfo.swift; sourceTree = "<group>"; }; F481045B4FC6017F50849FF1 /* GPUInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GPUInfo.swift; sourceTree = "<group>"; };
F80A817DCDA254349DEA6F06 /* PerformanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceView.swift; sourceTree = "<group>"; }; F80A817DCDA254349DEA6F06 /* PerformanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
3C4D5E6F7A8B9C0D1E2F3A4B /* Apps */ = {
isa = PBXGroup;
children = (
1A2B3C4D5E6F7A8B9C0D1E2F /* AppsView.swift */,
);
path = Apps;
sourceTree = "<group>";
};
06D63CC4D5468CEC1E31137A /* Services */ = { 06D63CC4D5468CEC1E31137A /* Services */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C2C96471F109FF1C20ABF59E /* AlertCenter.swift */,
892247B0F17363DFC146EDF8 /* AppSettings.swift */,
19ED582C412C63A0A2E2103E /* CPUMonitor.swift */, 19ED582C412C63A0A2E2103E /* CPUMonitor.swift */,
162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */, 162FC58F6C44EC1DD7022A86 /* DiskMonitor.swift */,
BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */, BE3BB62AE0DC43777AD00658 /* GPUMonitor.swift */,
27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */, 27FDD19B8DBB7D955A5ECB5B /* MemoryMonitor.swift */,
05CC33DC4E93F6ED2E52F4E1 /* MetricsStore.swift */,
347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */, 347E47F825E496F40FD8ED11 /* NetworkMonitor.swift */,
95F7CCD4F64BE7478DFAA17F /* PowerMonitor.swift */,
295232B4C746511757DCF8FE /* ProcessMonitor.swift */, 295232B4C746511757DCF8FE /* ProcessMonitor.swift */,
279B972C08A878053821285D /* SystemMonitor.swift */, 279B972C08A878053821285D /* SystemMonitor.swift */,
); );
@@ -100,6 +139,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
36BB5A20B1330E54F2DCF357 /* TopManager */, 36BB5A20B1330E54F2DCF357 /* TopManager */,
D078BA97D4F498E5282AC5E0 /* TopManagerTests */,
E6438D9AF8EDEFEDCB087B38 /* Products */, E6438D9AF8EDEFEDCB087B38 /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@@ -107,11 +147,14 @@
32ADF00BA31F63CAE52B2D0A /* Models */ = { 32ADF00BA31F63CAE52B2D0A /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0D5B918C9462144E9EE7880A /* Alert.swift */,
44281079654C7E1C9E16806C /* CPUInfo.swift */, 44281079654C7E1C9E16806C /* CPUInfo.swift */,
352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */, 352CA1BE2A3B43643A9DBD72 /* DiskInfo.swift */,
F481045B4FC6017F50849FF1 /* GPUInfo.swift */, F481045B4FC6017F50849FF1 /* GPUInfo.swift */,
C70E54E350F7C8C22D342594 /* HealthModel.swift */,
BD3EDCDEDF4A0CE1921A3B25 /* MemoryInfo.swift */, BD3EDCDEDF4A0CE1921A3B25 /* MemoryInfo.swift */,
3010F605712E09175D9D8878 /* NetworkInfo.swift */, 3010F605712E09175D9D8878 /* NetworkInfo.swift */,
51C512B13DC0C1024CF39A5E /* PowerInfo.swift */,
4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */, 4CF69D4888FDBB3669619A1A /* ProcessInfo.swift */,
); );
path = Models; path = Models;
@@ -141,6 +184,7 @@
7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */, 7326AE4B2D75476BDCFE3FFB /* MemoryLineChart.swift */,
BBFADE83965AA863B775A910 /* NetworkChartView.swift */, BBFADE83965AA863B775A910 /* NetworkChartView.swift */,
F80A817DCDA254349DEA6F06 /* PerformanceView.swift */, F80A817DCDA254349DEA6F06 /* PerformanceView.swift */,
9AA15E30C4B694CB36DC0893 /* TrendChartView.swift */,
); );
path = Performance; path = Performance;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -153,6 +197,14 @@
path = Resources; path = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
6901D0C7B3DA57547BCFAAFC /* Apps */ = {
isa = PBXGroup;
children = (
813543165B39EBD213EB3BC7 /* AppsView.swift */,
);
path = Apps;
sourceTree = "<group>";
};
9C38088C52BDE6A2BA1CEF73 /* Utilities */ = { 9C38088C52BDE6A2BA1CEF73 /* Utilities */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -166,7 +218,9 @@
children = ( children = (
C4F1ACD0492CAB741595DEAA /* ContentView.swift */, C4F1ACD0492CAB741595DEAA /* ContentView.swift */,
9D71CC63FC2FEF52A069372E /* MenuBarView.swift */, 9D71CC63FC2FEF52A069372E /* MenuBarView.swift */,
3C4D5E6F7A8B9C0D1E2F3A4B /* Apps */, AACBF14FFC5EC68C9E438040 /* SettingsView.swift */,
6901D0C7B3DA57547BCFAAFC /* Apps */,
CD1E8360C279FD4EFAD85DF6 /* Health */,
4935484C55FD4C912A30D623 /* Performance */, 4935484C55FD4C912A30D623 /* Performance */,
CC52DA4C64987575E4D96A00 /* PowerStorage */, CC52DA4C64987575E4D96A00 /* PowerStorage */,
F2616FC0A232C728E792C2E8 /* Processes */, F2616FC0A232C728E792C2E8 /* Processes */,
@@ -183,10 +237,32 @@
path = PowerStorage; path = PowerStorage;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CD1E8360C279FD4EFAD85DF6 /* Health */ = {
isa = PBXGroup;
children = (
126A382E2755C5061E6D29F5 /* HealthView.swift */,
);
path = Health;
sourceTree = "<group>";
};
D078BA97D4F498E5282AC5E0 /* TopManagerTests */ = {
isa = PBXGroup;
children = (
70CCC4ED4F2BDC35ABD4F4F6 /* BatteryMathTests.swift */,
301352EB5B362C732CE1A246 /* FormattingTests.swift */,
E539A84E57291B69D4EE0314 /* HealthAlertTests.swift */,
0E786723FC46A64EFF5D3CE4 /* MetricsStoreTests.swift */,
332290DABD5326C87258723C /* ProcessDataTests.swift */,
01C19ABF49697A97F0C40378 /* SettingsTests.swift */,
);
path = TopManagerTests;
sourceTree = "<group>";
};
E6438D9AF8EDEFEDCB087B38 /* Products */ = { E6438D9AF8EDEFEDCB087B38 /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A3A3D0B1EB522C9975F71DA8 /* TopManager.app */, A3A3D0B1EB522C9975F71DA8 /* TopManager.app */,
0F5A69E0EE7FDD44157911D7 /* TopManagerTests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -203,6 +279,24 @@
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
D28E798AA7BEAA0638DFD52E /* TopManagerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9D898AF16ACAC9453397ADCE /* Build configuration list for PBXNativeTarget "TopManagerTests" */;
buildPhases = (
692062A41592E988A6547E4C /* Sources */,
);
buildRules = (
);
dependencies = (
48A04DA713F139236C91459E /* PBXTargetDependency */,
);
name = TopManagerTests;
packageProductDependencies = (
);
productName = TopManagerTests;
productReference = 0F5A69E0EE7FDD44157911D7 /* TopManagerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
F86DD5615146D759EB0D44EB /* TopManager */ = { F86DD5615146D759EB0D44EB /* TopManager */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 56829675F6738AEDCFCDEE52 /* Build configuration list for PBXNativeTarget "TopManager" */; buildConfigurationList = 56829675F6738AEDCFCDEE52 /* Build configuration list for PBXNativeTarget "TopManager" */;
@@ -230,8 +324,12 @@
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1500; LastUpgradeCheck = 1500;
TargetAttributes = { TargetAttributes = {
D28E798AA7BEAA0638DFD52E = {
DevelopmentTeam = P3663392R2;
ProvisioningStyle = Automatic;
};
F86DD5615146D759EB0D44EB = { F86DD5615146D759EB0D44EB = {
DevelopmentTeam = ""; DevelopmentTeam = P3663392R2;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
}; };
}; };
@@ -251,6 +349,7 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
F86DD5615146D759EB0D44EB /* TopManager */, F86DD5615146D759EB0D44EB /* TopManager */,
D28E798AA7BEAA0638DFD52E /* TopManagerTests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -268,11 +367,27 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
692062A41592E988A6547E4C /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2DE14B65EA1D4E9B4D9DA5A6 /* BatteryMathTests.swift in Sources */,
49D85FF611927294159C89D1 /* FormattingTests.swift in Sources */,
489E29F78A4093D3548673E6 /* HealthAlertTests.swift in Sources */,
24F63686869A69C8B0FCD7FA /* MetricsStoreTests.swift in Sources */,
2B74C177F49B4358D9642712 /* ProcessDataTests.swift in Sources */,
8789C5F4A1AF4B1E8E434D71 /* SettingsTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
CF5286324D09B9417D3070C3 /* Sources */ = { CF5286324D09B9417D3070C3 /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
2B3C4D5E6F7A8B9C0D1E2F3A /* AppsView.swift in Sources */, EB7EAB13C0DC7865BB1A99D5 /* Alert.swift in Sources */,
67F66E7EC7C01A6309DC060E /* AlertCenter.swift in Sources */,
7A6088469A2643D360B35B89 /* AppSettings.swift in Sources */,
A7EC9FB55D5088D669491C11 /* AppsView.swift in Sources */,
DE50ECC69FCE60B10364802C /* ByteFormatters.swift in Sources */, DE50ECC69FCE60B10364802C /* ByteFormatters.swift in Sources */,
FA252F9182ADDA26AD1E54DE /* CPUChartView.swift in Sources */, FA252F9182ADDA26AD1E54DE /* CPUChartView.swift in Sources */,
1F212CE158B7D14B29077B18 /* CPUInfo.swift in Sources */, 1F212CE158B7D14B29077B18 /* CPUInfo.swift in Sources */,
@@ -283,27 +398,42 @@
8F9ED99AB6D8D90AE79B5179 /* DiskUsageView.swift in Sources */, 8F9ED99AB6D8D90AE79B5179 /* DiskUsageView.swift in Sources */,
1F25A84B560709AE71C27D23 /* GPUInfo.swift in Sources */, 1F25A84B560709AE71C27D23 /* GPUInfo.swift in Sources */,
E379C349DC9617D6C505B74B /* GPUMonitor.swift in Sources */, E379C349DC9617D6C505B74B /* GPUMonitor.swift in Sources */,
79E837AE662021C521F7E74F /* HealthModel.swift in Sources */,
6A0810422D2EC8F9F5D191C4 /* HealthView.swift in Sources */,
E40A4E80ED4092A3D3A9C219 /* MemoryDonutView.swift in Sources */, E40A4E80ED4092A3D3A9C219 /* MemoryDonutView.swift in Sources */,
7A1144E43159D96564C13164 /* MemoryInfo.swift in Sources */, 7A1144E43159D96564C13164 /* MemoryInfo.swift in Sources */,
266B888ADE2FDDBCEC8D28A5 /* MemoryLineChart.swift in Sources */, 266B888ADE2FDDBCEC8D28A5 /* MemoryLineChart.swift in Sources */,
F5722D2B18EF7ABF43847EE4 /* MemoryMonitor.swift in Sources */, F5722D2B18EF7ABF43847EE4 /* MemoryMonitor.swift in Sources */,
35852165B2DA76AB7F073723 /* MenuBarView.swift in Sources */, 35852165B2DA76AB7F073723 /* MenuBarView.swift in Sources */,
BCBAC37351B9A3878CC3F01B /* MetricsStore.swift in Sources */,
CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */, CB7F422FBF9F6D152FC4F41B /* NetworkChartView.swift in Sources */,
E4AF7F7CA1E1FE8638CA5A77 /* NetworkInfo.swift in Sources */, E4AF7F7CA1E1FE8638CA5A77 /* NetworkInfo.swift in Sources */,
EB0D84848F42134927F9C9B1 /* NetworkMonitor.swift in Sources */, EB0D84848F42134927F9C9B1 /* NetworkMonitor.swift in Sources */,
00EC5A90B7B8C6C5015D90D1 /* PerformanceView.swift in Sources */, 00EC5A90B7B8C6C5015D90D1 /* PerformanceView.swift in Sources */,
B0165358B483B0ECC28DB4DE /* PowerInfo.swift in Sources */,
D9E882888E23C26B748E22CF /* PowerMonitor.swift in Sources */,
501C13D57386C179C0B3B14B /* PowerStorageView.swift in Sources */, 501C13D57386C179C0B3B14B /* PowerStorageView.swift in Sources */,
C077A279E20A90FD6038CCB0 /* ProcessDetailView.swift in Sources */, C077A279E20A90FD6038CCB0 /* ProcessDetailView.swift in Sources */,
744F52D036D1F3E081923382 /* ProcessInfo.swift in Sources */, 744F52D036D1F3E081923382 /* ProcessInfo.swift in Sources */,
9146403BC02D18A234444DF6 /* ProcessMonitor.swift in Sources */, 9146403BC02D18A234444DF6 /* ProcessMonitor.swift in Sources */,
0AA7D351AEB05078FA9B4B93 /* ProcessView.swift in Sources */, 0AA7D351AEB05078FA9B4B93 /* ProcessView.swift in Sources */,
B89695749D32F394DB459C15 /* SettingsView.swift in Sources */,
DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */, DB3603F30B6F559BE66E4349 /* SystemMonitor.swift in Sources */,
D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */, D6FC8D1EACCB4DED5DF39372 /* TopManagerApp.swift in Sources */,
9F3A8E658859AD9C325E52A8 /* TrendChartView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
48A04DA713F139236C91459E /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = F86DD5615146D759EB0D44EB /* TopManager */;
targetProxy = B01CE3A16D70795D608951EE /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
00F8AAE6BB8D4ED66312B6CB /* Release */ = { 00F8AAE6BB8D4ED66312B6CB /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
@@ -360,7 +490,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = P3663392R2;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
@@ -390,6 +520,24 @@
}; };
name = Debug; name = Debug;
}; };
C0DE23B98ECF14373EAC79A6 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.topmanager.TopManagerTests;
SDKROOT = macosx;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TopManager.app/Contents/MacOS/TopManager";
};
name = Release;
};
C4D5AD1B7274BBED9D423AC0 /* Debug */ = { C4D5AD1B7274BBED9D423AC0 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -445,7 +593,7 @@
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = P3663392R2;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -468,6 +616,24 @@
}; };
name = Release; name = Release;
}; };
EABC414CD1181D6B70269064 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
"@loader_path/../Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.topmanager.TopManagerTests;
SDKROOT = macosx;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TopManager.app/Contents/MacOS/TopManager";
};
name = Debug;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@@ -480,6 +646,15 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug; defaultConfigurationName = Debug;
}; };
9D898AF16ACAC9453397ADCE /* Build configuration list for PBXNativeTarget "TopManagerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EABC414CD1181D6B70269064 /* Debug */,
C0DE23B98ECF14373EAC79A6 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
A70528C1B1F0BAB178B0B048 /* Build configuration list for PBXProject "TopManager" */ = { A70528C1B1F0BAB178B0B048 /* Build configuration list for PBXProject "TopManager" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (

Fájl megtekintése

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F86DD5615146D759EB0D44EB"
BuildableName = "TopManager.app"
BlueprintName = "TopManager"
ReferencedContainer = "container:TopManager.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D28E798AA7BEAA0638DFD52E"
BuildableName = "TopManagerTests.xctest"
BlueprintName = "TopManagerTests"
ReferencedContainer = "container:TopManager.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F86DD5615146D759EB0D44EB"
BuildableName = "TopManager.app"
BlueprintName = "TopManager"
ReferencedContainer = "container:TopManager.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D28E798AA7BEAA0638DFD52E"
BuildableName = "TopManagerTests.xctest"
BlueprintName = "TopManagerTests"
ReferencedContainer = "container:TopManager.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F86DD5615146D759EB0D44EB"
BuildableName = "TopManager.app"
BlueprintName = "TopManager"
ReferencedContainer = "container:TopManager.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F86DD5615146D759EB0D44EB"
BuildableName = "TopManager.app"
BlueprintName = "TopManager"
ReferencedContainer = "container:TopManager.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

Fájl megtekintése

@@ -0,0 +1,68 @@
import Foundation
enum AlertSeverity: Int, Comparable {
case info = 0, warning, critical
static func < (lhs: AlertSeverity, rhs: AlertSeverity) -> Bool { lhs.rawValue < rhs.rawValue }
var label: String {
switch self {
case .info: return "Info"
case .warning: return "Warning"
case .critical: return "Critical"
}
}
var symbol: String {
switch self {
case .info: return "info.circle.fill"
case .warning: return "exclamationmark.triangle.fill"
case .critical: return "exclamationmark.octagon.fill"
}
}
var colorName: String {
switch self {
case .info: return "blue"
case .warning: return "orange"
case .critical: return "red"
}
}
}
/// Distinct alert categories. The kind is the de-dup key: at most one active
/// alert per kind at a time, so we never storm the user with duplicates.
enum AlertKind: String {
case cpuHigh, memoryPressure, diskFull, thermal, runawayProcess, lowBattery
}
/// Named `SystemAlert` (not `Alert`) to avoid shadowing SwiftUI's `Alert` type
/// inside view files.
struct SystemAlert: Identifiable {
let id = UUID()
let kind: AlertKind
let title: String
let message: String
let severity: AlertSeverity
let timestamp: Date
}
/// User-tunable thresholds (wired to Settings in a later increment; sensible
/// defaults here).
struct AlertThresholds: Equatable {
var cpuPercent: Double = 90
var cpuSustainedCycles: Int = 3
var memoryPressureCritical = true
var diskUsedFraction: Double = 0.95
var thermalSerious = true
var processCpuPercent: Double = 190 // per-core (~1.9 cores) sustained
var processSustainedCycles: Int = 3
var lowBatteryPercent = 15
}
/// Pure helper for "sustained" conditions: a breach must persist for N
/// consecutive samples before it fires, and it clears as soon as it recovers.
/// Returned `count` is fed back in on the next call.
enum AlertEvaluator {
static func sustained(breached: Bool, previousCount: Int, requiredCycles: Int) -> (firing: Bool, count: Int) {
let count = breached ? previousCount + 1 : 0
return (count >= requiredCycles, count)
}
}

Fájl megtekintése

@@ -0,0 +1,82 @@
import Foundation
/// Architecture-neutral thermal level so the pure health model doesn't depend on
/// Foundation's `ProcessInfo.ThermalState` (keeps it trivially unit-testable).
enum ThermalLevel {
case nominal, fair, serious, critical
}
/// Everything the health score needs, as plain values.
struct HealthInput {
let cpuUsage: Double // global CPU %
let memoryPressure: MemoryPressure
let swapUsedBytes: UInt64
let thermal: ThermalLevel
let diskUsedFraction: Double // 0…1 for the fullest volume
}
/// Computes a 0–100 "system health" score and a plain-language diagnosis.
/// This is what turns raw numbers into "is my Mac OK, and if not, why?".
enum HealthModel {
static func score(_ input: HealthInput) -> Int {
var score = 100.0
// Sustained high CPU: lose up to ~20 points as usage climbs 60→100%.
if input.cpuUsage > 60 {
score -= min(20, (input.cpuUsage - 60) * 0.5)
}
switch input.memoryPressure {
case .warning: score -= 15
case .critical: score -= 30
default: break
}
if input.swapUsedBytes > 2_000_000_000 { score -= 10 }
else if input.swapUsedBytes > 500_000_000 { score -= 5 }
switch input.thermal {
case .fair: score -= 5
case .serious: score -= 20
case .critical: score -= 35
case .nominal: break
}
if input.diskUsedFraction > 0.95 { score -= 15 }
else if input.diskUsedFraction > 0.90 { score -= 8 }
return max(0, min(100, Int(score.rounded())))
}
static func diagnosis(_ input: HealthInput) -> [String] {
var issues: [String] = []
if input.cpuUsage > 85 {
issues.append("CPU is heavily loaded (\(Int(input.cpuUsage))%)")
}
switch input.memoryPressure {
case .critical: issues.append("Memory pressure is critical — the system is low on RAM")
case .warning: issues.append("Memory pressure is elevated")
default: break
}
if input.swapUsedBytes > 2_000_000_000 {
issues.append("High swap usage — closing apps may help")
}
switch input.thermal {
case .serious, .critical: issues.append("System is running hot and may be throttling")
default: break
}
if input.diskUsedFraction > 0.90 {
issues.append("Startup disk is nearly full (\(Int(input.diskUsedFraction * 100))%)")
}
return issues
}
static func rating(_ score: Int) -> String {
switch score {
case 85...: return "Excellent"
case 70..<85: return "Good"
case 50..<70: return "Fair"
default: return "Poor"
}
}
}

Fájl megtekintése

@@ -0,0 +1,83 @@
import Foundation
/// Battery / power state. Mirrors what Activity Monitor's Energy tab and the
/// macOS battery menu surface, which TopManager currently lacks entirely.
struct PowerInfo {
let hasBattery: Bool
let currentCharge: Int // 0–100 %
let isCharging: Bool
let isPluggedIn: Bool // external power connected
let fullyCharged: Bool
let cycleCount: Int?
let designCapacity: Int? // mAh
let maxCapacity: Int? // mAh (current full-charge capacity)
let temperature: Double? // °C
let voltage: Double? // volts
let amperage: Int? // mA (negative while discharging)
let timeToEmpty: Int? // minutes (nil if unknown/charging)
let timeToFull: Int? // minutes (nil if unknown/discharging)
let adapterWatts: Int? // power adapter rating
/// Battery health as current-full-charge ÷ design capacity.
var healthPercent: Double? {
guard let maxCapacity, let designCapacity, designCapacity > 0 else { return nil }
return BatteryMath.healthPercent(maxCapacity: maxCapacity, designCapacity: designCapacity)
}
var condition: BatteryCondition {
BatteryMath.condition(healthPercent: healthPercent, cycleCount: cycleCount)
}
/// Instantaneous power flow in watts (positive = charging, negative = draining).
var powerWatts: Double? {
guard let voltage, let amperage else { return nil }
return BatteryMath.watts(volts: voltage, milliAmps: amperage)
}
var powerSourceLabel: String {
isPluggedIn ? "AC Power" : "Battery Power"
}
}
enum BatteryCondition: String {
case normal = "Normal"
case serviceRecommended = "Service Recommended"
case unknown = "Unknown"
var color: String {
switch self {
case .normal: return "green"
case .serviceRecommended: return "orange"
case .unknown: return "gray"
}
}
}
/// Pure, testable battery math (no IOKit, no clock).
enum BatteryMath {
static func healthPercent(maxCapacity: Int, designCapacity: Int) -> Double {
guard designCapacity > 0 else { return 0 }
return Double(maxCapacity) / Double(designCapacity) * 100
}
/// Apple flags "Service Recommended" as health degrades or cycles climb.
/// This approximates that classification from the raw numbers.
static func condition(healthPercent: Double?, cycleCount: Int?) -> BatteryCondition {
guard let healthPercent else { return .unknown }
if healthPercent < 80 { return .serviceRecommended }
if let cycleCount, cycleCount > 1000 { return .serviceRecommended }
return .normal
}
static func watts(volts: Double, milliAmps: Int) -> Double {
volts * Double(milliAmps) / 1000.0
}
/// Format a minutes duration as "2h 15m" / "45m"; nil-safe caller.
static func formatMinutes(_ minutes: Int) -> String {
guard minutes > 0 else { return "—" }
let h = minutes / 60
let m = minutes % 60
return h > 0 ? "\(h)h \(m)m" : "\(m)m"
}
}

Fájl megtekintése

@@ -14,9 +14,17 @@ struct ProcessItem: Identifiable, Hashable {
let icon: NSImage? let icon: NSImage?
let parentPid: pid_t let parentPid: pid_t
let startTime: Date? let startTime: Date?
let diskReadRate: Double // bytes/sec read from disk
let diskWriteRate: Double // bytes/sec written to disk
let diskReadBytes: UInt64 // cumulative bytes read
let diskWriteBytes: UInt64 // cumulative bytes written
let energyImpact: Double // heuristic energy-impact score (proxy for Activity Monitor's)
let executablePath: String?
var iconPlaceholder: String { "" } var iconPlaceholder: String { "" }
var diskTotalRate: Double { diskReadRate + diskWriteRate }
init( init(
pid: pid_t, pid: pid_t,
name: String, name: String,
@@ -28,7 +36,13 @@ struct ProcessItem: Identifiable, Hashable {
state: ProcessState, state: ProcessState,
icon: NSImage?, icon: NSImage?,
parentPid: pid_t, parentPid: pid_t,
startTime: Date? startTime: Date?,
diskReadRate: Double = 0,
diskWriteRate: Double = 0,
diskReadBytes: UInt64 = 0,
diskWriteBytes: UInt64 = 0,
energyImpact: Double = 0,
executablePath: String? = nil
) { ) {
self.id = pid self.id = pid
self.pid = pid self.pid = pid
@@ -42,6 +56,12 @@ struct ProcessItem: Identifiable, Hashable {
self.icon = icon self.icon = icon
self.parentPid = parentPid self.parentPid = parentPid
self.startTime = startTime self.startTime = startTime
self.diskReadRate = diskReadRate
self.diskWriteRate = diskWriteRate
self.diskReadBytes = diskReadBytes
self.diskWriteBytes = diskWriteBytes
self.energyImpact = energyImpact
self.executablePath = executablePath
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
@@ -54,7 +74,19 @@ struct ProcessItem: Identifiable, Hashable {
lhs.cpuUsageTotal == rhs.cpuUsageTotal && lhs.cpuUsageTotal == rhs.cpuUsageTotal &&
lhs.memoryUsage == rhs.memoryUsage && lhs.memoryUsage == rhs.memoryUsage &&
lhs.threadCount == rhs.threadCount && lhs.threadCount == rhs.threadCount &&
lhs.state == rhs.state lhs.state == rhs.state &&
lhs.diskReadRate == rhs.diskReadRate &&
lhs.diskWriteRate == rhs.diskWriteRate &&
lhs.energyImpact == rhs.energyImpact
}
}
/// Heuristic "energy impact" proxy. This is *not* Apple's exact private formula;
/// it approximates it: sustained CPU dominates, with a smaller penalty for idle
/// (timer) wakeups, which correlate with power draw even at low CPU.
enum EnergyModel {
static func impact(cpuPercent: Double, idleWakeupsPerSec: Double) -> Double {
max(0, cpuPercent) + max(0, idleWakeupsPerSec) * 0.045
} }
} }

Fájl megtekintése

@@ -0,0 +1,132 @@
import Foundation
import Combine
import UserNotifications
/// The proactive brain: each refresh it computes the system health score and
/// evaluates alert conditions, firing a native notification + inbox entry on the
/// transition into each condition (never duplicating a still-active alert).
@MainActor
final class AlertCenter: ObservableObject {
static let shared = AlertCenter()
@Published var healthScore: Int = 100
@Published var diagnosis: [String] = []
@Published var alerts: [SystemAlert] = [] // inbox, newest first
@Published private(set) var activeKinds: Set<AlertKind> = []
@Published var thresholds = AlertThresholds()
@Published var notificationsEnabled = true
private let inboxLimit = 50
private var cpuCount = 0
private var procCount = 0
private var notificationsAuthorized = false
private init() {}
var activeAlertCount: Int { activeKinds.count }
func requestNotificationAuthorization() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in
Task { @MainActor in self.notificationsAuthorized = granted }
}
}
func evaluate(cpuUsage: Double,
memory: MemoryInfo?,
disk: DiskInfo?,
thermal: ThermalLevel,
topProcess: ProcessItem?,
power: PowerInfo?) {
let diskFraction = disk?.volumes.map { $0.usagePercentage / 100 }.max() ?? 0
// Health score + diagnosis
let input = HealthInput(
cpuUsage: cpuUsage,
memoryPressure: memory?.memoryPressure ?? .unknown,
swapUsedBytes: memory?.swapUsed ?? 0,
thermal: thermal,
diskUsedFraction: diskFraction
)
healthScore = HealthModel.score(input)
diagnosis = HealthModel.diagnosis(input)
// CPU (sustained)
let cpuEval = AlertEvaluator.sustained(breached: cpuUsage >= thresholds.cpuPercent,
previousCount: cpuCount,
requiredCycles: thresholds.cpuSustainedCycles)
cpuCount = cpuEval.count
setActive(.cpuHigh, active: cpuEval.firing,
title: "High CPU usage",
message: String(format: "CPU has stayed above %.0f%% (now %.0f%%).", thresholds.cpuPercent, cpuUsage),
severity: .warning)
// Memory pressure
let memCritical = thresholds.memoryPressureCritical && memory?.memoryPressure == .critical
setActive(.memoryPressure, active: memCritical,
title: "Critical memory pressure",
message: "The system is very low on memory. Consider closing apps.",
severity: .critical)
// Disk
let diskFull = diskFraction >= thresholds.diskUsedFraction
setActive(.diskFull, active: diskFull,
title: "Disk almost full",
message: String(format: "A volume is %.0f%% full.", diskFraction * 100),
severity: .warning)
// Thermal
let thermalBad = thresholds.thermalSerious && (thermal == .serious || thermal == .critical)
setActive(.thermal, active: thermalBad,
title: "System is running hot",
message: "Thermal state is elevated; performance may be throttled.",
severity: thermal == .critical ? .critical : .warning)
// Runaway process (sustained)
let runaway = (topProcess?.cpuUsage ?? 0) >= thresholds.processCpuPercent
let procEval = AlertEvaluator.sustained(breached: runaway,
previousCount: procCount,
requiredCycles: thresholds.processSustainedCycles)
procCount = procEval.count
setActive(.runawayProcess, active: procEval.firing,
title: "Runaway process",
message: topProcess.map { "\($0.name) (PID \($0.pid)) is using \(Int($0.cpuUsage))% CPU." } ?? "",
severity: .warning)
// Low battery
let lowBattery = (power?.hasBattery ?? false)
&& !(power?.isPluggedIn ?? true)
&& (power?.currentCharge ?? 100) <= thresholds.lowBatteryPercent
setActive(.lowBattery, active: lowBattery,
title: "Low battery",
message: "Battery is at \(power?.currentCharge ?? 0)% and not charging.",
severity: .warning)
}
private func setActive(_ kind: AlertKind, active: Bool, title: String, message: String, severity: AlertSeverity) {
if active {
guard !activeKinds.contains(kind) else { return } // de-dup: already firing
activeKinds.insert(kind)
let alert = SystemAlert(kind: kind, title: title, message: message, severity: severity, timestamp: Date())
alerts.insert(alert, at: 0)
if alerts.count > inboxLimit { alerts.removeLast(alerts.count - inboxLimit) }
postNotification(alert)
} else {
activeKinds.remove(kind)
}
}
private func postNotification(_ alert: SystemAlert) {
guard notificationsEnabled, notificationsAuthorized else { return }
let content = UNMutableNotificationContent()
content.title = alert.title
content.body = alert.message
content.sound = alert.severity == .critical ? .defaultCritical : .default
let request = UNNotificationRequest(identifier: alert.id.uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
}
func clearInbox() {
alerts.removeAll()
}
}

Fájl megtekintése

@@ -0,0 +1,108 @@
import Foundation
import ServiceManagement
/// What the menu-bar title shows at a glance.
enum MenuBarMetric: String, CaseIterable, Identifiable {
case cpu = "CPU %"
case memory = "Memory %"
case health = "Health"
case download = "Download"
var id: String { rawValue }
}
/// Wraps the macOS login-item API (SMAppService, macOS 13+).
enum LoginItem {
static var isEnabled: Bool {
SMAppService.mainApp.status == .enabled
}
static func setEnabled(_ enabled: Bool) {
do {
if enabled {
if SMAppService.mainApp.status != .enabled { try SMAppService.mainApp.register() }
} else {
if SMAppService.mainApp.status == .enabled { try SMAppService.mainApp.unregister() }
}
} catch {
NSLog("TopManager: login item toggle failed: \(error.localizedDescription)")
}
}
}
/// Persisted user preferences. Changes are written to `UserDefaults` and pushed
/// into the running services (monitor cadence, alert thresholds, notifications).
@MainActor
final class AppSettings: ObservableObject {
static let shared = AppSettings()
private let d = UserDefaults.standard
@Published var refreshInterval: Double {
didSet {
d.set(refreshInterval, forKey: Keys.refreshInterval)
SystemMonitor.shared.updateRefreshInterval(refreshInterval)
}
}
@Published var menuBarMetric: MenuBarMetric {
didSet { d.set(menuBarMetric.rawValue, forKey: Keys.menuBarMetric) }
}
@Published var notificationsEnabled: Bool {
didSet {
d.set(notificationsEnabled, forKey: Keys.notificationsEnabled)
AlertCenter.shared.notificationsEnabled = notificationsEnabled
}
}
@Published var cpuAlertThreshold: Double {
didSet {
d.set(cpuAlertThreshold, forKey: Keys.cpuAlertThreshold)
AlertCenter.shared.thresholds.cpuPercent = cpuAlertThreshold
}
}
@Published var diskAlertPercent: Double {
didSet {
d.set(diskAlertPercent, forKey: Keys.diskAlertPercent)
AlertCenter.shared.thresholds.diskUsedFraction = diskAlertPercent / 100
}
}
@Published var lowBatteryPercent: Int {
didSet {
d.set(lowBatteryPercent, forKey: Keys.lowBatteryPercent)
AlertCenter.shared.thresholds.lowBatteryPercent = lowBatteryPercent
}
}
@Published var launchAtLogin: Bool {
didSet { LoginItem.setEnabled(launchAtLogin) }
}
private init() {
refreshInterval = AppSettings.clampInterval(d.object(forKey: Keys.refreshInterval) as? Double ?? 3.0)
menuBarMetric = MenuBarMetric(rawValue: d.string(forKey: Keys.menuBarMetric) ?? "") ?? .cpu
notificationsEnabled = d.object(forKey: Keys.notificationsEnabled) as? Bool ?? true
cpuAlertThreshold = d.object(forKey: Keys.cpuAlertThreshold) as? Double ?? 90
diskAlertPercent = d.object(forKey: Keys.diskAlertPercent) as? Double ?? 95
lowBatteryPercent = d.object(forKey: Keys.lowBatteryPercent) as? Int ?? 15
launchAtLogin = LoginItem.isEnabled
}
/// Push all persisted values into the live services (call once on launch).
func applyAll() {
SystemMonitor.shared.updateRefreshInterval(refreshInterval)
AlertCenter.shared.notificationsEnabled = notificationsEnabled
AlertCenter.shared.thresholds.cpuPercent = cpuAlertThreshold
AlertCenter.shared.thresholds.diskUsedFraction = diskAlertPercent / 100
AlertCenter.shared.thresholds.lowBatteryPercent = lowBatteryPercent
}
/// Refresh cadence must stay in a sane range (1–30s).
nonisolated static func clampInterval(_ value: Double) -> Double {
min(30, max(1, value))
}
private enum Keys {
static let refreshInterval = "refreshInterval"
static let menuBarMetric = "menuBarMetric"
static let notificationsEnabled = "notificationsEnabled"
static let cpuAlertThreshold = "cpuAlertThreshold"
static let diskAlertPercent = "diskAlertPercent"
static let lowBatteryPercent = "lowBatteryPercent"
}
}

Fájl megtekintése

@@ -86,14 +86,8 @@ final class GPUMonitor {
} }
private static func getAppleSiliconChipName() -> String? { private static func getAppleSiliconChipName() -> String? {
let matchingDict = IOServiceMatching("IOPlatformExpertDevice")
var service: io_service_t = 0
guard IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &service) == KERN_SUCCESS else {
return nil
}
let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice")) let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
guard platformExpert != 0 else { return nil }
defer { IOObjectRelease(platformExpert) } defer { IOObjectRelease(platformExpert) }
if let modelData = IORegistryEntryCreateCFProperty( if let modelData = IORegistryEntryCreateCFProperty(
@@ -125,6 +119,25 @@ final class GPUMonitor {
} }
} }
/// Walks an `io_iterator_t` and returns the first non-`nil` value `body`
/// produces, releasing **every** object it pulled out of the iterator.
///
/// The obvious `while` + `defer { release; next() }` shape leaks one
/// `io_object_t` whenever the loop is left with `break`/`return`, because
/// the deferred `IOIteratorNext` hands back an object nobody releases. At a
/// GPU poll every few seconds that leaks tens of thousands of mach port
/// rights per day.
private static func firstMatch<T>(in iterator: io_iterator_t, _ body: (io_service_t) -> T?) -> T? {
var result: T?
while result == nil {
let service = IOIteratorNext(iterator)
guard service != 0 else { break }
result = body(service)
IOObjectRelease(service)
}
return result
}
private func fetchAppleSiliconGPUInfo() -> GPUInfo? { private func fetchAppleSiliconGPUInfo() -> GPUInfo? {
// Apple Silicon uses unified memory - get memory pressure instead // Apple Silicon uses unified memory - get memory pressure instead
let totalMemory = ProcessInfo.processInfo.physicalMemory let totalMemory = ProcessInfo.processInfo.physicalMemory
@@ -139,38 +152,31 @@ final class GPUMonitor {
if IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iterator) == KERN_SUCCESS { if IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iterator) == KERN_SUCCESS {
defer { IOObjectRelease(iterator) } defer { IOObjectRelease(iterator) }
var service = IOIteratorNext(iterator) _ = Self.firstMatch(in: iterator) { service -> Bool? in
while service != 0 { guard let props = IORegistryEntryCreateCFProperty(
defer {
IOObjectRelease(service)
service = IOIteratorNext(iterator)
}
if let props = IORegistryEntryCreateCFProperty(
service, service,
"PerformanceStatistics" as CFString, "PerformanceStatistics" as CFString,
kCFAllocatorDefault, kCFAllocatorDefault,
0 0
)?.takeRetainedValue() as? [String: Any] { )?.takeRetainedValue() as? [String: Any] else { return nil }
// Try various keys for GPU memory on Apple Silicon // Try various keys for GPU memory on Apple Silicon
if let inUse = props["In use system memory"] as? UInt64 { if let inUse = props["In use system memory"] as? UInt64 {
inUseSystemMemory = inUse inUseSystemMemory = inUse
} else if let inUse = props["Alloc system memory"] as? UInt64 { } else if let inUse = props["Alloc system memory"] as? UInt64 {
inUseSystemMemory = inUse inUseSystemMemory = inUse
} else if let inUse = props["inUseSystemMemory"] as? UInt64 { } else if let inUse = props["inUseSystemMemory"] as? UInt64 {
inUseSystemMemory = inUse inUseSystemMemory = inUse
}
// GPU utilization
if let util = props["Device Utilization %"] as? Int {
utilizationPercent = Double(util)
} else if let util = props["GPU Activity(%)"] as? Int {
utilizationPercent = Double(util)
}
break
} }
// GPU utilization
if let util = props["Device Utilization %"] as? Int {
utilizationPercent = Double(util)
} else if let util = props["GPU Activity(%)"] as? Int {
utilizationPercent = Double(util)
}
return true
} }
} }
@@ -194,13 +200,7 @@ final class GPUMonitor {
} }
defer { IOObjectRelease(iterator) } defer { IOObjectRelease(iterator) }
var service = IOIteratorNext(iterator) return Self.firstMatch(in: iterator) { service -> GPUInfo? in
while service != 0 {
defer {
IOObjectRelease(service)
service = IOIteratorNext(iterator)
}
var name = "GPU" var name = "GPU"
if let modelData = IORegistryEntryCreateCFProperty( if let modelData = IORegistryEntryCreateCFProperty(
service, service,
@@ -246,8 +246,8 @@ final class GPUMonitor {
isUnifiedMemory: false isUnifiedMemory: false
) )
} }
}
return nil return nil
}
} }
} }

Fájl megtekintése

@@ -0,0 +1,190 @@
import Foundation
/// A single point-in-time snapshot of system-wide metrics, persisted to disk so
/// history survives app restarts (the live in-RAM histories only hold ~60 points).
struct MetricsSample: Codable, Identifiable, Equatable {
let t: Date // timestamp
let cpu: Double // global CPU usage %
let memUsed: UInt64
let memTotal: UInt64
let netDown: Double // bytes/sec
let netUp: Double // bytes/sec
var id: Date { t }
var memPercent: Double {
memTotal > 0 ? Double(memUsed) / Double(memTotal) * 100 : 0
}
}
/// Pure, side-effect-free math over samples. Kept separate from `MetricsStore`
/// so it is trivially unit-testable without touching disk or the clock.
enum MetricsMath {
/// Drop samples older than `maxAge` (relative to `now`) and cap total count,
/// keeping the most recent `maxCount`.
static func trim(_ samples: [MetricsSample], now: Date, maxAge: TimeInterval, maxCount: Int) -> [MetricsSample] {
let cutoff = now.addingTimeInterval(-maxAge)
var result = samples.filter { $0.t >= cutoff }
if result.count > maxCount {
result = Array(result.suffix(maxCount))
}
return result
}
/// Average samples into fixed-width time buckets so long ranges (e.g. 24h)
/// plot ~100 points instead of tens of thousands. `bucket <= 0` returns input.
static func downsample(_ samples: [MetricsSample], bucket: TimeInterval) -> [MetricsSample] {
guard bucket > 0, samples.count > 1 else { return samples }
var groups: [Int64: [MetricsSample]] = [:]
for s in samples {
let key = Int64((s.t.timeIntervalSince1970 / bucket).rounded(.down))
groups[key, default: []].append(s)
}
return groups.keys.sorted().map { key in
let group = groups[key]!
let n = Double(group.count)
let midpoint = Date(timeIntervalSince1970: (Double(key) + 0.5) * bucket)
return MetricsSample(
t: midpoint,
cpu: group.reduce(0) { $0 + $1.cpu } / n,
memUsed: UInt64(group.reduce(0.0) { $0 + Double($1.memUsed) } / n),
memTotal: group.map { $0.memTotal }.max() ?? 0,
netDown: group.reduce(0) { $0 + $1.netDown } / n,
netUp: group.reduce(0) { $0 + $1.netUp } / n
)
}
}
}
/// Selectable time window for trend charts.
enum HistoryRange: String, CaseIterable, Identifiable {
case live = "Live"
case m5 = "5m"
case m30 = "30m"
case h1 = "1h"
case h24 = "24h"
var id: String { rawValue }
/// Window length in seconds; `nil` for the live (in-RAM) view.
var seconds: TimeInterval? {
switch self {
case .live: return nil
case .m5: return 5 * 60
case .m30: return 30 * 60
case .h1: return 60 * 60
case .h24: return 24 * 60 * 60
}
}
/// Downsample bucket size chosen to keep ~100–150 plotted points.
var bucket: TimeInterval {
switch self {
case .live: return 0
case .m5: return 0 // ~raw (refresh cadence already ~3s)
case .m30: return 15
case .h1: return 30
case .h24: return 600
}
}
}
/// Thread-safe on-disk ring buffer of `MetricsSample`s.
///
/// Access to the backing array is guarded by a lock; file writes happen on a
/// background queue from a snapshot so the recording thread never blocks on I/O.
final class MetricsStore {
static let shared = MetricsStore()
private let lock = NSLock()
private var _samples: [MetricsSample] = []
private let fileURL: URL
private let maxAge: TimeInterval
private let maxCount: Int
private let saveEvery: Int
private var unsaved = 0
private let ioQueue = DispatchQueue(label: "com.topmanager.metricsstore.io", qos: .utility)
/// - Note: `saveEvery` is deliberately coarse. Every save re-encodes the
/// *whole* history (up to `maxCount` samples), which costs a few hundred
/// thousand transient allocations; at the old cadence of 10 that ran twice
/// a minute and kept the malloc small zone needlessly dirty. 40 samples is
/// ~2 minutes at the default 3 s refresh, and `flush()` still runs on quit
/// and when the app goes to the background.
init(fileURL: URL? = nil,
maxAge: TimeInterval = 24 * 60 * 60,
maxCount: Int = 20_000,
saveEvery: Int = 40) {
self.fileURL = fileURL ?? Self.defaultFileURL()
self.maxAge = maxAge
self.maxCount = maxCount
self.saveEvery = saveEvery
load()
}
static func defaultFileURL() -> URL {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
?? URL(fileURLWithPath: NSTemporaryDirectory())
let dir = base.appendingPathComponent("TopManager", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir.appendingPathComponent("metrics-history.json")
}
/// Append a sample, trim, and persist on a throttled cadence.
func record(_ sample: MetricsSample) {
lock.lock()
_samples.append(sample)
_samples = MetricsMath.trim(_samples, now: sample.t, maxAge: maxAge, maxCount: maxCount)
unsaved += 1
let snapshot: [MetricsSample]? = unsaved >= saveEvery ? _samples : nil
if snapshot != nil { unsaved = 0 }
lock.unlock()
if let snapshot { persist(snapshot) }
}
func samples(since date: Date) -> [MetricsSample] {
lock.lock(); defer { lock.unlock() }
return _samples.filter { $0.t >= date }
}
var count: Int {
lock.lock(); defer { lock.unlock() }
return _samples.count
}
/// Samples for a chart range, downsampled for long windows.
func downsampled(for range: HistoryRange, now: Date = Date()) -> [MetricsSample] {
guard let secs = range.seconds else { return [] }
let windowed = samples(since: now.addingTimeInterval(-secs))
return MetricsMath.downsample(windowed, bucket: range.bucket)
}
/// Force an immediate save (e.g. on stop / quit).
func flush() {
lock.lock()
let snapshot = _samples
unsaved = 0
lock.unlock()
persist(snapshot)
}
private func persist(_ snapshot: [MetricsSample]) {
ioQueue.async { [fileURL] in
guard let data = try? JSONEncoder().encode(snapshot) else { return }
try? data.write(to: fileURL, options: .atomic)
}
}
private func load() {
guard let data = try? Data(contentsOf: fileURL),
let decoded = try? JSONDecoder().decode([MetricsSample].self, from: data) else {
return
}
lock.lock()
_samples = MetricsMath.trim(decoded, now: Date(), maxAge: maxAge, maxCount: maxCount)
lock.unlock()
}
}

Fájl megtekintése

@@ -0,0 +1,77 @@
import Foundation
import IOKit
/// Reads battery / power state from the AppleSmartBattery IORegistry entry.
/// No entitlements or root required. On a desktop Mac (no battery) it reports
/// `hasBattery == false` and AC power.
final class PowerMonitor {
func fetchPowerInfo() -> PowerInfo {
let service = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("AppleSmartBattery"))
guard service != 0 else {
// No battery (e.g. Mac mini / Studio / Pro): assume AC power.
return PowerInfo(
hasBattery: false, currentCharge: 100, isCharging: false, isPluggedIn: true,
fullyCharged: true, cycleCount: nil, designCapacity: nil, maxCapacity: nil,
temperature: nil, voltage: nil, amperage: nil, timeToEmpty: nil,
timeToFull: nil, adapterWatts: nil
)
}
defer { IOObjectRelease(service) }
var unmanagedProps: Unmanaged<CFMutableDictionary>?
guard IORegistryEntryCreateCFProperties(service, &unmanagedProps, kCFAllocatorDefault, 0) == KERN_SUCCESS,
let props = unmanagedProps?.takeRetainedValue() as? [String: Any] else {
return PowerInfo(
hasBattery: false, currentCharge: 100, isCharging: false, isPluggedIn: true,
fullyCharged: true, cycleCount: nil, designCapacity: nil, maxCapacity: nil,
temperature: nil, voltage: nil, amperage: nil, timeToEmpty: nil,
timeToFull: nil, adapterWatts: nil
)
}
let int = { (key: String) -> Int? in props[key] as? Int }
let bool = { (key: String) -> Bool? in props[key] as? Bool }
// Percent charge: CurrentCapacity is 0–100 on Apple Silicon.
let currentCharge = int("CurrentCapacity") ?? 0
let isCharging = bool("IsCharging") ?? false
let isPluggedIn = bool("ExternalConnected") ?? false
let fullyCharged = bool("FullyCharged") ?? false
let cycleCount = int("CycleCount")
let designCapacity = int("DesignCapacity")
// Raw (mAh) capacity is the meaningful "full charge capacity" on AS.
let maxCapacity = int("AppleRawMaxCapacity") ?? int("MaxCapacity")
let temperature = int("Temperature").map { Double($0) / 100.0 } // centi-°C
let voltage = int("Voltage").map { Double($0) / 1000.0 } // mV → V
let amperage = int("Amperage")
let timeToEmpty = normalizedMinutes(int("AvgTimeToEmpty"), valid: !isPluggedIn)
let timeToFull = normalizedMinutes(int("AvgTimeToFull"), valid: isCharging)
let adapterWatts = (props["AdapterDetails"] as? [String: Any])?["Watts"] as? Int
return PowerInfo(
hasBattery: true,
currentCharge: currentCharge,
isCharging: isCharging,
isPluggedIn: isPluggedIn,
fullyCharged: fullyCharged,
cycleCount: cycleCount,
designCapacity: designCapacity,
maxCapacity: maxCapacity,
temperature: temperature,
voltage: voltage,
amperage: amperage,
timeToEmpty: timeToEmpty,
timeToFull: timeToFull,
adapterWatts: adapterWatts
)
}
/// AppleSmartBattery reports 65535 (or 0) for "still calculating"; treat as unknown.
private func normalizedMinutes(_ value: Int?, valid: Bool) -> Int? {
guard valid, let value, value > 0, value != 65535 else { return nil }
return value
}
}

Fájl megtekintése

@@ -4,10 +4,12 @@ import AppKit
final class ProcessMonitor { final class ProcessMonitor {
private var previousCPUTimes: [pid_t: (user: UInt64, system: UInt64, timestamp: Date)] = [:] private var previousCPUTimes: [pid_t: (user: UInt64, system: UInt64, timestamp: Date)] = [:]
private var previousDiskIO: [pid_t: (read: UInt64, write: UInt64, idle: UInt64, timestamp: Date)] = [:]
private var lastKnownCPU: [pid_t: Double] = [:] // Cache last known CPU usage private var lastKnownCPU: [pid_t: Double] = [:] // Cache last known CPU usage
private let iconCache = NSCache<NSNumber, NSImage>() private let iconCache = NSCache<NSNumber, NSImage>()
private var noIconPids: Set<pid_t> = [] // Cache for PIDs with no icon private var noIconPids: Set<pid_t> = [] // Cache for PIDs with no icon
private var nameCache: [pid_t: String] = [:] private var nameCache: [pid_t: String] = [:]
private var pathCache: [pid_t: String] = [:] // Full executable path
private var userCache: [uid_t: String] = [:] private var userCache: [uid_t: String] = [:]
private let timebaseInfo: mach_timebase_info_data_t private let timebaseInfo: mach_timebase_info_data_t
private var refreshCounter = 0 private var refreshCounter = 0
@@ -48,8 +50,10 @@ final class ProcessMonitor {
let stalePids = Set(nameCache.keys).subtracting(currentPids) let stalePids = Set(nameCache.keys).subtracting(currentPids)
for pid in stalePids { for pid in stalePids {
nameCache.removeValue(forKey: pid) nameCache.removeValue(forKey: pid)
pathCache.removeValue(forKey: pid)
iconCache.removeObject(forKey: NSNumber(value: pid)) iconCache.removeObject(forKey: NSNumber(value: pid))
previousCPUTimes.removeValue(forKey: pid) previousCPUTimes.removeValue(forKey: pid)
previousDiskIO.removeValue(forKey: pid)
noIconPids.remove(pid) noIconPids.remove(pid)
lastKnownCPU.removeValue(forKey: pid) lastKnownCPU.removeValue(forKey: pid)
} }
@@ -80,6 +84,11 @@ final class ProcessMonitor {
let memoryUsage: Int64 let memoryUsage: Int64
let threadCount: Int32 let threadCount: Int32
let cpuUsage: Double let cpuUsage: Double
var diskReadRate: Double = 0
var diskWriteRate: Double = 0
var diskReadBytes: UInt64 = 0
var diskWriteBytes: UInt64 = 0
var energyImpact: Double = 0
if needsDetailedInfo { if needsDetailedInfo {
var taskInfo = proc_taskinfo() var taskInfo = proc_taskinfo()
@@ -96,6 +105,17 @@ final class ProcessMonitor {
userTime: rusageData.userTime, userTime: rusageData.userTime,
systemTime: rusageData.systemTime systemTime: rusageData.systemTime
) )
let extra = computeExtraRates(
pid: pid,
diskRead: rusageData.diskRead,
diskWrite: rusageData.diskWrite,
idleWakeups: rusageData.idleWakeups
)
diskReadRate = extra.readRate
diskWriteRate = extra.writeRate
diskReadBytes = rusageData.diskRead
diskWriteBytes = rusageData.diskWrite
energyImpact = EnergyModel.impact(cpuPercent: cpuUsage, idleWakeupsPerSec: extra.idleWakeupsPerSec)
} else { } else {
memoryUsage = 0 memoryUsage = 0
threadCount = 0 threadCount = 0
@@ -126,7 +146,13 @@ final class ProcessMonitor {
state: state, state: state,
icon: icon, icon: icon,
parentPid: parentPid, parentPid: parentPid,
startTime: startTime startTime: startTime,
diskReadRate: diskReadRate,
diskWriteRate: diskWriteRate,
diskReadBytes: diskReadBytes,
diskWriteBytes: diskWriteBytes,
energyImpact: energyImpact,
executablePath: pathCache[pid]
) )
} }
@@ -150,6 +176,7 @@ final class ProcessMonitor {
// This protects against edge cases where buffer might not be null-terminated // This protects against edge cases where buffer might not be null-terminated
let pathData = Data(bytes: pathBuffer, count: pathLength) let pathData = Data(bytes: pathBuffer, count: pathLength)
if let path = String(data: pathData, encoding: .utf8) { if let path = String(data: pathData, encoding: .utf8) {
pathCache[pid] = path
name = (path as NSString).lastPathComponent name = (path as NSString).lastPathComponent
} else { } else {
name = "Process \(pid)" name = "Process \(pid)"
@@ -203,7 +230,16 @@ final class ProcessMonitor {
return name return name
} }
private func fetchRusageData(pid: pid_t) -> (memory: Int64, userTime: UInt64, systemTime: UInt64) { private struct RusageData {
var memory: Int64 = 0
var userTime: UInt64 = 0
var systemTime: UInt64 = 0
var diskRead: UInt64 = 0
var diskWrite: UInt64 = 0
var idleWakeups: UInt64 = 0
}
private func fetchRusageData(pid: pid_t) -> RusageData {
var rusage = rusage_info_v4() var rusage = rusage_info_v4()
let result = withUnsafeMutablePointer(to: &rusage) { ptr -> Int32 in let result = withUnsafeMutablePointer(to: &rusage) { ptr -> Int32 in
ptr.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) { rusagePtr in ptr.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) { rusagePtr in
@@ -212,25 +248,45 @@ final class ProcessMonitor {
} }
if result == 0 { if result == 0 {
return ( return RusageData(
memory: Int64(rusage.ri_phys_footprint), memory: Int64(rusage.ri_phys_footprint),
userTime: rusage.ri_user_time, userTime: rusage.ri_user_time,
systemTime: rusage.ri_system_time systemTime: rusage.ri_system_time,
diskRead: rusage.ri_diskio_bytesread,
diskWrite: rusage.ri_diskio_byteswritten,
idleWakeups: rusage.ri_pkg_idle_wkups
) )
} }
// Fallback to proc_taskinfo if rusage fails // Fallback to proc_taskinfo if rusage fails (no disk/wakeup data available)
var taskInfo = proc_taskinfo() var taskInfo = proc_taskinfo()
let taskInfoSize = Int32(MemoryLayout<proc_taskinfo>.size) let taskInfoSize = Int32(MemoryLayout<proc_taskinfo>.size)
if proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, taskInfoSize) == taskInfoSize { if proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, taskInfoSize) == taskInfoSize {
return ( return RusageData(
memory: Int64(taskInfo.pti_resident_size), memory: Int64(taskInfo.pti_resident_size),
userTime: taskInfo.pti_total_user, userTime: taskInfo.pti_total_user,
systemTime: taskInfo.pti_total_system systemTime: taskInfo.pti_total_system
) )
} }
return (memory: 0, userTime: 0, systemTime: 0) return RusageData()
}
/// Computes per-second disk read/write and idle-wakeup rates from cumulative
/// counters, using the previous snapshot. Mirrors the CPU delta approach.
private func computeExtraRates(pid: pid_t, diskRead: UInt64, diskWrite: UInt64, idleWakeups: UInt64)
-> (readRate: Double, writeRate: Double, idleWakeupsPerSec: Double) {
let now = Date()
defer { previousDiskIO[pid] = (diskRead, diskWrite, idleWakeups, now) }
guard let prev = previousDiskIO[pid] else { return (0, 0, 0) }
let dt = now.timeIntervalSince(prev.timestamp)
guard dt > 0 else { return (0, 0, 0) }
let readRate = diskRead >= prev.read ? Double(diskRead - prev.read) / dt : 0
let writeRate = diskWrite >= prev.write ? Double(diskWrite - prev.write) / dt : 0
let idleRate = idleWakeups >= prev.idle ? Double(idleWakeups - prev.idle) / dt : 0
return (readRate, writeRate, idleRate)
} }
private func fetchBasicProcessInfo(pid: pid_t) -> ProcessItem? { private func fetchBasicProcessInfo(pid: pid_t) -> ProcessItem? {
@@ -373,3 +429,55 @@ final class ProcessMonitor {
previousCPUTimes.removeAll() previousCPUTimes.removeAll()
} }
} }
// MARK: - On-demand deep inspection (called only when the inspector is open)
/// Pure parser for a KERN_PROCARGS2 buffer, split out so it is unit-testable
/// without a live process. Layout: [Int32 argc][exec_path\0][\0 padding][argv…][envp…]
enum ProcArgs {
static func parse(_ bytes: [UInt8]) -> (path: String, args: [String])? {
guard bytes.count > 4 else { return nil }
// argc: first 4 bytes, host little-endian (arm64/x86_64)
let argc = Int(bytes[0]) | (Int(bytes[1]) << 8) | (Int(bytes[2]) << 16) | (Int(bytes[3]) << 24)
guard argc >= 0 else { return nil }
var i = 4
let pathStart = i
while i < bytes.count && bytes[i] != 0 { i += 1 }
let path = String(decoding: bytes[pathStart..<i], as: UTF8.self)
// Skip the NUL padding between exec_path and argv[0]
while i < bytes.count && bytes[i] == 0 { i += 1 }
var args: [String] = []
var consumed = 0
while consumed < argc && i < bytes.count {
let start = i
while i < bytes.count && bytes[i] != 0 { i += 1 }
args.append(String(decoding: bytes[start..<i], as: UTF8.self))
i += 1 // skip the NUL terminator
consumed += 1
}
return (path, args)
}
}
/// Fetches the launch arguments for a process (works for the current user's
/// processes without elevated privileges; returns [] when denied/unavailable).
func fetchProcessArguments(pid: pid_t) -> [String] {
var mib: [Int32] = [CTL_KERN, KERN_PROCARGS2, pid]
var size = 0
guard sysctl(&mib, 3, nil, &size, nil, 0) == 0, size > 0 else { return [] }
var buffer = [UInt8](repeating: 0, count: size)
guard sysctl(&mib, 3, &buffer, &size, nil, 0) == 0 else { return [] }
// sysctl may shrink `size`; only parse what was actually written.
return ProcArgs.parse(Array(buffer.prefix(size)))?.args ?? []
}
/// Counts open file descriptors for a process (files, sockets, pipes, …).
func fetchOpenFileCount(pid: pid_t) -> Int? {
let bufferSize = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0)
guard bufferSize > 0 else { return nil }
return Int(bufferSize) / MemoryLayout<proc_fdinfo>.stride
}

Fájl megtekintése

@@ -13,6 +13,7 @@ final class SystemMonitor: ObservableObject {
@MainActor @Published var diskInfo: DiskInfo? @MainActor @Published var diskInfo: DiskInfo?
@MainActor @Published var networkInfo: NetworkInfo? @MainActor @Published var networkInfo: NetworkInfo?
@MainActor @Published var gpuInfo: GPUInfo? @MainActor @Published var gpuInfo: GPUInfo?
@MainActor @Published var powerInfo: PowerInfo?
@MainActor @Published var lastError: String? @MainActor @Published var lastError: String?
// History for charts // History for charts
@@ -28,6 +29,7 @@ final class SystemMonitor: ObservableObject {
private let diskMonitor = DiskMonitor() private let diskMonitor = DiskMonitor()
private let networkMonitor = NetworkMonitor() private let networkMonitor = NetworkMonitor()
private let gpuMonitor = GPUMonitor() private let gpuMonitor = GPUMonitor()
private let powerMonitor = PowerMonitor()
// Timer // Timer
private var timer: Timer? private var timer: Timer?
@@ -63,24 +65,37 @@ final class SystemMonitor: ObservableObject {
let processData = self.processMonitor.fetchProcesses() let processData = self.processMonitor.fetchProcesses()
let diskData = self.diskMonitor.fetchDiskInfo() let diskData = self.diskMonitor.fetchDiskInfo()
let gpuData = self.gpuMonitor.fetchGPUInfo() let gpuData = self.gpuMonitor.fetchGPUInfo()
let powerData = self.powerMonitor.fetchPowerInfo()
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateUI(cpu: cpuData, memory: memData, network: netData, self.updateUI(cpu: cpuData, memory: memData, network: netData,
processes: processData, disk: diskData, gpu: gpuData) processes: processData, disk: diskData, gpu: gpuData, power: powerData)
} }
} }
// Start periodic refresh (3 second interval) // Start periodic refresh at the user-configured cadence
timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in scheduleTimer(interval: AppSettings.shared.refreshInterval)
}
private func scheduleTimer(interval: Double) {
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
self?.backgroundQueue.async { self?.backgroundQueue.async {
self?.refreshAllBackground() self?.refreshAllBackground()
} }
} }
} }
/// Change the refresh cadence at runtime (from Settings). No-op if not monitoring.
@MainActor func updateRefreshInterval(_ interval: Double) {
guard timer != nil else { return }
scheduleTimer(interval: AppSettings.clampInterval(interval))
}
@MainActor func stopMonitoring() { @MainActor func stopMonitoring() {
timer?.invalidate() timer?.invalidate()
timer = nil timer = nil
MetricsStore.shared.flush()
} }
private func refreshAllBackground() { private func refreshAllBackground() {
@@ -98,23 +113,26 @@ final class SystemMonitor: ObservableObject {
processData = processMonitor.fetchProcesses() processData = processMonitor.fetchProcesses()
} }
// Fetch disk and GPU every 3 cycles // Fetch disk, GPU and power every 3 cycles
var diskData: DiskInfo? = nil var diskData: DiskInfo? = nil
var gpuData: GPUInfo? = nil var gpuData: GPUInfo? = nil
var powerData: PowerInfo? = nil
if currentCount % 3 == 0 { if currentCount % 3 == 0 {
diskData = diskMonitor.fetchDiskInfo() diskData = diskMonitor.fetchDiskInfo()
gpuData = gpuMonitor.fetchGPUInfo() gpuData = gpuMonitor.fetchGPUInfo()
powerData = powerMonitor.fetchPowerInfo()
} }
// Update UI on main thread // Update UI on main thread
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.updateUI(cpu: cpuData, memory: memData, network: netData, self?.updateUI(cpu: cpuData, memory: memData, network: netData,
processes: processData, disk: diskData, gpu: gpuData) processes: processData, disk: diskData, gpu: gpuData, power: powerData)
} }
} }
@MainActor private func updateUI(cpu: CPUInfo?, memory: MemoryInfo?, network: NetworkInfo?, @MainActor private func updateUI(cpu: CPUInfo?, memory: MemoryInfo?, network: NetworkInfo?,
processes: [ProcessItem]?, disk: DiskInfo?, gpu: GPUInfo?) { processes: [ProcessItem]?, disk: DiskInfo?, gpu: GPUInfo?,
power: PowerInfo?) {
if let info = cpu { if let info = cpu {
cpuInfo = info cpuInfo = info
let historyPoint = CPUHistoryPoint( let historyPoint = CPUHistoryPoint(
@@ -177,6 +195,42 @@ final class SystemMonitor: ObservableObject {
if let info = gpu { if let info = gpu {
gpuInfo = info gpuInfo = info
} }
if let info = power {
powerInfo = info
}
// Persist a system-wide sample so history survives restarts and long ranges.
if let c = cpuInfo, let m = memoryInfo {
MetricsStore.shared.record(MetricsSample(
t: c.timestamp,
cpu: c.globalUsage,
memUsed: m.usedMemory,
memTotal: m.totalMemory,
netDown: networkInfo?.totalDownloadRate ?? 0,
netUp: networkInfo?.totalUploadRate ?? 0
))
}
// Proactive health + alert evaluation (runs every cycle).
AlertCenter.shared.evaluate(
cpuUsage: cpuInfo?.globalUsage ?? 0,
memory: memoryInfo,
disk: diskInfo,
thermal: currentThermalLevel(),
topProcess: self.processes.max { $0.cpuUsage < $1.cpuUsage },
power: powerInfo
)
}
private func currentThermalLevel() -> ThermalLevel {
switch ProcessInfo.processInfo.thermalState {
case .nominal: return .nominal
case .fair: return .fair
case .serious: return .serious
case .critical: return .critical
@unknown default: return .nominal
}
} }
// Process control methods with PID validation // Process control methods with PID validation

Fájl megtekintése

@@ -1,34 +1,68 @@
import SwiftUI import SwiftUI
import UserNotifications
/// Flushes persisted metrics history when the app is quit, and lets alert banners
/// appear even while TopManager is the foreground app (macOS suppresses an app's
/// own notifications in the foreground unless the delegate opts in).
final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
UNUserNotificationCenter.current().delegate = self
}
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .sound, .list])
}
func applicationWillTerminate(_ notification: Notification) {
MetricsStore.shared.flush()
}
}
@main @main
struct TopManagerApp: App { struct TopManagerApp: App {
@StateObject private var monitor = SystemMonitor.shared @StateObject private var monitor = SystemMonitor.shared
@StateObject private var settings = AppSettings.shared
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@Environment(\.scenePhase) private var scenePhase
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView()
.environmentObject(monitor) .environmentObject(monitor)
.environmentObject(AlertCenter.shared)
.environmentObject(settings)
.onAppear { .onAppear {
monitor.startMonitoring() monitor.startMonitoring()
settings.applyAll()
AlertCenter.shared.requestNotificationAuthorization()
} }
} }
.windowStyle(.automatic) .windowStyle(.automatic)
.defaultSize(width: 1000, height: 700) .defaultSize(width: 1000, height: 700)
.onChange(of: scenePhase) { phase in
if phase == .background {
MetricsStore.shared.flush()
}
}
.commands { .commands {
CommandGroup(replacing: .newItem) { } CommandGroup(replacing: .newItem) { }
} }
// Menu Bar Extra showing CPU % Settings {
SettingsView()
.environmentObject(settings)
}
// Menu Bar Extra with configurable metric
MenuBarExtra { MenuBarExtra {
MenuBarView() MenuBarView()
.environmentObject(monitor) .environmentObject(monitor)
.environmentObject(AlertCenter.shared)
.environmentObject(settings)
} label: { } label: {
if let cpu = monitor.cpuInfo { MenuBarLabel(monitor: monitor, settings: settings, alerts: AlertCenter.shared)
Text(String(format: "%.0f%%", cpu.globalUsage))
.monospacedDigit()
} else {
Image(systemName: "cpu")
}
} }
.menuBarExtraStyle(.window) .menuBarExtraStyle(.window)
} }

Fájl megtekintése

@@ -1,8 +1,15 @@
import SwiftUI import SwiftUI
/// The main window's tab shell.
///
/// IMPORTANT: this view must stay free of any observable dependency
/// (`@EnvironmentObject`, `@StateObject`, `@State`, …). On macOS, every
/// re-evaluation of a body containing `.tabItem` permanently retains the
/// wrapped `AnyView(Label(…))` inside the AppKit tab bridge, so a body that
/// re-runs on the monitor's refresh cadence leaks ~6 KB per tick (≈440 MB over
/// three days). With no stored properties SwiftUI evaluates this body exactly
/// once. The tab *contents* observe `SystemMonitor` themselves.
struct ContentView: View { struct ContentView: View {
@EnvironmentObject var monitor: SystemMonitor
var body: some View { var body: some View {
TabView { TabView {
ProcessView() ProcessView()

Fájl megtekintése

@@ -0,0 +1,135 @@
import SwiftUI
/// System Health summary card: a 0–100 score, rating, and plain-language
/// diagnosis of what (if anything) is wrong right now.
struct HealthCardView: View {
@EnvironmentObject var alertCenter: AlertCenter
var body: some View {
GroupBox {
HStack(alignment: .top, spacing: 20) {
// Score ring
ZStack {
Circle()
.stroke(Color.secondary.opacity(0.2), lineWidth: 10)
Circle()
.trim(from: 0, to: CGFloat(alertCenter.healthScore) / 100)
.stroke(scoreColor, style: StrokeStyle(lineWidth: 10, lineCap: .round))
.rotationEffect(.degrees(-90))
VStack(spacing: 0) {
Text("\(alertCenter.healthScore)")
.font(.system(size: 30, weight: .bold, design: .rounded))
.monospacedDigit()
Text(HealthModel.rating(alertCenter.healthScore))
.font(.caption)
.foregroundColor(.secondary)
}
}
.frame(width: 96, height: 96)
VStack(alignment: .leading, spacing: 8) {
HStack {
Label("System Health", systemImage: "heart.text.square")
.font(.headline)
Spacer()
if alertCenter.activeAlertCount > 0 {
Label("\(alertCenter.activeAlertCount) active", systemImage: "bell.badge.fill")
.font(.caption)
.foregroundColor(.orange)
}
}
if alertCenter.diagnosis.isEmpty {
Label("Everything looks healthy.", systemImage: "checkmark.circle.fill")
.font(.callout)
.foregroundColor(.green)
} else {
ForEach(alertCenter.diagnosis, id: \.self) { issue in
Label(issue, systemImage: "exclamationmark.triangle.fill")
.font(.callout)
.foregroundColor(.secondary)
}
}
Spacer(minLength: 0)
}
}
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
private var scoreColor: Color {
switch alertCenter.healthScore {
case 85...: return .green
case 70..<85: return .mint
case 50..<70: return .orange
default: return .red
}
}
}
/// Inbox of recent alerts fired by the AlertCenter.
struct AlertsInboxView: View {
@EnvironmentObject var alertCenter: AlertCenter
var body: some View {
GroupBox {
VStack(alignment: .leading, spacing: 10) {
HStack {
Label("Recent Alerts", systemImage: "bell")
.font(.headline)
Spacer()
if !alertCenter.alerts.isEmpty {
Button("Clear") { alertCenter.clearInbox() }
.buttonStyle(.borderless)
.font(.caption)
}
}
if alertCenter.alerts.isEmpty {
Text("No alerts. TopManager will notify you if something needs attention.")
.font(.callout)
.foregroundColor(.secondary)
} else {
ForEach(alertCenter.alerts.prefix(8)) { alert in
AlertRowView(alert: alert)
}
}
}
.padding(.vertical, 8)
}
}
}
struct AlertRowView: View {
let alert: SystemAlert
var body: some View {
HStack(alignment: .top, spacing: 8) {
Image(systemName: alert.severity.symbol)
.foregroundColor(severityColor)
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(alert.title).fontWeight(.medium)
Spacer()
Text(alert.timestamp, style: .time)
.font(.caption)
.foregroundColor(.secondary)
}
Text(alert.message)
.font(.caption)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}
Divider()
}
private var severityColor: Color {
switch alert.severity {
case .info: return .blue
case .warning: return .orange
case .critical: return .red
}
}
}

Fájl megtekintése

@@ -1,10 +1,55 @@
import SwiftUI import SwiftUI
/// The compact menu-bar title. Renders whichever metric the user picked.
struct MenuBarLabel: View {
@ObservedObject var monitor: SystemMonitor
@ObservedObject var settings: AppSettings
@ObservedObject var alerts: AlertCenter
var body: some View {
switch settings.menuBarMetric {
case .cpu:
textOrIcon(monitor.cpuInfo.map { String(format: "%.0f%%", $0.globalUsage) }, icon: "cpu")
case .memory:
textOrIcon(monitor.memoryInfo.map { String(format: "%.0f%%", $0.usagePercentage) }, icon: "memorychip")
case .health:
Text("♥ \(alerts.healthScore)").monospacedDigit()
case .download:
textOrIcon(monitor.networkInfo.map { formatBytesPerSecond($0.totalDownloadRate) }, icon: "arrow.down")
}
}
@ViewBuilder private func textOrIcon(_ text: String?, icon: String) -> some View {
if let text { Text(text).monospacedDigit() } else { Image(systemName: icon) }
}
}
struct MenuBarView: View { struct MenuBarView: View {
@EnvironmentObject var monitor: SystemMonitor @EnvironmentObject var monitor: SystemMonitor
@EnvironmentObject var alertCenter: AlertCenter
private var topCPU: [ProcessItem] {
monitor.processes.sorted { $0.cpuUsage > $1.cpuUsage }.prefix(3).map { $0 }
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "heart.text.square")
.frame(width: 16)
.foregroundColor(healthColor)
Text("Health:")
Spacer()
Text("\(alertCenter.healthScore)/100")
.monospacedDigit()
.foregroundColor(healthColor)
if alertCenter.activeAlertCount > 0 {
Image(systemName: "bell.badge.fill")
.foregroundColor(.orange)
}
}
Divider()
if let cpu = monitor.cpuInfo { if let cpu = monitor.cpuInfo {
HStack { HStack {
Image(systemName: "cpu") Image(systemName: "cpu")
@@ -58,6 +103,46 @@ struct MenuBarView: View {
} }
} }
if let power = monitor.powerInfo, power.hasBattery {
HStack {
Image(systemName: power.isCharging ? "battery.100.bolt" : "battery.100")
.frame(width: 16)
Text("Battery:")
Spacer()
Text("\(power.currentCharge)%")
.monospacedDigit()
if let toEmpty = power.timeToEmpty {
Text("(\(BatteryMath.formatMinutes(toEmpty)))")
.foregroundColor(.secondary)
}
}
}
if !topCPU.isEmpty {
Divider()
Text("Top CPU Consumers")
.font(.caption)
.foregroundColor(.secondary)
ForEach(topCPU) { process in
HStack(spacing: 6) {
Text(process.name)
.lineLimit(1)
.truncationMode(.middle)
Spacer()
Text(String(format: "%.0f%%", process.cpuUsage))
.monospacedDigit()
.foregroundColor(.secondary)
Button {
_ = monitor.terminateProcess(process.pid, expectedStartTime: process.startTime)
} label: {
Image(systemName: "xmark.circle")
}
.buttonStyle(.borderless)
.help("Quit \(process.name)")
}
}
}
Divider() Divider()
Button("Open TopManager") { Button("Open TopManager") {
@@ -73,7 +158,16 @@ struct MenuBarView: View {
.keyboardShortcut("q") .keyboardShortcut("q")
} }
.padding(12) .padding(12)
.frame(width: 220) .frame(width: 240)
}
private var healthColor: Color {
switch alertCenter.healthScore {
case 85...: return .green
case 70..<85: return .mint
case 50..<70: return .orange
default: return .red
}
} }
} }

Fájl megtekintése

@@ -1,28 +1,26 @@
import SwiftUI import SwiftUI
import Charts import Charts
/// Sub-tab shell for the Performance tab. The selection is left to SwiftUI on
/// purpose: an explicit `@State` binding would re-evaluate this body — and so
/// leak its `.tabItem` labels — on every tab switch. See `ContentView`.
struct PerformanceView: View { struct PerformanceView: View {
@State private var selectedTab = 0
var body: some View { var body: some View {
TabView(selection: $selectedTab) { TabView {
CPUTabView() CPUTabView()
.tabItem { .tabItem {
Label("CPU", systemImage: "cpu") Label("CPU", systemImage: "cpu")
} }
.tag(0)
MemoryTabView() MemoryTabView()
.tabItem { .tabItem {
Label("Memory", systemImage: "memorychip") Label("Memory", systemImage: "memorychip")
} }
.tag(1)
NetworkTabView() NetworkTabView()
.tabItem { .tabItem {
Label("Network", systemImage: "network") Label("Network", systemImage: "network")
} }
.tag(2)
} }
} }
} }
@@ -31,6 +29,7 @@ struct PerformanceView: View {
struct CPUTabView: View { struct CPUTabView: View {
@EnvironmentObject var monitor: SystemMonitor @EnvironmentObject var monitor: SystemMonitor
@State private var range: HistoryRange = .live
private let coreColumns = [ private let coreColumns = [
GridItem(.flexible()), GridItem(.flexible()),
@@ -48,6 +47,7 @@ struct CPUTabView: View {
HStack { HStack {
Text("Total CPU Usage") Text("Total CPU Usage")
.font(.headline) .font(.headline)
HistoryRangePicker(range: $range)
Spacer() Spacer()
if let cpu = monitor.cpuInfo { if let cpu = monitor.cpuInfo {
Text(String(format: "%.1f%%", cpu.globalUsage)) Text(String(format: "%.1f%%", cpu.globalUsage))
@@ -57,8 +57,13 @@ struct CPUTabView: View {
} }
} }
CPULineChart(history: monitor.cpuHistory) if range == .live {
.frame(height: 120) CPULineChart(history: monitor.cpuHistory)
.frame(height: 120)
} else {
MetricTrendChart(samples: MetricsStore.shared.downsampled(for: range), metric: .cpu)
.frame(height: 120)
}
// Legend // Legend
HStack(spacing: 20) { HStack(spacing: 20) {
@@ -201,6 +206,7 @@ struct CoreLineChartView: View {
struct MemoryTabView: View { struct MemoryTabView: View {
@EnvironmentObject var monitor: SystemMonitor @EnvironmentObject var monitor: SystemMonitor
@State private var range: HistoryRange = .live
var body: some View { var body: some View {
ScrollView { ScrollView {
@@ -230,6 +236,7 @@ struct MemoryTabView: View {
HStack { HStack {
Text("Memory Usage Over Time") Text("Memory Usage Over Time")
.font(.headline) .font(.headline)
HistoryRangePicker(range: $range)
Spacer() Spacer()
if let mem = monitor.memoryInfo { if let mem = monitor.memoryInfo {
Text(String(format: "%.1f%%", mem.usagePercentage)) Text(String(format: "%.1f%%", mem.usagePercentage))
@@ -239,8 +246,13 @@ struct MemoryTabView: View {
} }
} }
MemoryLineChart(history: monitor.memoryHistory) if range == .live {
.frame(height: 150) MemoryLineChart(history: monitor.memoryHistory)
.frame(height: 150)
} else {
MetricTrendChart(samples: MetricsStore.shared.downsampled(for: range), metric: .memory)
.frame(height: 150)
}
if let mem = monitor.memoryInfo { if let mem = monitor.memoryInfo {
HStack { HStack {
@@ -394,6 +406,7 @@ struct MemoryStatItem: View {
struct NetworkTabView: View { struct NetworkTabView: View {
@EnvironmentObject var monitor: SystemMonitor @EnvironmentObject var monitor: SystemMonitor
@State private var range: HistoryRange = .live
var body: some View { var body: some View {
ScrollView { ScrollView {
@@ -405,6 +418,7 @@ struct NetworkTabView: View {
Label("Download", systemImage: "arrow.down.circle") Label("Download", systemImage: "arrow.down.circle")
.font(.headline) .font(.headline)
.foregroundColor(.blue) .foregroundColor(.blue)
HistoryRangePicker(range: $range)
Spacer() Spacer()
if let net = monitor.networkInfo { if let net = monitor.networkInfo {
Text(formatBytesPerSecond(net.totalDownloadRate)) Text(formatBytesPerSecond(net.totalDownloadRate))
@@ -414,8 +428,13 @@ struct NetworkTabView: View {
} }
} }
DownloadLineChart(history: monitor.networkHistory) if range == .live {
.frame(height: 150) DownloadLineChart(history: monitor.networkHistory)
.frame(height: 150)
} else {
MetricTrendChart(samples: MetricsStore.shared.downsampled(for: range), metric: .download)
.frame(height: 150)
}
} }
.padding(.vertical, 8) .padding(.vertical, 8)
} }
@@ -436,8 +455,13 @@ struct NetworkTabView: View {
} }
} }
UploadLineChart(history: monitor.networkHistory) if range == .live {
.frame(height: 150) UploadLineChart(history: monitor.networkHistory)
.frame(height: 150)
} else {
MetricTrendChart(samples: MetricsStore.shared.downsampled(for: range), metric: .upload)
.frame(height: 150)
}
} }
.padding(.vertical, 8) .padding(.vertical, 8)
} }

Fájl megtekintése

@@ -0,0 +1,105 @@
import SwiftUI
import Charts
/// A range selector segmented control used above the live/trend charts.
struct HistoryRangePicker: View {
@Binding var range: HistoryRange
var body: some View {
Picker("", selection: $range) {
ForEach(HistoryRange.allCases) { r in
Text(r.rawValue).tag(r)
}
}
.pickerStyle(.segmented)
.labelsHidden()
.fixedSize()
}
}
/// Store-backed trend chart. Plots one metric of `MetricsSample` over a chosen
/// window. Used for the non-live ranges (5m / 30m / 1h / 24h).
struct MetricTrendChart: View {
enum Metric { case cpu, memory, download, upload }
let samples: [MetricsSample]
let metric: Metric
private var color: Color {
switch metric {
case .cpu: return .gray
case .memory: return .purple
case .download: return .blue
case .upload: return .green
}
}
private func value(_ s: MetricsSample) -> Double {
switch metric {
case .cpu: return s.cpu
case .memory: return s.memPercent
case .download: return s.netDown
case .upload: return s.netUp
}
}
private var isPercent: Bool {
metric == .cpu || metric == .memory
}
private var yDomain: ClosedRange<Double> {
if isPercent { return -5...105 }
let maxV = max(samples.map(value).max() ?? 0, 1024)
return 0...(maxV * 1.1)
}
var body: some View {
Group {
if samples.isEmpty {
VStack {
Spacer()
Text("Collecting history…")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
}
.frame(maxWidth: .infinity)
} else {
Chart {
ForEach(samples) { point in
LineMark(
x: .value("Time", point.t),
y: .value("Value", value(point))
)
.foregroundStyle(color)
.lineStyle(StrokeStyle(lineWidth: 1.5))
AreaMark(
x: .value("Time", point.t),
y: .value("Value", value(point))
)
.foregroundStyle(color.opacity(0.12))
}
}
.chartYScale(domain: yDomain)
.chartYAxis {
AxisMarks(position: .leading) { value in
AxisGridLine()
AxisValueLabel {
if let v = value.as(Double.self) {
Text(isPercent ? "\(Int(v))%" : formatBytesPerSecond(v))
.font(.caption2)
}
}
}
}
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 4)) { _ in
AxisGridLine()
}
}
.drawingGroup()
}
}
}
}

Fájl megtekintése

@@ -6,6 +6,9 @@ struct PowerStorageView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(spacing: 20) { VStack(spacing: 20) {
// System Health score + diagnosis
HealthCardView()
// System Status and GPU side by side // System Status and GPU side by side
HStack(alignment: .top, spacing: 20) { HStack(alignment: .top, spacing: 20) {
// System Status (left column) // System Status (left column)
@@ -141,6 +144,11 @@ struct PowerStorageView: View {
} }
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
// Battery & Power
if let power = monitor.powerInfo {
BatteryStatusView(power: power)
}
// Storage // Storage
GroupBox { GroupBox {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@@ -184,6 +192,109 @@ struct PowerStorageView: View {
} }
} }
struct BatteryStatusView: View {
let power: PowerInfo
var body: some View {
GroupBox {
VStack(alignment: .leading, spacing: 10) {
HStack {
Label("Battery & Power", systemImage: batteryIcon)
.font(.headline)
Spacer()
HStack(spacing: 4) {
Image(systemName: power.isPluggedIn ? "powerplug.fill" : "battery.100")
.foregroundColor(.secondary)
Text(power.powerSourceLabel)
.foregroundColor(.secondary)
}
}
if power.hasBattery {
HStack {
Text("Charge:")
Spacer()
if power.isCharging {
Image(systemName: "bolt.fill").foregroundColor(.yellow)
}
Text("\(power.currentCharge)%").monospacedDigit()
}
ProgressView(value: Double(power.currentCharge), total: 100)
.tint(chargeColor)
row("Condition") {
Text(power.condition.rawValue)
.foregroundColor(conditionColor)
}
if let health = power.healthPercent {
row("Health (max/design)") {
Text(String(format: "%.0f%%", health)).monospacedDigit()
}
}
if let cycles = power.cycleCount {
row("Cycle Count") { Text("\(cycles)").monospacedDigit() }
}
if let temp = power.temperature {
row("Temperature") { Text(String(format: "%.1f °C", temp)).monospacedDigit() }
}
if let watts = power.powerWatts {
row(power.isCharging ? "Charge Power" : "Power Draw") {
Text(String(format: "%.1f W", abs(watts))).monospacedDigit()
}
}
if let toEmpty = power.timeToEmpty {
row("Time Remaining") { Text(BatteryMath.formatMinutes(toEmpty)).monospacedDigit() }
}
if let toFull = power.timeToFull {
row("Time to Full") { Text(BatteryMath.formatMinutes(toFull)).monospacedDigit() }
}
if let adapter = power.adapterWatts, adapter > 0 {
row("Power Adapter") { Text("\(adapter) W").monospacedDigit() }
}
} else {
Text("No battery detected — running on AC power.")
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
}
private func row<Content: View>(_ label: String, @ViewBuilder value: () -> Content) -> some View {
HStack {
Text(label + ":")
Spacer()
value()
}
}
private var batteryIcon: String {
if !power.hasBattery { return "powerplug" }
if power.isCharging { return "battery.100.bolt" }
switch power.currentCharge {
case ..<15: return "battery.25"
case ..<50: return "battery.50"
case ..<85: return "battery.75"
default: return "battery.100"
}
}
private var chargeColor: Color {
if power.currentCharge < 15 { return .red }
if power.currentCharge < 30 { return .orange }
return .green
}
private var conditionColor: Color {
switch power.condition {
case .normal: return .green
case .serviceRecommended: return .orange
case .unknown: return .secondary
}
}
}
struct ThermalStateView: View { struct ThermalStateView: View {
let state: ProcessInfo.ThermalState let state: ProcessInfo.ThermalState

Fájl megtekintése

@@ -1,63 +1,138 @@
import SwiftUI import SwiftUI
import AppKit
struct ProcessDetailView: View { /// Deep-dive inspector for a single process, presented as a sheet. Shows live
let process: ProcessItem /// stats (looked up from the monitor by PID) plus on-demand details that are too
/// expensive to compute for every row every cycle: full path, launch arguments,
/// and open-file-descriptor count.
struct ProcessInspectorSheet: View {
let initialProcess: ProcessItem
@EnvironmentObject var monitor: SystemMonitor
@Environment(\.dismiss) private var dismiss
@State private var arguments: [String] = []
@State private var openFiles: Int?
@State private var loadedDetails = false
/// Prefer the latest live snapshot for this PID; fall back to the one we opened with.
private var process: ProcessItem {
monitor.processes.first { $0.pid == initialProcess.pid } ?? initialProcess
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
// Header header
HStack(spacing: 12) {
if let icon = process.icon {
Image(nsImage: icon)
.resizable()
.frame(width: 48, height: 48)
} else {
Image(systemName: "app.dashed")
.font(.system(size: 48))
}
VStack(alignment: .leading) { Divider()
Text(process.name)
.font(.title2)
.fontWeight(.semibold)
Text("PID: \(process.pid)") ScrollView {
.foregroundColor(.secondary) VStack(alignment: .leading, spacing: 16) {
} liveStats
pathSection
Spacer() detailsSection
VStack(alignment: .trailing) {
HStack {
Image(systemName: process.state.symbol)
Text(process.state.rawValue)
}
.foregroundColor(stateColor(process.state))
} }
} }
Divider() Divider()
footer
// Details grid }
LazyVGrid(columns: [ .padding(20)
GridItem(.flexible()), .frame(width: 460, height: 560)
GridItem(.flexible()) .task(id: initialProcess.pid) {
], alignment: .leading, spacing: 12) { let pid = initialProcess.pid
DetailRow(label: "CPU Usage", value: String(format: "%.1f%%", process.cpuUsage)) let result = await Task.detached(priority: .userInitiated) {
DetailRow(label: "Memory", value: formatBytes(process.memoryUsage)) (args: fetchProcessArguments(pid: pid), files: fetchOpenFileCount(pid: pid))
DetailRow(label: "Threads", value: "\(process.threadCount)") }.value
DetailRow(label: "User", value: process.user) arguments = result.args
DetailRow(label: "Parent PID", value: "\(process.parentPid)") openFiles = result.files
loadedDetails = true
if let startTime = process.startTime { }
DetailRow(label: "Started", value: formatDate(startTime)) }
}
} private var header: some View {
HStack(spacing: 12) {
Spacer() if let icon = process.icon {
Image(nsImage: icon).resizable().frame(width: 44, height: 44)
} else {
Image(systemName: "app.dashed").font(.system(size: 40)).foregroundColor(.secondary)
}
VStack(alignment: .leading, spacing: 2) {
Text(process.name).font(.title2).fontWeight(.semibold).lineLimit(1)
Text("PID \(process.pid) • \(process.user)").foregroundColor(.secondary)
}
Spacer()
HStack(spacing: 4) {
Image(systemName: process.state.symbol)
Text(process.state.rawValue)
}
.foregroundColor(stateColor(process.state))
}
}
private var liveStats: some View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())],
alignment: .leading, spacing: 12) {
DetailRow(label: "CPU (core)", value: String(format: "%.1f%%", process.cpuUsage))
DetailRow(label: "CPU (total)", value: String(format: "%.2f%%", process.cpuUsageTotal))
DetailRow(label: "Energy", value: String(format: "%.1f", process.energyImpact))
DetailRow(label: "Memory", value: formatBytes(process.memoryUsage))
DetailRow(label: "Threads", value: "\(process.threadCount)")
DetailRow(label: "Open files", value: openFiles.map(String.init) ?? "…")
DetailRow(label: "Disk read/s", value: process.diskReadRate > 0 ? formatBytesPerSecond(process.diskReadRate) : "—")
DetailRow(label: "Disk write/s", value: process.diskWriteRate > 0 ? formatBytesPerSecond(process.diskWriteRate) : "—")
DetailRow(label: "Disk total", value: formatBytes(process.diskReadBytes + process.diskWriteBytes))
DetailRow(label: "Parent PID", value: "\(process.parentPid)")
if let start = process.startTime {
DetailRow(label: "Started", value: formatDate(start))
}
}
}
@ViewBuilder private var pathSection: some View {
if let path = process.executablePath {
VStack(alignment: .leading, spacing: 4) {
Text("Path").font(.caption).foregroundColor(.secondary)
Text(path)
.font(.system(.caption, design: .monospaced))
.textSelection(.enabled)
.lineLimit(3)
.truncationMode(.middle)
}
}
}
@ViewBuilder private var detailsSection: some View {
VStack(alignment: .leading, spacing: 4) {
Text("Launch arguments").font(.caption).foregroundColor(.secondary)
if !loadedDetails {
Text("Loading…").font(.caption).foregroundColor(.secondary)
} else if arguments.isEmpty {
Text("Unavailable (process may be owned by another user)")
.font(.caption).foregroundColor(.secondary)
} else {
Text(arguments.joined(separator: " "))
.font(.system(.caption, design: .monospaced))
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
private var footer: some View {
HStack {
if let path = process.executablePath {
Button {
NSWorkspace.shared.selectFile(path, inFileViewerRootedAtPath: "")
} label: { Label("Reveal in Finder", systemImage: "folder") }
Button {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(path, forType: .string)
} label: { Label("Copy Path", systemImage: "doc.on.doc") }
}
Spacer()
Button("Close") { dismiss() }.keyboardShortcut(.defaultAction)
} }
.padding()
.frame(minWidth: 300)
} }
private func stateColor(_ state: ProcessState) -> Color { private func stateColor(_ state: ProcessState) -> Color {
@@ -95,17 +170,14 @@ struct DetailRow: View {
} }
#Preview { #Preview {
ProcessDetailView(process: ProcessItem( ProcessInspectorSheet(initialProcess: ProcessItem(
pid: 1234, pid: 1234, name: "Safari", user: "ariel",
name: "Safari", cpuUsage: 12.5, cpuUsageTotal: 1.56,
user: "ariel", memoryUsage: 512 * 1024 * 1024, threadCount: 42,
cpuUsage: 12.5, state: .running, icon: nil, parentPid: 1, startTime: Date(),
cpuUsageTotal: 1.56, diskReadRate: 2048, diskWriteRate: 1024,
memoryUsage: 512 * 1024 * 1024, diskReadBytes: 900_000, diskWriteBytes: 400_000,
threadCount: 42, energyImpact: 14.2, executablePath: "/Applications/Safari.app/Contents/MacOS/Safari"
state: .running,
icon: nil,
parentPid: 1,
startTime: Date()
)) ))
.environmentObject(SystemMonitor.shared)
} }

Fájl megtekintése

@@ -1,7 +1,7 @@
import SwiftUI import SwiftUI
enum ProcessSortColumn: String { enum ProcessSortColumn: String {
case name, pid, cpu, cpuTotal, memory, threads, user, state case name, pid, cpu, cpuTotal, memory, threads, user, state, energy, disk
} }
struct ProcessView: View { struct ProcessView: View {
@@ -30,6 +30,9 @@ struct ProcessView: View {
@State private var errorTitle = "" @State private var errorTitle = ""
@State private var errorMessage = "" @State private var errorMessage = ""
// Deep-dive inspector
@State private var inspectorProcess: ProcessItem?
// Protected system processes that cannot be force-killed via UI // Protected system processes that cannot be force-killed via UI
private static let protectedProcesses = ["kernel_task", "launchd", "WindowServer", "loginwindow"] private static let protectedProcesses = ["kernel_task", "launchd", "WindowServer", "loginwindow"]
@@ -63,6 +66,10 @@ struct ProcessView: View {
comparison = lhs.user.localizedCaseInsensitiveCompare(rhs.user) comparison = lhs.user.localizedCaseInsensitiveCompare(rhs.user)
case .state: case .state:
comparison = lhs.state.rawValue.compare(rhs.state.rawValue) comparison = lhs.state.rawValue.compare(rhs.state.rawValue)
case .energy:
comparison = lhs.energyImpact < rhs.energyImpact ? .orderedAscending : (lhs.energyImpact > rhs.energyImpact ? .orderedDescending : .orderedSame)
case .disk:
comparison = lhs.diskTotalRate < rhs.diskTotalRate ? .orderedAscending : (lhs.diskTotalRate > rhs.diskTotalRate ? .orderedDescending : .orderedSame)
} }
// Primary sort // Primary sort
@@ -89,23 +96,22 @@ struct ProcessView: View {
// Process table // Process table
Table(displayedProcesses, selection: $selectedProcess, sortOrder: $sortOrder) { Table(displayedProcesses, selection: $selectedProcess, sortOrder: $sortOrder) {
TableColumn("") { process in TableColumn("Name", value: \.name) { process in
if let icon = process.icon { HStack(spacing: 6) {
Image(nsImage: icon) if let icon = process.icon {
.resizable() Image(nsImage: icon)
.frame(width: 16, height: 16) .resizable()
} else { .frame(width: 16, height: 16)
Image(systemName: "app.dashed") } else {
.frame(width: 16, height: 16) Image(systemName: "app.dashed")
.frame(width: 16, height: 16)
.foregroundColor(.secondary)
}
Text(process.name)
.lineLimit(1)
} }
} }
.width(24) .width(min: 170, ideal: 220)
TableColumn("Name", value: \.name) { process in
Text(process.name)
.lineLimit(1)
}
.width(min: 150, ideal: 200)
TableColumn("PID", value: \.pid) { process in TableColumn("PID", value: \.pid) { process in
Text("\(process.pid)") Text("\(process.pid)")
@@ -127,12 +133,29 @@ struct ProcessView: View {
} }
.width(70) .width(70)
TableColumn("Energy", value: \.energyImpact) { process in
Text(String(format: "%.1f", process.energyImpact))
.monospacedDigit()
.foregroundColor(energyColor(process.energyImpact))
}
.width(60)
TableColumn("Memory", value: \.memoryUsage) { process in TableColumn("Memory", value: \.memoryUsage) { process in
Text(formatBytes(process.memoryUsage)) Text(formatBytes(process.memoryUsage))
.monospacedDigit() .monospacedDigit()
} }
.width(80) .width(80)
TableColumn("Disk I/O", value: \.diskTotalRate) { process in
if process.diskTotalRate > 0 {
Text(formatBytesPerSecond(process.diskTotalRate))
.monospacedDigit()
} else {
Text("—").foregroundColor(.secondary)
}
}
.width(90)
TableColumn("Threads", value: \.threadCount) { process in TableColumn("Threads", value: \.threadCount) { process in
Text("\(process.threadCount)") Text("\(process.threadCount)")
.monospacedDigit() .monospacedDigit()
@@ -157,6 +180,10 @@ struct ProcessView: View {
.contextMenu(forSelectionType: ProcessItem.ID.self) { selection in .contextMenu(forSelectionType: ProcessItem.ID.self) { selection in
if let pid = selection.first, if let pid = selection.first,
let process = monitor.processes.first(where: { $0.pid == pid }) { let process = monitor.processes.first(where: { $0.pid == pid }) {
Button("Get Info (⌘I)") {
inspectorProcess = process
}
Divider()
Button("Terminate (⌫)") { Button("Terminate (⌫)") {
initiateTerminate() initiateTerminate()
} }
@@ -177,7 +204,11 @@ struct ProcessView: View {
} }
} }
} primaryAction: { selection in } primaryAction: { selection in
// Double-click action // Double-click opens the deep-dive inspector
if let pid = selection.first,
let process = monitor.processes.first(where: { $0.pid == pid }) {
inspectorProcess = process
}
} }
.onDeleteCommand { .onDeleteCommand {
initiateTerminate() initiateTerminate()
@@ -211,6 +242,10 @@ struct ProcessView: View {
sortColumn = .cpuTotal sortColumn = .cpuTotal
} else if keyPathString.contains("cpuUsage") { } else if keyPathString.contains("cpuUsage") {
sortColumn = .cpu sortColumn = .cpu
} else if keyPathString.contains("energyImpact") {
sortColumn = .energy
} else if keyPathString.contains("diskTotalRate") {
sortColumn = .disk
} else if keyPathString.contains("memoryUsage") { } else if keyPathString.contains("memoryUsage") {
sortColumn = .memory sortColumn = .memory
} else if keyPathString.contains("name") { } else if keyPathString.contains("name") {
@@ -229,7 +264,10 @@ struct ProcessView: View {
} }
.background( .background(
KeyboardShortcutHandler( KeyboardShortcutHandler(
onCommandBackspace: { initiateForceKill() } onCommandBackspace: { initiateForceKill() },
onCommandI: {
if let process = selectedProcessItem { inspectorProcess = process }
}
) )
) )
.sheet(isPresented: $showTerminateConfirm) { .sheet(isPresented: $showTerminateConfirm) {
@@ -268,6 +306,10 @@ struct ProcessView: View {
} message: { } message: {
Text(errorMessage) Text(errorMessage)
} }
.sheet(item: $inspectorProcess) { process in
ProcessInspectorSheet(initialProcess: process)
.environmentObject(monitor)
}
} }
private func initiateTerminate() { private func initiateTerminate() {
@@ -350,6 +392,17 @@ struct ProcessView: View {
return .primary return .primary
} }
private func energyColor(_ impact: Double) -> Color {
if impact > 50 {
return .red
} else if impact > 20 {
return .orange
} else if impact > 8 {
return .yellow
}
return .primary
}
private func stateColor(_ state: ProcessState) -> Color { private func stateColor(_ state: ProcessState) -> Color {
switch state { switch state {
case .running: return .green case .running: return .green
@@ -441,28 +494,39 @@ struct ConfirmationSheet: View {
struct KeyboardShortcutHandler: NSViewRepresentable { struct KeyboardShortcutHandler: NSViewRepresentable {
let onCommandBackspace: () -> Void let onCommandBackspace: () -> Void
var onCommandI: (() -> Void)? = nil
func makeNSView(context: Context) -> NSView { func makeNSView(context: Context) -> NSView {
let view = KeyCaptureView() let view = KeyCaptureView()
view.onCommandBackspace = onCommandBackspace view.onCommandBackspace = onCommandBackspace
view.onCommandI = onCommandI
return view return view
} }
func updateNSView(_ nsView: NSView, context: Context) { func updateNSView(_ nsView: NSView, context: Context) {
if let view = nsView as? KeyCaptureView { if let view = nsView as? KeyCaptureView {
view.onCommandBackspace = onCommandBackspace view.onCommandBackspace = onCommandBackspace
view.onCommandI = onCommandI
} }
} }
class KeyCaptureView: NSView { class KeyCaptureView: NSView {
var onCommandBackspace: (() -> Void)? var onCommandBackspace: (() -> Void)?
var onCommandI: (() -> Void)?
override func performKeyEquivalent(with event: NSEvent) -> Bool { override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Backspace = keyCode 51 guard event.modifierFlags.contains(.command) else {
if event.keyCode == 51 && event.modifierFlags.contains(.command) { return super.performKeyEquivalent(with: event)
}
// Backspace = keyCode 51, "i" = keyCode 34
if event.keyCode == 51 {
onCommandBackspace?() onCommandBackspace?()
return true return true
} }
if event.keyCode == 34 {
onCommandI?()
return true
}
return super.performKeyEquivalent(with: event) return super.performKeyEquivalent(with: event)
} }
} }

Fájl megtekintése

@@ -0,0 +1,97 @@
import SwiftUI
/// Settings window shell. Like `ContentView`, it deliberately observes nothing:
/// a body containing `.tabItem` leaks its `AnyView(Label(…))` on every
/// re-evaluation, and dragging a threshold slider re-publishes `AppSettings`
/// on every step. The tabs below own the observation instead.
struct SettingsView: View {
var body: some View {
TabView {
GeneralSettingsTab()
.tabItem { Label("General", systemImage: "gearshape") }
AlertSettingsTab()
.tabItem { Label("Alerts", systemImage: "bell") }
}
.frame(width: 440, height: 340)
.padding()
}
}
private struct GeneralSettingsTab: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
Form {
Section {
Toggle("Launch at login", isOn: $settings.launchAtLogin)
VStack(alignment: .leading) {
HStack {
Text("Refresh interval")
Spacer()
Text(String(format: "%.0f s", settings.refreshInterval))
.monospacedDigit()
.foregroundColor(.secondary)
}
Slider(value: $settings.refreshInterval, in: 1...30, step: 1)
}
}
Section("Menu Bar") {
Picker("Show in menu bar", selection: $settings.menuBarMetric) {
ForEach(MenuBarMetric.allCases) { metric in
Text(metric.rawValue).tag(metric)
}
}
}
}
.formStyle(.grouped)
}
}
private struct AlertSettingsTab: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
Form {
Section {
Toggle("Enable notifications", isOn: $settings.notificationsEnabled)
} footer: {
Text("TopManager posts a macOS notification when a threshold is first crossed.")
.font(.caption).foregroundColor(.secondary)
}
Section("Thresholds") {
sliderRow(title: "High CPU", value: $settings.cpuAlertThreshold,
range: 50...100, unit: "%")
sliderRow(title: "Disk almost full", value: $settings.diskAlertPercent,
range: 80...99, unit: "%")
VStack(alignment: .leading) {
HStack {
Text("Low battery")
Spacer()
Text("\(settings.lowBatteryPercent)%").monospacedDigit().foregroundColor(.secondary)
}
Slider(value: Binding(
get: { Double(settings.lowBatteryPercent) },
set: { settings.lowBatteryPercent = Int($0) }
), in: 5...50, step: 5)
}
}
}
.formStyle(.grouped)
}
private func sliderRow(title: String, value: Binding<Double>, range: ClosedRange<Double>, unit: String) -> some View {
VStack(alignment: .leading) {
HStack {
Text(title)
Spacer()
Text(String(format: "%.0f%@", value.wrappedValue, unit))
.monospacedDigit()
.foregroundColor(.secondary)
}
Slider(value: value, in: range, step: 1)
}
}
}

Fájl megtekintése

@@ -0,0 +1,58 @@
import XCTest
@testable import TopManager
final class BatteryMathTests: XCTestCase {
func testHealthPercent() {
// 4000 mAh current full-charge vs 5000 mAh design = 80%
XCTAssertEqual(BatteryMath.healthPercent(maxCapacity: 4000, designCapacity: 5000), 80, accuracy: 0.001)
}
func testHealthPercentZeroDesignIsSafe() {
XCTAssertEqual(BatteryMath.healthPercent(maxCapacity: 100, designCapacity: 0), 0)
}
func testConditionNormalWhenHealthy() {
XCTAssertEqual(BatteryMath.condition(healthPercent: 92, cycleCount: 120), .normal)
}
func testConditionServiceWhenHealthLow() {
XCTAssertEqual(BatteryMath.condition(healthPercent: 74, cycleCount: 120), .serviceRecommended)
}
func testConditionServiceWhenCyclesHigh() {
XCTAssertEqual(BatteryMath.condition(healthPercent: 95, cycleCount: 1200), .serviceRecommended)
}
func testConditionUnknownWhenNoHealth() {
XCTAssertEqual(BatteryMath.condition(healthPercent: nil, cycleCount: 100), .unknown)
}
func testWattsPositiveWhenCharging() {
// 12 V * 2000 mA = 24 W
XCTAssertEqual(BatteryMath.watts(volts: 12, milliAmps: 2000), 24, accuracy: 0.001)
}
func testWattsNegativeWhenDischarging() {
XCTAssertEqual(BatteryMath.watts(volts: 12, milliAmps: -1500), -18, accuracy: 0.001)
}
func testFormatMinutes() {
XCTAssertEqual(BatteryMath.formatMinutes(135), "2h 15m")
XCTAssertEqual(BatteryMath.formatMinutes(45), "45m")
XCTAssertEqual(BatteryMath.formatMinutes(0), "—")
}
func testPowerInfoDerivedValues() {
let p = PowerInfo(
hasBattery: true, currentCharge: 66, isCharging: false, isPluggedIn: false,
fullyCharged: false, cycleCount: 150, designCapacity: 5000, maxCapacity: 4600,
temperature: 30.5, voltage: 12.0, amperage: -1500, timeToEmpty: 200,
timeToFull: nil, adapterWatts: nil
)
XCTAssertEqual(p.healthPercent ?? 0, 92, accuracy: 0.001)
XCTAssertEqual(p.condition, .normal)
XCTAssertEqual(p.powerWatts ?? 0, -18, accuracy: 0.001)
XCTAssertEqual(p.powerSourceLabel, "Battery Power")
}
}

Fájl megtekintése

@@ -0,0 +1,114 @@
import XCTest
@testable import TopManager
/// Baseline tests proving the test harness works and locking in the existing
/// formatting / model-math behavior so later refactors can't silently break it.
final class FormattingTests: XCTestCase {
// MARK: - Byte formatting
func testFormatBytesZero() {
// ByteCountFormatter renders 0 as "Zero KB"/"Zero bytes" depending on OS/locale.
let s = formatBytes(UInt64(0)).lowercased()
XCTAssertTrue(s.contains("zero") || s.contains("0"), "Unexpected zero rendering: \(s)")
}
func testFormatBytesGigabyteScale() {
// 2 GiB in binary style
let twoGiB = UInt64(2) * 1024 * 1024 * 1024
let s = formatBytes(twoGiB)
XCTAssertTrue(s.contains("GB"), "Expected GB unit, got \(s)")
XCTAssertTrue(s.contains("2"), "Expected value 2, got \(s)")
}
func testFormatBytesNegativeDoubleClampsToZero() {
// Double overload clamps negatives to 0 rather than trapping on UInt64(negative)
let s = formatBytes(Double(-500)).lowercased()
XCTAssertTrue(s.contains("zero") || s.contains("0"), "Unexpected clamp rendering: \(s)")
}
func testFormatBytesPerSecondBelowOne() {
XCTAssertEqual(formatBytesPerSecond(0.4), "0 B/s")
}
func testFormatBytesPerSecondSuffix() {
let s = formatBytesPerSecond(5 * 1024 * 1024) // 5 MiB/s
XCTAssertTrue(s.hasSuffix("/s"), "Expected /s suffix, got \(s)")
XCTAssertTrue(s.contains("MB"), "Expected MB unit, got \(s)")
}
// MARK: - Percentage
func testFormatPercentageDefaultDecimals() {
XCTAssertEqual(formatPercentage(12.345), "12.3%")
}
func testFormatPercentageCustomDecimals() {
XCTAssertEqual(formatPercentage(12.345, decimals: 2), "12.35%")
}
// MARK: - Uptime
func testFormatUptimeMinutesOnly() {
XCTAssertEqual(formatUptime(45 * 60), "45m")
}
func testFormatUptimeHoursAndMinutes() {
XCTAssertEqual(formatUptime(3 * 3600 + 12 * 60), "3h 12m")
}
func testFormatUptimeDays() {
XCTAssertEqual(formatUptime(2 * 86400 + 5 * 3600 + 9 * 60), "2d 5h 9m")
}
// MARK: - Model math
func testMemoryUsagePercentage() {
let mem = MemoryInfo(
totalMemory: 16_000_000_000,
usedMemory: 8_000_000_000,
freeMemory: 8_000_000_000,
activeMemory: 4_000_000_000,
inactiveMemory: 2_000_000_000,
wiredMemory: 2_000_000_000,
compressedMemory: 1_000_000_000,
cachedMemory: 1_000_000_000,
swapUsed: 0,
swapTotal: 0,
memoryPressure: .nominal
)
XCTAssertEqual(mem.usagePercentage, 50.0, accuracy: 0.001)
XCTAssertEqual(mem.appMemory, 6_000_000_000)
}
func testMemoryUsagePercentageZeroTotalIsSafe() {
let mem = MemoryInfo(
totalMemory: 0, usedMemory: 0, freeMemory: 0, activeMemory: 0,
inactiveMemory: 0, wiredMemory: 0, compressedMemory: 0, cachedMemory: 0,
swapUsed: 0, swapTotal: 0, memoryPressure: .unknown
)
XCTAssertEqual(mem.usagePercentage, 0)
}
func testVolumeUsagePercentageAndUsedSpace() {
let vol = VolumeInfo(
name: "Macintosh HD",
mountPoint: "/",
totalSpace: 1000,
freeSpace: 250,
fileSystem: "APFS",
isRemovable: false,
isInternal: true
)
XCTAssertEqual(vol.usedSpace, 750)
XCTAssertEqual(vol.usagePercentage, 75.0, accuracy: 0.001)
}
func testVolumeFreeGreaterThanTotalClampsUsedToZero() {
let vol = VolumeInfo(
name: "weird", mountPoint: "/x", totalSpace: 100, freeSpace: 200,
fileSystem: "APFS", isRemovable: false, isInternal: true
)
XCTAssertEqual(vol.usedSpace, 0)
}
}

Fájl megtekintése

@@ -0,0 +1,110 @@
import XCTest
@testable import TopManager
final class HealthAlertTests: XCTestCase {
private func healthy() -> HealthInput {
HealthInput(cpuUsage: 10, memoryPressure: .nominal, swapUsedBytes: 0,
thermal: .nominal, diskUsedFraction: 0.4)
}
// MARK: - Health score
func testHealthyScoreIsExcellent() {
let s = HealthModel.score(healthy())
XCTAssertEqual(s, 100)
XCTAssertEqual(HealthModel.rating(s), "Excellent")
}
func testHighCPULowersScore() {
var input = healthy()
input = HealthInput(cpuUsage: 100, memoryPressure: .nominal, swapUsedBytes: 0,
thermal: .nominal, diskUsedFraction: 0.4)
XCTAssertLessThan(HealthModel.score(input), HealthModel.score(healthy()))
}
func testCriticalMemoryAndThermalCompound() {
let input = HealthInput(cpuUsage: 95, memoryPressure: .critical, swapUsedBytes: 3_000_000_000,
thermal: .critical, diskUsedFraction: 0.97)
let s = HealthModel.score(input)
XCTAssertLessThan(s, 50)
XCTAssertEqual(HealthModel.rating(s), "Poor")
XCTAssertGreaterThanOrEqual(s, 0)
}
func testScoreClampedToRange() {
// Absolute worst case must not underflow below 0.
let worst = HealthInput(cpuUsage: 100, memoryPressure: .critical, swapUsedBytes: .max,
thermal: .critical, diskUsedFraction: 1.0)
let s = HealthModel.score(worst)
XCTAssertTrue((0...100).contains(s))
}
func testDiagnosisEmptyWhenHealthy() {
XCTAssertTrue(HealthModel.diagnosis(healthy()).isEmpty)
}
func testDiagnosisReportsProblems() {
let input = HealthInput(cpuUsage: 95, memoryPressure: .critical, swapUsedBytes: 3_000_000_000,
thermal: .serious, diskUsedFraction: 0.93)
let d = HealthModel.diagnosis(input)
XCTAssertFalse(d.isEmpty)
XCTAssertTrue(d.contains { $0.localizedCaseInsensitiveContains("CPU") })
XCTAssertTrue(d.contains { $0.localizedCaseInsensitiveContains("memory") })
XCTAssertTrue(d.contains { $0.localizedCaseInsensitiveContains("disk") })
}
// MARK: - Alert sustained evaluator
func testSustainedFiresOnlyAfterRequiredCycles() {
var count = 0
var result = AlertEvaluator.sustained(breached: true, previousCount: count, requiredCycles: 3)
count = result.count
XCTAssertFalse(result.firing) // 1
result = AlertEvaluator.sustained(breached: true, previousCount: count, requiredCycles: 3)
count = result.count
XCTAssertFalse(result.firing) // 2
result = AlertEvaluator.sustained(breached: true, previousCount: count, requiredCycles: 3)
count = result.count
XCTAssertTrue(result.firing) // 3 → fires
}
func testSustainedResetsWhenConditionClears() {
var count = 5
let result = AlertEvaluator.sustained(breached: false, previousCount: count, requiredCycles: 3)
count = result.count
XCTAssertFalse(result.firing)
XCTAssertEqual(count, 0)
}
func testSeverityOrdering() {
XCTAssertTrue(AlertSeverity.info < AlertSeverity.warning)
XCTAssertTrue(AlertSeverity.warning < AlertSeverity.critical)
}
// MARK: - AlertCenter integration (de-dup + inbox)
@MainActor
func testAlertCenterDedupesAndClearsActiveKinds() {
let center = AlertCenter.shared
center.clearInbox()
let mem = MemoryInfo(totalMemory: 16, usedMemory: 15, freeMemory: 1, activeMemory: 8,
inactiveMemory: 2, wiredMemory: 3, compressedMemory: 2, cachedMemory: 0,
swapUsed: 0, swapTotal: 0, memoryPressure: .critical)
let before = center.alerts.count
// Two consecutive critical-pressure evaluations → only ONE alert (de-dup).
center.evaluate(cpuUsage: 5, memory: mem, disk: nil, thermal: .nominal, topProcess: nil, power: nil)
center.evaluate(cpuUsage: 5, memory: mem, disk: nil, thermal: .nominal, topProcess: nil, power: nil)
XCTAssertEqual(center.alerts.count, before + 1, "critical memory pressure should fire exactly once")
XCTAssertTrue(center.activeKinds.contains(.memoryPressure))
// Pressure recovers → active kind clears, no new alert added.
let ok = MemoryInfo(totalMemory: 16, usedMemory: 4, freeMemory: 12, activeMemory: 2,
inactiveMemory: 1, wiredMemory: 1, compressedMemory: 0, cachedMemory: 0,
swapUsed: 0, swapTotal: 0, memoryPressure: .nominal)
center.evaluate(cpuUsage: 5, memory: ok, disk: nil, thermal: .nominal, topProcess: nil, power: nil)
XCTAssertFalse(center.activeKinds.contains(.memoryPressure))
}
}

Fájl megtekintése

@@ -0,0 +1,106 @@
import XCTest
@testable import TopManager
final class MetricsStoreTests: XCTestCase {
private func sample(_ secondsAgo: TimeInterval, cpu: Double = 0, memUsed: UInt64 = 0,
memTotal: UInt64 = 100, down: Double = 0, up: Double = 0,
now: Date) -> MetricsSample {
MetricsSample(t: now.addingTimeInterval(-secondsAgo), cpu: cpu,
memUsed: memUsed, memTotal: memTotal, netDown: down, netUp: up)
}
// MARK: - trim
func testTrimDropsOldSamples() {
let now = Date()
let samples = [
sample(10_000, now: now), // older than 1h
sample(100, now: now), // within 1h
sample(50, now: now)
]
let trimmed = MetricsMath.trim(samples, now: now, maxAge: 3600, maxCount: 100)
XCTAssertEqual(trimmed.count, 2)
XCTAssertFalse(trimmed.contains { $0.t < now.addingTimeInterval(-3600) })
}
func testTrimCapsCountKeepingNewest() {
let now = Date()
// 10 samples, 0..9 seconds ago; keep newest 5
let samples = (0..<10).map { sample(Double($0), cpu: Double($0), now: now) }
let trimmed = MetricsMath.trim(samples, now: now, maxAge: 3600, maxCount: 5)
XCTAssertEqual(trimmed.count, 5)
// suffix keeps the last 5 of the input order (cpu 5..9)
XCTAssertEqual(trimmed.map { $0.cpu }, [5, 6, 7, 8, 9])
}
// MARK: - downsample
func testDownsampleZeroBucketIsPassthrough() {
let now = Date()
let samples = (0..<5).map { sample(Double($0), cpu: Double($0), now: now) }
XCTAssertEqual(MetricsMath.downsample(samples, bucket: 0).count, 5)
}
func testDownsampleAveragesWithinBucket() {
// Two samples 0s and 5s into the same 60s bucket → averaged to one point.
let epoch = Date(timeIntervalSince1970: 1_000_000) // aligned into bucket 16666
let s1 = MetricsSample(t: epoch, cpu: 20, memUsed: 40, memTotal: 100, netDown: 100, netUp: 10)
let s2 = MetricsSample(t: epoch.addingTimeInterval(5), cpu: 40, memUsed: 60, memTotal: 100, netDown: 300, netUp: 30)
let out = MetricsMath.downsample([s1, s2], bucket: 60)
XCTAssertEqual(out.count, 1)
XCTAssertEqual(out[0].cpu, 30, accuracy: 0.001)
XCTAssertEqual(out[0].memUsed, 50)
XCTAssertEqual(out[0].netDown, 200, accuracy: 0.001)
XCTAssertEqual(out[0].netUp, 20, accuracy: 0.001)
}
func testDownsampleReducesPointCountForLongRange() {
let base = Date(timeIntervalSince1970: 2_000_000)
// 600 samples one second apart → into 60s buckets ≈ 10 points
let samples = (0..<600).map {
MetricsSample(t: base.addingTimeInterval(Double($0)), cpu: 1, memUsed: 1, memTotal: 100, netDown: 0, netUp: 0)
}
let out = MetricsMath.downsample(samples, bucket: 60)
XCTAssertLessThanOrEqual(out.count, 11)
XCTAssertGreaterThanOrEqual(out.count, 10)
// output must be time-sorted
XCTAssertEqual(out, out.sorted { $0.t < $1.t })
}
// MARK: - persistence round-trip
func testStorePersistsAndReloads() {
let tmp = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("tm-test-\(UUID().uuidString).json")
defer { try? FileManager.default.removeItem(at: tmp) }
let store = MetricsStore(fileURL: tmp, maxAge: 3600, maxCount: 100, saveEvery: 1)
let now = Date()
store.record(MetricsSample(t: now, cpu: 42, memUsed: 5, memTotal: 10, netDown: 1, netUp: 2))
// saveEvery: 1 persists on the background io queue; wait for the file to appear.
let deadline = Date().addingTimeInterval(2)
while !FileManager.default.fileExists(atPath: tmp.path), Date() < deadline {
RunLoop.current.run(until: Date().addingTimeInterval(0.02))
}
XCTAssertTrue(FileManager.default.fileExists(atPath: tmp.path), "history file was not written")
let reloaded = MetricsStore(fileURL: tmp, maxAge: 3600, maxCount: 100, saveEvery: 1)
XCTAssertEqual(reloaded.count, 1)
XCTAssertEqual(reloaded.samples(since: now.addingTimeInterval(-1)).first?.cpu, 42)
}
// MARK: - range config sanity
func testHistoryRangeLiveHasNoWindow() {
XCTAssertNil(HistoryRange.live.seconds)
XCTAssertEqual(HistoryRange.live.bucket, 0)
}
func testHistoryRangeWindowsIncrease() {
let windows = HistoryRange.allCases.compactMap { $0.seconds }
XCTAssertEqual(windows, windows.sorted())
XCTAssertEqual(HistoryRange.h24.seconds, 86_400)
}
}

Fájl megtekintése

@@ -0,0 +1,95 @@
import XCTest
@testable import TopManager
final class ProcessDataTests: XCTestCase {
// MARK: - KERN_PROCARGS2 parsing
private func buildProcArgsBuffer(argc: Int32, path: String, args: [String], env: [String]) -> [UInt8] {
var bytes: [UInt8] = [
UInt8(argc & 0xff),
UInt8((argc >> 8) & 0xff),
UInt8((argc >> 16) & 0xff),
UInt8((argc >> 24) & 0xff)
]
bytes += Array(path.utf8) + [0]
bytes += [0, 0] // NUL padding before argv[0]
for a in args { bytes += Array(a.utf8) + [0] }
for e in env { bytes += Array(e.utf8) + [0] }
return bytes
}
func testProcArgsParseExtractsPathAndArgs() {
let buffer = buildProcArgsBuffer(argc: 2, path: "/bin/foo",
args: ["/bin/foo", "--flag"],
env: ["PATH=/usr/bin"])
let parsed = ProcArgs.parse(buffer)
XCTAssertEqual(parsed?.path, "/bin/foo")
XCTAssertEqual(parsed?.args, ["/bin/foo", "--flag"])
}
func testProcArgsParseStopsAtArgcAndIgnoresEnv() {
// argc=1 → only the first arg is returned, env is not leaked into args
let buffer = buildProcArgsBuffer(argc: 1, path: "/usr/bin/x",
args: ["/usr/bin/x"],
env: ["SECRET=shh"])
let parsed = ProcArgs.parse(buffer)
XCTAssertEqual(parsed?.args, ["/usr/bin/x"])
XCTAssertFalse(parsed?.args.contains { $0.contains("SECRET") } ?? true)
}
func testProcArgsParseZeroArgc() {
let buffer = buildProcArgsBuffer(argc: 0, path: "/bin/x", args: [], env: [])
let parsed = ProcArgs.parse(buffer)
XCTAssertEqual(parsed?.path, "/bin/x")
XCTAssertEqual(parsed?.args, [])
}
func testProcArgsParseTooShortReturnsNil() {
XCTAssertNil(ProcArgs.parse([1, 2, 3]))
}
// MARK: - Energy model
func testEnergyImpactMonotonicInCPU() {
XCTAssertGreaterThan(
EnergyModel.impact(cpuPercent: 50, idleWakeupsPerSec: 0),
EnergyModel.impact(cpuPercent: 10, idleWakeupsPerSec: 0)
)
}
func testEnergyImpactMonotonicInWakeups() {
XCTAssertGreaterThan(
EnergyModel.impact(cpuPercent: 10, idleWakeupsPerSec: 500),
EnergyModel.impact(cpuPercent: 10, idleWakeupsPerSec: 0)
)
}
func testEnergyImpactClampsNegativeInputsToZero() {
XCTAssertEqual(EnergyModel.impact(cpuPercent: -5, idleWakeupsPerSec: -100), 0, accuracy: 0.0001)
}
func testEnergyImpactEqualsCPUWhenNoWakeups() {
XCTAssertEqual(EnergyModel.impact(cpuPercent: 33, idleWakeupsPerSec: 0), 33, accuracy: 0.0001)
}
// MARK: - ProcessItem derived + equality
func testDiskTotalRateSumsReadAndWrite() {
let p = ProcessItem(pid: 1, name: "a", user: "u", cpuUsage: 0, cpuUsageTotal: 0,
memoryUsage: 0, threadCount: 0, state: .running, icon: nil,
parentPid: 0, startTime: nil, diskReadRate: 100, diskWriteRate: 50)
XCTAssertEqual(p.diskTotalRate, 150)
}
func testEqualityDetectsEnergyChange() {
func make(energy: Double) -> ProcessItem {
ProcessItem(pid: 1, name: "a", user: "u", cpuUsage: 1, cpuUsageTotal: 1,
memoryUsage: 1, threadCount: 1, state: .running, icon: nil,
parentPid: 0, startTime: nil, energyImpact: energy)
}
// Table refresh relies on == seeing energy/disk changes.
XCTAssertNotEqual(make(energy: 5), make(energy: 9))
XCTAssertEqual(make(energy: 5), make(energy: 5))
}
}

Fájl megtekintése

@@ -0,0 +1,27 @@
import XCTest
@testable import TopManager
final class SettingsTests: XCTestCase {
func testClampIntervalBounds() {
XCTAssertEqual(AppSettings.clampInterval(0.2), 1)
XCTAssertEqual(AppSettings.clampInterval(100), 30)
XCTAssertEqual(AppSettings.clampInterval(5), 5)
XCTAssertEqual(AppSettings.clampInterval(1), 1)
XCTAssertEqual(AppSettings.clampInterval(30), 30)
}
func testMenuBarMetricRoundTrip() {
for metric in MenuBarMetric.allCases {
XCTAssertEqual(MenuBarMetric(rawValue: metric.rawValue), metric)
}
XCTAssertNil(MenuBarMetric(rawValue: "nonexistent"))
}
func testMenuBarMetricCoversKeyMetrics() {
XCTAssertTrue(MenuBarMetric.allCases.contains(.cpu))
XCTAssertTrue(MenuBarMetric.allCases.contains(.memory))
XCTAssertTrue(MenuBarMetric.allCases.contains(.health))
XCTAssertTrue(MenuBarMetric.allCases.contains(.download))
}
}

67
docs/TESTING.md Normal file
Fájl megtekintése

@@ -0,0 +1,67 @@
# TopManager — Testing Process
This document defines the repeatable test process used while evolving TopManager.
Every feature increment must pass all five gates below before it is considered done.
## Gate 1 — Compile
```bash
scripts/test.sh build
```
Fast feedback that the code compiles for `platform=macOS,arch=arm64`. First gate on every change.
## Gate 2 — Unit tests (automated, self-run)
```bash
scripts/test.sh test # or: xcodebuild ... test
```
Pure-logic is covered by XCTest in `TopManagerTests/` using `@testable import TopManager`.
The suite intentionally targets deterministic logic (no live system state):
- **Formatting / model math** — bytes, rates, percentages, uptime, usage %.
- **MetricsStore** — ring-buffer trim, persistence round-trip, downsampling per range.
- **Energy heuristic** — score is monotonic in CPU and wakeups, clamped to 0–100.
- **Alert engine** — threshold crossings fire/clear correctly, sustained-window logic,
de-duplication (no alert storms), severity ordering.
- **Health score** — 0–100, decreases with pressure/thermal/swap/disk load.
- **Battery math** — time-remaining and capacity-health formatting.
Tests must stay hermetic: no sleeping on real timers, no asserting on live CPU numbers.
## Gate 3 — Runtime smoke test (self-run on this machine)
Launch the built app, drive real load, confirm no crash and data populates.
```bash
# build a runnable .app
xcodebuild -project TopManager.xcodeproj -scheme TopManager -configuration Debug \
-destination 'platform=macOS,arch=arm64' build CODE_SIGNING_ALLOWED=NO
APP=$(find ~/Library/Developer/Xcode/DerivedData/TopManager-*/Build/Products/Debug -name TopManager.app | head -1)
open "$APP"
# generate CPU load in the background, then verify TopManager surfaces it
yes > /dev/null & LOAD=$!; sleep 8; kill $LOAD
```
Verify: process list shows the load generator near the top, charts move, no crash in
`Console`/stderr, menu bar updates, alerts fire when thresholds are crossed.
## Gate 4 — Ground-truth cross-checks
TopManager's numbers are validated against OS tools so we don't ship wrong data:
| TopManager metric | Ground truth command |
|------------------------|---------------------------------------------|
| Per-process CPU/mem | `ps -Ao pid,pcpu,pmem,comm -r | head` |
| System memory | `vm_stat`, `sysctl vm.swapusage` |
| Disk volumes / free | `df -h` |
| Battery / power | `pmset -g batt`, `ioreg -rn AppleSmartBattery` |
| Thermal state | `pmset -g therm` |
| Network throughput | `netstat -ib` |
Small deltas are expected (sampling windows differ); order-of-magnitude or ranking
mismatches are bugs.
## Gate 5 — Regression: original features intact
Manually confirm each shipped feature still works: Processes (sort/search/kill/suspend/
resume), Apps (activate/hide/quit/force-quit/copy bundle id), Performance (CPU/Mem/Net
charts), Power & Storage (system/GPU/storage/network), and the menu-bar readout.
## Loop
```
implement slice → Gate 1 → Gate 2 → Gate 3 → Gate 4 → Gate 5 → checkpoint commit
```
Repeat per increment until the value rubric in `docs/VALUE_PLAN.md` is met.

38
docs/VALUE_PLAN.md Normal file
Fájl megtekintése

@@ -0,0 +1,38 @@
# TopManager — 10× Value Plan
## Thesis
TopManager today is a polished, **read-only Activity Monitor clone**: it *shows* numbers.
Its value ceiling is "I can see what's using my Mac and kill a process."
To make it ~10× more valuable we move it from **viewer → system-intelligence & control
center**: it watches the system for you, remembers its history, explains problems, exposes
data Activity Monitor hides, and lets you act — including always-on from the menu bar.
All existing features are preserved (several are improved).
## Value rubric (capability scoring)
| Dimension | Before | After (target) |
|---|---|---|
| Per-process depth | CPU, mem, threads, state | + disk I/O rate, energy impact, args, open files, full path, tree |
| Time memory | 60 in-RAM points, lost on quit | persistent history, minutes → 24h, survives restart |
| Proactivity | none | threshold alerts + native notifications + health score + diagnosis |
| Energy / battery | **absent** | battery health (cycles/condition/capacity) + per-process energy |
| Always-on surface | bare "12%" | rich menu-bar HUD: CPU/RAM/net + top consumers + quick-kill |
| Customization | none (all hardcoded) | Settings: refresh rate, menu metric, thresholds, launch-at-login |
## Increments
0. **Test harness** — XCTest target, `@testable import`, 5-gate process. ✅
1. **Persistent history** — `MetricsStore` ring buffer on disk; 1m/5m/1h/24h charts.
2. **Deep per-process data** — disk I/O, energy impact, args, open files, path; surface the
(currently unused) `ProcessDetailView` as a real inspector; new sortable columns.
3. **Battery + energy** — IOKit `PowerMonitor`; Energy/Battery UI; menu-bar battery.
4. **Alerts + health** — `AlertEngine`, `UserNotifications`, in-app inbox, 0–100 health score,
"why is my Mac slow?" diagnosis.
5. **Settings + rich menu bar** — configurable refresh/metric/thresholds, launch-at-login
(`SMAppService`), menu-bar HUD with top consumers + quick actions.
6. **Verification** — full test run, runtime smoke, ground-truth cross-checks, sign, commit.
## Non-negotiables
- Never break a shipped feature (regression Gate 5).
- No new entitlements / no root / no privileged helper — must run & sign as-is.
- Every increment passes all five gates in `docs/TESTING.md` before moving on.

Fájl megtekintése

@@ -12,7 +12,7 @@ settings:
SWIFT_VERSION: "5.9" SWIFT_VERSION: "5.9"
CODE_SIGN_STYLE: Automatic CODE_SIGN_STYLE: Automatic
ENABLE_HARDENED_RUNTIME: YES ENABLE_HARDENED_RUNTIME: YES
DEVELOPMENT_TEAM: "" DEVELOPMENT_TEAM: "P3663392R2"
targets: targets:
TopManager: TopManager:
@@ -33,5 +33,34 @@ targets:
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
info: info:
path: TopManager/Info.plist path: TopManager/Info.plist
entitlements:
path: TopManager/TopManager.entitlements TopManagerTests:
type: bundle.unit-test
platform: macOS
sources:
- path: TopManagerTests
excludes:
- "**/.DS_Store"
dependencies:
- target: TopManager
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.topmanager.TopManagerTests
GENERATE_INFOPLIST_FILE: YES
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/TopManager.app/Contents/MacOS/TopManager"
BUNDLE_LOADER: "$(TEST_HOST)"
CODE_SIGN_STYLE: Automatic
schemes:
TopManager:
build:
targets:
TopManager: all
TopManagerTests: [test]
run:
config: Debug
test:
config: Debug
targets:
- TopManagerTests
gatherCoverageData: false

25
scripts/test.sh Executable file
Fájl megtekintése

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Fast build+test loop driver for TopManager.
# Usage:
# scripts/test.sh build # compile only (fast gate)
# scripts/test.sh test # compile + run unit tests
# scripts/test.sh # same as "test"
set -euo pipefail
cd "$(dirname "$0")/.."
ACTION="${1:-test}"
DEST='platform=macOS,arch=arm64'
COMMON=(-project TopManager.xcodeproj -scheme TopManager -configuration Debug -destination "$DEST" CODE_SIGNING_ALLOWED=NO)
case "$ACTION" in
build)
xcodebuild "${COMMON[@]}" build 2>&1 | tail -20
;;
test)
xcodebuild "${COMMON[@]}" test 2>&1 \
| grep -E "Test Suite|Executed|error:|warning:.*unused|BUILD|TEST (SUCCEEDED|FAILED)|failed \(" \
| tail -40
;;
*)
echo "unknown action: $ACTION" >&2; exit 2 ;;
esac