Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

## Checklist
- [ ] Builds without errors (`xcodegen generate && xcodebuild -scheme ChillMac build`)
- [ ] Unit tests pass (`xcodebuild -project ChillMac.xcodeproj -scheme ChillMac -destination 'platform=macOS' test`)
- [ ] Tested manually in the app
15 changes: 13 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ ChillMac/
BatteryInfo Battery info via IOKit/IOPowerSources (polls every 5s)
SMC/ - IOKit bridge (SMCConnection, SMCTypes, SMCKeys)
XPC/ - HelperConnection (client), HelperInstaller
Preview/ - PreviewSupport factories for SwiftUI canvas + tests (#if DEBUG)
FanControlHelper/
main.swift - Helper daemon entry point
HelperDelegate.swift - XPC listener + code signature validation
Expand Down Expand Up @@ -123,6 +124,16 @@ scripts/
- 420x640 main popover, 370x560 detail panels
- Footer with quit button, app name, and °F/°C toggle

## No Tests
## Testing

There is no test suite. Testing is done manually via the UI.
Unit tests live in `ChillMacTests` (Swift Testing). Run:

```bash
xcodegen generate && xcodebuild -project ChillMac.xcodeproj -scheme ChillMac -destination 'platform=macOS' test
```

- Use `@Test` / `#expect` / `@Suite` (Swift Testing only — no XCTest UI suite yet)
- Sample data for fixtures and canvas comes from `ChillMac/Preview/PreviewSupport.swift` — do not hit live SMC/XPC in unit tests or invent local sample data in views
- SwiftUI convention: add in-file `#Preview("…")` (plain macros, no traits) wired to `PreviewSupport` factories; keep `#Preview` under `#if DEBUG`
- Zero external test dependencies (no ViewInspector, no snapshot libs)
- Test plan: `TestPlans/Unit.xctestplan`
15 changes: 13 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ ChillMac/
BatteryInfo Battery info via IOKit/IOPowerSources (polls every 5s)
SMC/ - IOKit bridge (SMCConnection, SMCTypes, SMCKeys)
XPC/ - HelperConnection (client), HelperInstaller
Preview/ - PreviewSupport factories for SwiftUI canvas + tests (#if DEBUG)
FanControlHelper/
main.swift - Helper daemon entry point
HelperDelegate.swift - XPC listener + code signature validation
Expand Down Expand Up @@ -123,6 +124,16 @@ scripts/
- 420x640 main popover, 370x560 detail panels
- Footer with quit button, app name, and °F/°C toggle

## No Tests
## Testing

There is no test suite. Testing is done manually via the UI.
Unit tests live in `ChillMacTests` (Swift Testing). Run:

```bash
xcodegen generate && xcodebuild -project ChillMac.xcodeproj -scheme ChillMac -destination 'platform=macOS' test
```

- Use `@Test` / `#expect` / `@Suite` (Swift Testing only — no XCTest UI suite yet)
- Sample data for fixtures and canvas comes from `ChillMac/Preview/PreviewSupport.swift` — do not hit live SMC/XPC in unit tests or invent local sample data in views
- SwiftUI convention: add in-file `#Preview("…")` (plain macros, no traits) wired to `PreviewSupport` factories; keep `#Preview` under `#if DEBUG`
- Zero external test dependencies (no ViewInspector, no snapshot libs)
- Test plan: `TestPlans/Unit.xctestplan`
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Debug builds skip the helper's code signature check, so you don't need a Develop
1. Fork the repo and create a branch from `main`
2. Make your changes
3. Make sure it builds: `xcodegen generate && xcodebuild -scheme ChillMac build`
4. Test manually — there's no automated test suite
4. Run unit tests: `xcodebuild -project ChillMac.xcodeproj -scheme ChillMac -destination 'platform=macOS' test`
5. Open a PR

## Project Overview
Expand All @@ -40,7 +40,8 @@ Debug builds skip the helper's code signature check, so you don't need a Develop
- Match the existing code style (no linter configured, just stay consistent)
- SwiftUI views go in `ChillMac/Views/`
- New monitors/data models go in `ChillMac/Fan/`
- The app has zero external dependencies — let's keep it that way unless there's a strong reason
- Use `PreviewSupport` for sample data; add in-file `#Preview` (no live SMC/XPC in canvas)
- The app has zero external dependencies — let's keep it that way unless there's a strong reason (Swift Testing only; no third-party test libs)

## Architecture Notes

Expand Down
10 changes: 10 additions & 0 deletions ChillMac/App/DetailPanelController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,13 @@ struct DetailPanelCloseButton: View {
.buttonStyle(.plain)
}
}

#if DEBUG
#Preview("Detail Panel Close Button") {
return PreviewSupport.previewHost(theme: .dark, frame: .detail) {
DetailPanelCloseButton(action: {})
.padding()
}
}
#endif

