Skip to content
Draft
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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
CODE_SIGN_IDENTITY=""

release-artifact:
name: Build unsigned release zip
name: Build unsigned release package
runs-on: macos-26
timeout-minutes: 30
needs: test
Expand All @@ -73,6 +73,7 @@ jobs:
with:
name: CleanMac-unsigned-${{ github.sha }}
path: |
dist/*.dmg
dist/*.zip
dist/*.sha256
if-no-files-found: error
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ jobs:
if gh release view "$tag_name" >/dev/null 2>&1; then
echo "Release $tag_name already exists; replacing matching assets."
gh release upload "$tag_name" \
dist/*.dmg \
dist/*.zip \
dist/*.sha256 \
--clobber
else
gh release create "$tag_name" \
dist/*.dmg \
dist/*.zip \
dist/*.sha256 \
--title "CleanMac $tag_name" \
--notes "Unsigned CleanMac build. Download the zip and verify it with the attached sha256 file."
--notes "Unsigned CleanMac build. Open the DMG and drag CleanMac to Applications, or use the ZIP archive. Verify either download with its attached SHA-256 file."
fi
19 changes: 17 additions & 2 deletions CleanMac/Models/CleanMacModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ enum CleanMacSection: String, CaseIterable, Identifiable {
case scan
case results
case diskAnalysis
case systemMaintenance
case duplicates
case shredder
case applications
Expand All @@ -19,6 +20,7 @@ enum CleanMacSection: String, CaseIterable, Identifiable {
case .scan: L.t("section.scan")
case .results: L.t("section.results")
case .diskAnalysis: L.t("section.diskAnalysis")
case .systemMaintenance: L.t("section.system")
case .duplicates: L.t("section.duplicates")
case .shredder: L.t("section.shredder")
case .applications: L.t("section.applications")
Expand All @@ -32,9 +34,10 @@ enum CleanMacSection: String, CaseIterable, Identifiable {
case .scan: "magnifyingglass"
case .results: "checklist"
case .diskAnalysis: "chart.pie.fill"
case .systemMaintenance: "memorychip"
case .duplicates: "square.on.square"
case .shredder: "terminal.fill"
case .applications: "app.badge.checkmark"
case .shredder: "scissors"
case .applications: "square.stack.3d.up.fill"
case .settings: "gearshape"
}
}
Expand Down Expand Up @@ -121,6 +124,7 @@ extension CleanupScanReason {
case .developerPackageCache: L.t("results.reason.developerPackageCache.title")
case .developerIDECache: L.t("results.reason.developerIDECache.title")
case .developerAITemporaryFile: L.t("results.reason.developerAITemporaryFile.title")
case .staleCodexRuntimeInstaller: L.t("results.reason.staleCodexRuntimeInstaller.title")
case .staleLog: L.t("results.reason.staleLog.title")
case .rotatedLog: L.t("results.reason.rotatedLog.title")
case .staleTemporary: L.t("results.reason.staleTemporary.title")
Expand All @@ -145,6 +149,7 @@ extension CleanupScanReason {
case .developerPackageCache: L.t("results.reason.developerPackageCache.detail")
case .developerIDECache: L.t("results.reason.developerIDECache.detail")
case .developerAITemporaryFile: L.t("results.reason.developerAITemporaryFile.detail")
case .staleCodexRuntimeInstaller: L.t("results.reason.staleCodexRuntimeInstaller.detail")
case .staleLog: L.t("results.reason.staleLog.detail")
case .rotatedLog: L.t("results.reason.rotatedLog.detail")
case .staleTemporary: L.t("results.reason.staleTemporary.detail")
Expand All @@ -169,6 +174,7 @@ extension CleanupScanReason {
case .developerPackageCache: "shippingbox.and.arrow.backward"
case .developerIDECache: "curlybraces.square"
case .developerAITemporaryFile: "sparkles.rectangle.stack"
case .staleCodexRuntimeInstaller: "shippingbox.and.arrow.backward.fill"
case .staleLog: "doc.text.magnifyingglass"
case .rotatedLog: "arrow.triangle.2.circlepath"
case .staleTemporary: "clock.arrow.circlepath"
Expand Down Expand Up @@ -365,6 +371,15 @@ enum CleanMacCatalog {
risk: .safe,
isDefaultSelected: true
),
CleanupArea(
category: .staleCodexRuntimeInstallers,
title: L.t("area.staleCodexRuntimes.title"),
detail: L.t("area.staleCodexRuntimes.detail"),
pathHint: "~/.cache/codex-runtimes/codex-runtime-install-*",
systemImage: "shippingbox.and.arrow.backward.fill",
risk: .review,
isDefaultSelected: true
),
CleanupArea(
category: .logs,
title: L.t("area.logs.title"),
Expand Down
14 changes: 13 additions & 1 deletion CleanMac/Support/CleanMacPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Foundation
enum CleanMacPreferenceKeys {
static let onboardingCompleted = "CleanMac.onboardingCompleted"
static let selectedAreaIDs = "CleanMac.selectedAreaIDs"
static let selectedAreaSchemaVersion = "CleanMac.selectedAreaSchemaVersion"
static let lastScanItemCount = "CleanMac.lastScanItemCount"
static let lastScanBytes = "CleanMac.lastScanBytes"
static let lastScanTimestamp = "CleanMac.lastScanTimestamp"
Expand Down Expand Up @@ -57,6 +58,8 @@ enum CleanMacAutoScanFrequency: String, CaseIterable, Identifiable {
}

enum CleanMacScanPreferences {
private static let currentAreaSchemaVersion = 1

static var defaultSelectedAreaIDs: Set<String> {
Set(CleanMacCatalog.cleanupAreas.filter(\.isDefaultSelected).map(\.id))
}
Expand All @@ -68,16 +71,24 @@ enum CleanMacScanPreferences {
static func selectedAreaIDs(defaults: UserDefaults = .standard) -> Set<String> {
let rawValue = defaults.string(forKey: CleanMacPreferenceKeys.selectedAreaIDs)
guard let rawValue else {
defaults.set(currentAreaSchemaVersion, forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion)
return defaultSelectedAreaIDs
}
guard !rawValue.isEmpty else {
defaults.set(currentAreaSchemaVersion, forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion)
return []
}

let validIDs = Set(CleanMacCatalog.cleanupAreas.map(\.id))
let decodedIDs = Set(rawValue.split(separator: ",").map(String.init))
var decodedIDs = Set(rawValue.split(separator: ",").map(String.init))
.intersection(validIDs)

if defaults.integer(forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion) < currentAreaSchemaVersion {
decodedIDs.insert(CleanupCategory.staleCodexRuntimeInstallers.rawValue)
defaults.set(encodeAreaIDs(decodedIDs), forKey: CleanMacPreferenceKeys.selectedAreaIDs)
defaults.set(currentAreaSchemaVersion, forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion)
}

return decodedIDs.isEmpty ? defaultSelectedAreaIDs : decodedIDs
}

Expand All @@ -90,6 +101,7 @@ enum CleanMacScanPreferences {

static func storeSelectedAreaIDs(_ areaIDs: Set<String>, defaults: UserDefaults = .standard) {
defaults.set(encodeAreaIDs(areaIDs), forKey: CleanMacPreferenceKeys.selectedAreaIDs)
defaults.set(currentAreaSchemaVersion, forKey: CleanMacPreferenceKeys.selectedAreaSchemaVersion)
}

static func storeLastScan(_ report: CleanupScanReport, source: CleanMacScanSource, defaults: UserDefaults = .standard) {
Expand Down
100 changes: 63 additions & 37 deletions CleanMac/Support/StatusSystemMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ import CleanMacCore

struct StatusSystemSnapshot: Equatable {
let cpuFraction: Double
let memoryFraction: Double
let memoryUsedBytes: Int64
let memory: StatusMemorySnapshot
let disk: StatusDiskSnapshot
let battery: StatusBatterySnapshot?
let downloadBytesPerSecond: Int64
let uploadBytesPerSecond: Int64
let uptime: TimeInterval

var memoryFraction: Double {
memory.fraction
}

var memoryUsedBytes: Int64 {
memory.usedBytes
}

static var initial: StatusSystemSnapshot {
StatusSystemSnapshot(
cpuFraction: 0,
memoryFraction: 0,
memoryUsedBytes: 0,
memory: .unavailable,
disk: .current(),
battery: StatusBatterySnapshot.current(),
downloadBytesPerSecond: 0,
Expand All @@ -27,6 +33,57 @@ struct StatusSystemSnapshot: Equatable {
}
}

struct StatusMemorySnapshot: Equatable, Sendable {
let fraction: Double
let usedBytes: Int64
let totalBytes: Int64

var freeBytes: Int64 {
max(totalBytes - usedBytes, 0)
}

nonisolated static var unavailable: StatusMemorySnapshot {
StatusMemorySnapshot(fraction: 0, usedBytes: 0, totalBytes: 0)
}

nonisolated static func current() -> StatusMemorySnapshot {
var statistics = vm_statistics64()
var count = mach_msg_type_number_t(
MemoryLayout<vm_statistics64_data_t>.stride / MemoryLayout<integer_t>.stride
)

let result = withUnsafeMutablePointer(to: &statistics) { pointer in
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { reboundPointer in
host_statistics64(mach_host_self(), HOST_VM_INFO64, reboundPointer, &count)
}
}

let totalBytes = ProcessInfo.processInfo.physicalMemory
guard result == KERN_SUCCESS, totalBytes > 0 else {
return .unavailable
}

var pageSize: vm_size_t = 0
host_page_size(mach_host_self(), &pageSize)
let pageBytes = UInt64(pageSize)
guard pageBytes > 0 else {
return .unavailable
}

let usedPages = UInt64(statistics.active_count)
+ UInt64(statistics.wire_count)
+ UInt64(statistics.compressor_page_count)
let cappedPages = min(usedPages, totalBytes / pageBytes)
let usedBytes = cappedPages * pageBytes

return StatusMemorySnapshot(
fraction: min(max(Double(usedBytes) / Double(totalBytes), 0), 1),
usedBytes: Int64(clamping: usedBytes),
totalBytes: Int64(clamping: totalBytes)
)
}
}

struct StatusDiskSnapshot: Equatable {
let volumeName: String?
let totalBytes: Int64
Expand Down Expand Up @@ -152,15 +209,14 @@ struct StatusSystemSampler {
let cpuFraction = cpuUsage(current: currentCPU, previous: previousCPU)
previousCPU = currentCPU

let memory = readMemory()
let memory = StatusMemorySnapshot.current()
let currentNetwork = readNetworkCounters()
let networkRates = networkRates(current: currentNetwork, at: now, previous: previousNetwork)
previousNetwork = (now, currentNetwork)

return StatusSystemSnapshot(
cpuFraction: cpuFraction,
memoryFraction: memory.fraction,
memoryUsedBytes: memory.usedBytes,
memory: memory,
disk: .current(),
battery: StatusBatterySnapshot.current(),
downloadBytesPerSecond: networkRates.received,
Expand Down Expand Up @@ -207,36 +263,6 @@ struct StatusSystemSampler {
return min(max(Double(busyDelta) / Double(totalDelta), 0), 1)
}

private func readMemory() -> (fraction: Double, usedBytes: Int64) {
var statistics = vm_statistics64()
var count = mach_msg_type_number_t(
MemoryLayout<vm_statistics64_data_t>.stride / MemoryLayout<integer_t>.stride
)

let result = withUnsafeMutablePointer(to: &statistics) { pointer in
pointer.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { reboundPointer in
host_statistics64(mach_host_self(), HOST_VM_INFO64, reboundPointer, &count)
}
}

let totalBytes = ProcessInfo.processInfo.physicalMemory
guard result == KERN_SUCCESS, totalBytes > 0 else {
return (0, 0)
}

var pageSize: vm_size_t = 0
host_page_size(mach_host_self(), &pageSize)
let usedPages = UInt64(statistics.active_count)
+ UInt64(statistics.wire_count)
+ UInt64(statistics.compressor_page_count)
let usedBytes = min(usedPages * UInt64(pageSize), totalBytes)

return (
min(max(Double(usedBytes) / Double(totalBytes), 0), 1),
Int64(clamping: usedBytes)
)
}

private func readNetworkCounters() -> NetworkCounters {
var firstAddress: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&firstAddress) == 0, let firstAddress else {
Expand Down
Loading