Skip to content

Commit 51bd70a

Browse files
committed
archive is now possible
1 parent bd9cae0 commit 51bd70a

File tree

14 files changed

+579
-32540
lines changed

14 files changed

+579
-32540
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ CLI Monitoring:
9292
- Power adapter data validation
9393
- UI/UX optimization for power management workflows
9494

95+
- **Command+Q not quitting when popover is visible**: When the application is activated via the status bar icon and the popover window is displayed, the standard Command+Q shortcut does not quit the application. This is likely due to the popover being a non-activating panel, preventing the application from becoming the key application to handle the shortcut. An attempt to fix this using a global event monitor was unsuccessful. Further investigation and alternative solutions are needed.
96+
9597
## Contributing
9698
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
9799

app.xcodeproj/project.pbxproj

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
TargetAttributes = {
176176
9DB688032D8E7A53007083F0 = {
177177
CreatedOnToolsVersion = 16.2;
178+
LastSwiftMigration = 1630;
178179
};
179180
9DB688142D8E7A54007083F0 = {
180181
CreatedOnToolsVersion = 16.2;
@@ -395,24 +396,30 @@
395396
buildSettings = {
396397
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
397398
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
399+
CLANG_ENABLE_MODULES = YES;
398400
CODE_SIGN_ENTITLEMENTS = app/app.entitlements;
399401
CODE_SIGN_STYLE = Automatic;
400402
COMBINE_HIDPI_IMAGES = YES;
401403
CURRENT_PROJECT_VERSION = 1;
402404
DEAD_CODE_STRIPPING = YES;
403405
DEVELOPMENT_ASSET_PATHS = "\"app/Preview Content\"";
406+
DEVELOPMENT_TEAM = VKR4WCM7P3;
407+
ENABLE_HARDENED_RUNTIME = YES;
404408
ENABLE_PREVIEWS = YES;
405409
GENERATE_INFOPLIST_FILE = YES;
410+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
406411
INFOPLIST_KEY_LSUIElement = YES;
407412
INFOPLIST_KEY_NSHumanReadableCopyright = "";
408413
LD_RUNPATH_SEARCH_PATHS = (
409414
"$(inherited)",
410415
"@executable_path/../Frameworks",
411416
);
412-
MARKETING_VERSION = 1.0;
417+
MARKETING_VERSION = 0.1.1;
413418
PRODUCT_BUNDLE_IDENTIFIER = clzoc.app;
414419
PRODUCT_NAME = "$(TARGET_NAME)";
415420
SWIFT_EMIT_LOC_STRINGS = YES;
421+
SWIFT_OBJC_BRIDGING_HEADER = "app/app-Bridging-Header.h";
422+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
416423
SWIFT_VERSION = 5.0;
417424
};
418425
name = Debug;
@@ -422,24 +429,29 @@
422429
buildSettings = {
423430
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
424431
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
432+
CLANG_ENABLE_MODULES = YES;
425433
CODE_SIGN_ENTITLEMENTS = app/app.entitlements;
426434
CODE_SIGN_STYLE = Automatic;
427435
COMBINE_HIDPI_IMAGES = YES;
428436
CURRENT_PROJECT_VERSION = 1;
429437
DEAD_CODE_STRIPPING = YES;
430438
DEVELOPMENT_ASSET_PATHS = "\"app/Preview Content\"";
439+
DEVELOPMENT_TEAM = VKR4WCM7P3;
440+
ENABLE_HARDENED_RUNTIME = YES;
431441
ENABLE_PREVIEWS = YES;
432442
GENERATE_INFOPLIST_FILE = YES;
443+
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
433444
INFOPLIST_KEY_LSUIElement = YES;
434445
INFOPLIST_KEY_NSHumanReadableCopyright = "";
435446
LD_RUNPATH_SEARCH_PATHS = (
436447
"$(inherited)",
437448
"@executable_path/../Frameworks",
438449
);
439-
MARKETING_VERSION = 1.0;
450+
MARKETING_VERSION = 0.1.1;
440451
PRODUCT_BUNDLE_IDENTIFIER = clzoc.app;
441452
PRODUCT_NAME = "$(TARGET_NAME)";
442453
SWIFT_EMIT_LOC_STRINGS = YES;
454+
SWIFT_OBJC_BRIDGING_HEADER = "app/app-Bridging-Header.h";
443455
SWIFT_VERSION = 5.0;
444456
};
445457
name = Release;

app/Battery.swift

Lines changed: 67 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -24,83 +24,69 @@
2424
import Foundation
2525
import SwiftUI
2626

27-
/// Manages fetching and parsing battery and power information from system commands.
28-
///
29-
/// This class acts as an `ObservableObject`, providing published properties
30-
/// that SwiftUI views can observe to display real-time battery and power metrics.
31-
/// It uses `ioreg` and the bundled `power_info` command-line tool to gather data.
27+
// Manages fetching and parsing battery and power information from system commands.
28+
//
29+
// This class acts as an `ObservableObject`, providing published properties
30+
// that SwiftUI views can observe to display real-time battery and power metrics.
31+
// It uses `ioreg` and the bundled `power_info` command-line tool to gather data.
3232
class BatteryInfoManager: ObservableObject {
33-
/// The current maximum capacity of the battery in mAh (AppleRawMaxCapacity).
33+
// The current maximum capacity of the battery in mAh (AppleRawMaxCapacity).
3434
@Published var batteryCapacity: Int = 0
35-
/// The original design capacity of the battery in mAh.
35+
// The original design capacity of the battery in mAh.
3636
@Published var designCapacity: Int = 0
37-
/// The number of charge cycles the battery has undergone.
37+
// The number of charge cycles the battery has undergone.
3838
@Published var cycleCount: Int = 0
39-
/// The battery's health percentage, calculated as `(batteryCapacity / designCapacity) * 100`.
39+
// The battery's health percentage, calculated as `(batteryCapacity / designCapacity) * 100`.
4040
@Published var health: Double = 0.0
41-
/// Indicates whether the battery is currently charging.
41+
// Indicates whether the battery is currently charging.
4242
@Published var isCharging: Bool = false
43-
/// The current charge percentage of the battery (CurrentCapacity).
43+
// The current charge percentage of the battery (CurrentCapacity).
4444
@Published var batteryPercent: Int = 0
45-
/// The voltage being supplied by the power adapter in Volts.
45+
// The voltage being supplied by the power adapter in Volts.
4646
@Published var voltage: Double = 0.0
47-
/// The amperage being supplied by the power adapter in Amps.
47+
// The amperage being supplied by the power adapter in Amps.
4848
@Published var amperage: Double = 0.0
49-
/// The current power consumption of the entire system in Watts.
49+
// The current power consumption of the entire system in Watts.
5050
@Published var loadwatt: Double = 0.0
51-
/// The power being drawn from the power adapter in Watts.
51+
// The power being drawn from the power adapter in Watts.
5252
@Published var inputwatt: Double = 0.0
53-
/// The battery's internal temperature in degrees Celsius.
53+
// The battery's internal temperature in degrees Celsius.
5454
@Published var temperature: Double = 0.0
55-
/// The current power draw from/to the battery in Watts. Positive means charging, negative means discharging.
55+
// The current power draw from/to the battery in Watts. Positive means charging, negative means discharging.
5656
@Published var batteryPower: Double = 0.0
57-
/// The current voltage of the battery in Volts.
57+
// The current voltage of the battery in Volts.
5858
@Published var batteryVoltage: Double = 0.0
59-
/// The current amperage flow from/to the battery in Amps. Positive means charging, negative means discharging.
59+
// The current amperage flow from/to the battery in Amps. Positive means charging, negative means discharging.
6060
@Published var batteryAmperage: Double = 0.0
61-
/// The serial number of the battery.
61+
// The serial number of the battery.
6262
@Published var serialNumber: String = "--"
6363

64-
/// Initializes the manager and triggers the first battery info update.
64+
@Published var batteryVoltage_mV: UInt16 = 0
65+
@Published var batteryAmperage_mA: Int16 = 0
66+
67+
// Initializes the manager and triggers the first battery info update.
6568
init() {
6669
updateBatteryInfo()
6770
}
6871

69-
/// Asynchronously fetches and updates all battery information properties.
70-
///
71-
/// This function runs the `power_info` tool and `ioreg` command,
72-
/// captures their output, and then calls `parseBatteryInfo` on the main thread
73-
/// to update the published properties.
72+
// Asynchronously fetches and updates all battery information properties.
73+
//
74+
// This function runs the `power_info` tool and `ioreg` command,
75+
// captures their output, and then calls `parseBatteryInfo` on the main thread
76+
// to update the published properties.
7477
func updateBatteryInfo() {
7578
Task {
76-
guard let power_info_URL = Bundle.main.url(forResource: "power_info", withExtension: nil) else {
77-
// TODO: Replace fatalError with more robust error handling (e.g., logging, showing alert)
78-
fatalError("Executable 'power_info' not found in bundle.")
79-
}
80-
81-
let po = Process()
82-
po.executableURL = URL(fileURLWithPath: "/bin/zsh")
83-
po.arguments = ["-c", "source ~/.zshrc; \(power_info_URL.path)"]
84-
let pp = Pipe()
85-
po.standardOutput = pp
86-
po.standardError = pp
87-
8879
let process = Process()
8980
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
9081
process.arguments = ["-c", "ioreg -r -c AppleSmartBattery | grep -E 'DesignCapacity|CycleCount|Serial|Temperature|CurrentCapacity|AppleRawMaxCapacity' "]
9182
let pipe = Pipe()
9283
process.standardOutput = pipe
9384

9485
do {
95-
try po.run()
96-
po.waitUntilExit()
97-
let dt = pp.fileHandleForReading.readDataToEndOfFile()
98-
var output = String(data: dt, encoding: .utf8) ?? ""
99-
10086
try process.run()
10187
process.waitUntilExit()
10288
let data = pipe.fileHandleForReading.readDataToEndOfFile()
103-
output += String(data: data, encoding: .utf8) ?? ""
89+
let output = String(data: data, encoding: .utf8) ?? ""
10490

10591
await parseBatteryInfo(from: output)
10692
} catch {
@@ -109,19 +95,48 @@ class BatteryInfoManager: ObservableObject {
10995
}
11096
}
11197

112-
/// Parses the combined output from `power_info` and `ioreg` to update the battery properties.
113-
///
114-
/// This function uses regular expressions to extract specific values from the command output string.
115-
/// It must be called on the main actor because it updates `@Published` properties.
116-
///
117-
/// - Parameter output: The combined string output from the system commands.
11898
@MainActor
11999
private func parseBatteryInfo(from output: String) {
100+
loadwatt = Double(getRawSystemPower())
101+
inputwatt = Double(getAdapterPower())
102+
amperage = Double(getAdapterAmperage())
103+
voltage = Double(getAdapterVoltage())
104+
batteryVoltage = Double(getBatteryVoltage())
105+
batteryAmperage = Double(getBatteryAmperage())
106+
batteryPower = Double(getBatteryPower())
107+
isCharging = getChargingStatus().contains("Charging")
108+
109+
// --- Parse Temperature (ioreg - VirtualTemperature seems more reliable than power_info's) ---
110+
if let match = output.range(of: "\"VirtualTemperature\" = ([0-9]+)", options: .regularExpression) {
111+
let valueStr = String(output[match]).components(separatedBy: "=").last?.trimmingCharacters(in: .whitespaces) ?? "0"
112+
let temperatureValue = Int(valueStr.trimmingCharacters(in: CharacterSet(charactersIn: "\" "))) ?? 0
113+
temperature = Double(temperatureValue) / 100.0
114+
}
115+
116+
// --- Parse Serial Number (ioreg) ---
117+
if let match = output.range(of: "\"Serial\" = \"([^\"]+)\"", options: .regularExpression) {
118+
let fullMatch = String(output[match])
119+
let pattern = "\"Serial\" = \"([^\"]+)\""
120+
if let regex = try? NSRegularExpression(pattern: pattern),
121+
let nsMatch = regex.firstMatch(in: fullMatch, range: NSRange(fullMatch.startIndex..., in: fullMatch)),
122+
nsMatch.numberOfRanges > 1,
123+
let valueRange = Range(nsMatch.range(at: 1), in: fullMatch) {
124+
serialNumber = String(fullMatch[valueRange])
125+
}
126+
}
127+
128+
// --- Parse Current Charge Percentage (ioreg) ---
129+
if let match = output.range(of: "\"CurrentCapacity\" = ([0-9]+)", options: .regularExpression) {
130+
let valueStr = String(output[match]).components(separatedBy: "=").last?.trimmingCharacters(in: .whitespaces) ?? "0"
131+
batteryPercent = Int(valueStr.trimmingCharacters(in: CharacterSet(charactersIn: "\" "))) ?? 0
132+
}
133+
120134
// --- Parse Design Capacity (ioreg) ---
121135
if let match = output.range(of: "\"DesignCapacity\" = ([0-9]+)", options: .regularExpression) {
122136
let valueStr = String(output[match]).components(separatedBy: "=").last?.trimmingCharacters(in: .whitespaces) ?? "0"
123137
designCapacity = Int(valueStr.trimmingCharacters(in: CharacterSet(charactersIn: "\" "))) ?? 0
124138
}
139+
125140
// --- Parse Current Max Capacity & Calculate Health (ioreg) ---
126141
if let match = output.range(of: "\"AppleRawMaxCapacity\" = ([0-9]+)", options: .regularExpression) {
127142
let valueStr = String(output[match]).components(separatedBy: "=").last?.trimmingCharacters(in: .whitespaces) ?? "0"
@@ -130,101 +145,12 @@ class BatteryInfoManager: ObservableObject {
130145
health = (Double(batteryCapacity) / Double(designCapacity)) * 100
131146
}
132147
}
148+
133149
// --- Parse Cycle Count (ioreg) ---
134150
if let match = output.range(of: "\"CycleCount\" = ([0-9]+)", options: .regularExpression) {
135151
let valueStr = String(output[match]).components(separatedBy: "=").last?.trimmingCharacters(in: .whitespaces) ?? "0"
136152
cycleCount = Int(valueStr.trimmingCharacters(in: CharacterSet(charactersIn: "\" "))) ?? 0
137153
}
138-
// --- Parse Charging Status (power_info) ---
139-
if let match = output.range(of: "battery_status=([a-zA-Z]+)", options: .regularExpression) {
140-
let valueStr = String(output[match]).components(separatedBy: "=").last?.trimmingCharacters(in: .whitespaces) ?? "Idle"
141-
isCharging = valueStr.contains("Charging")
142-
}
143-
// --- Parse Current Charge Percentage (ioreg) ---
144-
if let match = output.range(of: "\"CurrentCapacity\" = ([0-9]+)", options: .regularExpression) {
145-
let valueStr = String(output[match]).components(separatedBy: "=").last?.trimmingCharacters(in: .whitespaces) ?? "0"
146-
batteryPercent = Int(valueStr.trimmingCharacters(in: CharacterSet(charactersIn: "\" "))) ?? 0
147-
}
148-
// --- Parse Adapter Voltage (power_info) ---
149-
let patternV = "adapter_voltage=([0-9]+(?:\\.[0-9]+)?)V"
150-
if let regex = try? NSRegularExpression(pattern: patternV) {
151-
let matches = regex.matches(in: output, range: NSRange(output.startIndex..., in: output))
152-
if let match = matches.first, let range = Range(match.range(at: 1), in: output) {
153-
let valueStr = String(output[range])
154-
voltage = Double(valueStr) ?? 0.0
155-
}
156-
}
157-
// --- Parse Adapter Amperage (power_info) ---
158-
let patternA = "adapter_amperage=([0-9]+(?:\\.[0-9]+)?)A"
159-
if let regex = try? NSRegularExpression(pattern: patternA) {
160-
let matches = regex.matches(in: output, range: NSRange(output.startIndex..., in: output))
161-
if let match = matches.first, let range = Range(match.range(at: 1), in: output) {
162-
let valueStr = String(output[range])
163-
amperage = Double(valueStr) ?? 0.0
164-
}
165-
}
166-
// --- Parse System Power (power_info) ---
167-
let patternSysP = "sys_power=([0-9]+(?:\\.[0-9]+)?)W"
168-
if let regex = try? NSRegularExpression(pattern: patternSysP) {
169-
let matches = regex.matches(in: output, range: NSRange(output.startIndex..., in: output))
170-
if let match = matches.first, let range = Range(match.range(at: 1), in: output) {
171-
let valueStr = String(output[range])
172-
loadwatt = Double(valueStr) ?? 0.0
173-
}
174-
}
175-
// --- Parse Adapter Power (power_info) ---
176-
let patternAdpP = "adapter_power=([0-9]+(?:\\.[0-9]+)?)W"
177-
if let regex = try? NSRegularExpression(pattern: patternAdpP) {
178-
let matches = regex.matches(in: output, range: NSRange(output.startIndex..., in: output))
179-
if let match = matches.first, let range = Range(match.range(at: 1), in: output) {
180-
let valueStr = String(output[range])
181-
inputwatt = Double(valueStr) ?? 0.0
182-
}
183-
}
184-
// --- Parse Battery Power (power_info) ---
185-
let patternBattP = "battery_power=(\\-?[0-9]+(?:\\.[0-9]+)?)W" // Allow negative
186-
if let regex = try? NSRegularExpression(pattern: patternBattP) {
187-
let matches = regex.matches(in: output, range: NSRange(output.startIndex..., in: output))
188-
if let match = matches.first, let range = Range(match.range(at: 1), in: output) {
189-
let valueStr = String(output[range])
190-
batteryPower = Double(valueStr) ?? 0.0
191-
}
192-
}
193-
// --- Parse Battery Voltage (power_info) ---
194-
let patternBattV = "battery_voltage=([0-9]+(?:\\.[0-9]+)?)V"
195-
if let regex = try? NSRegularExpression(pattern: patternBattV) {
196-
let matches = regex.matches(in: output, range: NSRange(output.startIndex..., in: output))
197-
if let match = matches.first, let range = Range(match.range(at: 1), in: output) {
198-
let valueStr = String(output[range])
199-
batteryVoltage = Double(valueStr) ?? 0.0
200-
}
201-
}
202-
// --- Parse Battery Amperage (power_info) ---
203-
let patternBattA = "battery_amperage=(\\-?[0-9]+(?:\\.[0-9]+)?)A" // Allow negative
204-
if let regex = try? NSRegularExpression(pattern: patternBattA) {
205-
let matches = regex.matches(in: output, range: NSRange(output.startIndex..., in: output))
206-
if let match = matches.first, let range = Range(match.range(at: 1), in: output) {
207-
let valueStr = String(output[range])
208-
batteryAmperage = Double(valueStr) ?? 0.0
209-
}
210-
}
211-
212-
// --- Parse Temperature (ioreg - VirtualTemperature seems more reliable than power_info's) ---
213-
if let match = output.range(of: "\"VirtualTemperature\" = ([0-9]+)", options: .regularExpression) {
214-
let valueStr = String(output[match]).components(separatedBy: "=").last?.trimmingCharacters(in: .whitespaces) ?? "0"
215-
let temperatureValue = Int(valueStr.trimmingCharacters(in: CharacterSet(charactersIn: "\" "))) ?? 0
216-
temperature = Double(temperatureValue) / 100.0
217-
}
218-
// --- Parse Serial Number (ioreg) ---
219-
if let match = output.range(of: "\"Serial\" = \"([^\"]+)\"", options: .regularExpression) {
220-
let fullMatch = String(output[match])
221-
let pattern = "\"Serial\" = \"([^\"]+)\""
222-
if let regex = try? NSRegularExpression(pattern: pattern),
223-
let nsMatch = regex.firstMatch(in: fullMatch, range: NSRange(fullMatch.startIndex..., in: fullMatch)),
224-
nsMatch.numberOfRanges > 1,
225-
let valueRange = Range(nsMatch.range(at: 1), in: fullMatch) {
226-
serialNumber = String(fullMatch[valueRange])
227-
}
228-
}
154+
229155
}
230156
}

0 commit comments

Comments
 (0)