11 changes: 11 additions & 0 deletions ChillMac/App/Theme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,14 @@ struct ThemedView<Content: View>: View {
.preferredColorScheme(settings.preferredColorScheme)
}
}

#if DEBUG
#Preview("ThemedView") {
return ThemedView(content:
Text("Sample")
.padding(20)
)
.frame(width: 200, height: 100)
}
#endif

25 changes: 25 additions & 0 deletions ChillMac/Fan/SystemInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ final class SystemInfo: ObservableObject {
refreshDynamic()
}

#if DEBUG
/// Canned sample for previews/tests — skips `fetchHardwareInfo()` and timers.
static var previewSample: SystemInfo {
SystemInfo(previewSample: ())
}

private init(previewSample: Void) {
machineModel = "MacBook Pro"
chipName = "Apple M3 Pro"
ramAmount = "36 GB"
macOSVersion = "macOS 15.0.0"
diskUsage = "256 GB"
uptime = "2d 5h 12m"
diskTotalBytes = 512_110_190_592
diskAvailableBytes = 275_146_342_400
diskCategories = [
DiskCategory(name: "Applications", bytes: 45_000_000_000, color: .systemRed),
DiskCategory(name: "Downloads", bytes: 12_000_000_000, color: .systemPink),
DiskCategory(name: "Documents", bytes: 28_000_000_000, color: .systemBlue),
DiskCategory(name: "Desktop", bytes: 4_000_000_000, color: .systemGreen),
DiskCategory(name: "Other", bytes: 148_000_000_000, color: .systemGray),
]
}
#endif

func startMonitoring() {
guard timer == nil else { return }
refreshDynamic()
Expand Down
179 changes: 179 additions & 0 deletions ChillMac/Preview/PreviewSupport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#if DEBUG
import SwiftUI

/// Shared sample data for SwiftUI `#Preview` and unit tests.
/// Factories construct and seed only — never start monitors, XPC, or network.
enum PreviewSupport {
// MARK: - Sample values

static let sampleFans: [FanInfo] = [
FanInfo(
id: 0,
name: "Left Fan",
currentRPM: 2800,
minRPM: 1200,
maxRPM: 6000,
targetRPM: 2800,
isManualMode: false
),
FanInfo(
id: 1,
name: "Right Fan",
currentRPM: 2650,
minRPM: 1200,
maxRPM: 6000,
targetRPM: 2650,
isManualMode: true
),
]

static let sampleSensors: [TemperatureSensor] = [
TemperatureSensor(id: "Tp09", label: "CPU Die", temperature: 58.5),
TemperatureSensor(id: "TG0P", label: "GPU", temperature: 52.0),
TemperatureSensor(id: "TH0x", label: "SSD", temperature: 41.2),
TemperatureSensor(id: "TB0T", label: "Battery", temperature: 33.8),
]

private static let sampleCpuHistory: [Double] = [
12, 18, 22, 35, 48, 42, 38, 55, 62, 58,
45, 40, 33, 28, 25, 30, 44, 50, 47, 36,
28, 22, 18, 15,
]

// MARK: - Factories

static var fanMonitor: FanMonitor {
let monitor = FanMonitor()
monitor.helperReady = true
monitor.fans = sampleFans
monitor.sensors = sampleSensors
monitor.peakTemperature = 58.5
monitor.peakTemperatureLabel = "CPU Die"
monitor.peakCpuTemperature = 58.5
monitor.peakGpuTemperature = 52.0
monitor.peakSsdTemperature = 41.2
return monitor
}

/// Fan monitor seeded like Performance Mode Max under load (matches expanded Fans card).
static var fanMonitorPerformanceActive: FanMonitor {
let monitor = fanMonitor
monitor.peakTemperature = 92.2
monitor.peakTemperatureLabel = "CPU Die"
monitor.peakCpuTemperature = 92.2
monitor.performanceCurvePercent = 100
return monitor
}

/// Posts the same notification StatusBarController sends when the popover opens,
/// so `@State appeared` fades content in during canvas previews.
static func triggerPopoverAppeared() {
NotificationCenter.default.post(name: .popoverDidShow, object: nil)
}

static var cpuInfo: CpuInfo {
let info = CpuInfo()
info.userPercent = 28
info.systemPercent = 12
info.idlePercent = 60
info.totalUsage = 40
info.history = sampleCpuHistory
info.userHistory = sampleCpuHistory.map { $0 * 0.7 }
info.systemHistory = sampleCpuHistory.map { $0 * 0.3 }
info.topProcesses = [
.init(name: "Xcode", cpuPercent: 42.5, icon: nil),
.init(name: "Safari", cpuPercent: 18.2, icon: nil),
.init(name: "Finder", cpuPercent: 4.1, icon: nil),
]
return info
}

static var memoryInfo: MemoryInfo {
let info = MemoryInfo()
let total = info.totalMemory
info.activeMemory = UInt64(Double(total) * 0.35)
info.wiredMemory = UInt64(Double(total) * 0.15)
info.compressedMemory = UInt64(Double(total) * 0.10)
info.availableMemory = total - info.activeMemory - info.wiredMemory - info.compressedMemory
info.pressurePercent = 60
info.swapUsed = 512 * 1_048_576
info.topProcesses = [
.init(name: "Xcode", memoryBytes: 2_147_483_648, icon: nil),
.init(name: "Safari", memoryBytes: 805_306_368, icon: nil),
.init(name: "Finder", memoryBytes: 268_435_456, icon: nil),
]
return info
}

static var batteryInfo: BatteryInfo {
let info = BatteryInfo()
info.currentCharge = 72
info.maxCapacity = 4800
info.designCapacity = 5200
info.cycleCount = 312
info.healthPercent = 92
info.temperature = 31.5
info.isCharging = false
info.isPluggedIn = true
info.timeRemaining = "4h 22m"
info.condition = "Normal"
return info
}

static var systemInfo: SystemInfo {
SystemInfo.previewSample
}

static var fpsMonitor: DisplayFPSMonitor {
let monitor = DisplayFPSMonitor()
monitor.fps = 120
return monitor
}

static var updateChecker: UpdateChecker {
let checker = UpdateChecker()
checker.hasChecked = true
checker.updateAvailable = false
checker.latestVersion = checker.currentVersion
return checker
}

static var helper: HelperConnection {
HelperConnection()
}

// MARK: - Preview chrome

enum PreviewFrame {
case popover
case detail
}

static func previewHost<Content: View>(
theme: AppTheme = .dark,
frame: PreviewFrame = .popover,
@ViewBuilder content: () -> Content
) -> some View {
let size: CGSize = {
switch frame {
case .popover: return CGSize(width: 420, height: 640)
case .detail: return CGSize(width: 360, height: 560)
}
}()
return content()
.environment(\.theme, theme)
.preferredColorScheme(themeIsDark(theme) ? .dark : .light)
.frame(width: size.width, height: size.height)
}

private static func themeIsDark(_ theme: AppTheme) -> Bool {
theme.bgGradientTop == AppTheme.dark.bgGradientTop
}
}

extension View {
func previewHost(theme: AppTheme = .dark, frame: PreviewSupport.PreviewFrame = .popover) -> some View {
PreviewSupport.previewHost(theme: theme, frame: frame) { self }
}
}
#endif
25 changes: 25 additions & 0 deletions ChillMac/Views/BatteryDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,28 @@ private struct BatteryDetailRow: View {
.padding(.vertical, 8)
}
}

#if DEBUG
#Preview("Battery Detail — Dark") {
BatteryDetailView(
batteryInfo: PreviewSupport.batteryInfo,
settings: AppSettings.shared
)
.previewHost(theme: .dark, frame: .detail)
}

#Preview("Battery Detail — Light") {
BatteryDetailView(
batteryInfo: PreviewSupport.batteryInfo,
settings: AppSettings.shared
)
.previewHost(theme: .light, frame: .detail)
}

#Preview("Battery Detail Row") {
let info = PreviewSupport.batteryInfo
return BatteryDetailRow(label: "Condition", value: info.condition)
.padding()
.previewHost(theme: .dark, frame: .detail)
}
#endif
43 changes: 43 additions & 0 deletions ChillMac/Views/CpuDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,46 @@ struct CpuGraphView: View {
}
}
}

#if DEBUG
#Preview("CpuDetailView Dark") {
CpuDetailView(
cpuInfo: PreviewSupport.cpuInfo,
systemInfo: PreviewSupport.systemInfo,
monitor: PreviewSupport.fanMonitor,
settings: AppSettings.shared
)
.previewHost(theme: .dark, frame: .detail)
}

#Preview("CpuDetailView Light") {
CpuDetailView(
cpuInfo: PreviewSupport.cpuInfo,
systemInfo: PreviewSupport.systemInfo,
monitor: PreviewSupport.fanMonitor,
settings: AppSettings.shared
)
.previewHost(theme: .light, frame: .detail)
}

#Preview("CpuGraphView") {
let cpu = PreviewSupport.cpuInfo
return CpuGraphView(
history: cpu.history,
userHistory: cpu.userHistory,
systemHistory: cpu.systemHistory
)
.previewHost(theme: .dark, frame: .detail)
}

#Preview("UsageRow") {
let cpu = PreviewSupport.cpuInfo
return UsageRow(
color: .teal,
value: String(format: "%.1f %%", 100 - cpu.totalUsage),
label: "Available"
)
.padding()
.previewHost(theme: .dark, frame: .detail)
}
#endif
Loading