Repository navigation
iOS: add Mac power controls #8137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import Foundation | ||
|
|
||
| public enum MobileMacPowerError: Error, Equatable, Sendable { | ||
| case unavailable | ||
| case unsupported | ||
| case invalidResponse | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import Foundation | ||
|
|
||
| public struct MobileMacPowerStatus: Equatable, Sendable { | ||
| public let keepAwakeEnabled: Bool | ||
| public let lowPowerEnabled: Bool | ||
|
|
||
| static func decode(_ data: Data) throws -> MobileMacPowerStatus { | ||
| let envelope = try JSONSerialization.jsonObject(with: data) as? [String: Any] | ||
| guard envelope?["ok"] as? Bool == true, | ||
| let result = envelope?["result"] as? [String: Any], | ||
| let keepAwake = result["keep_awake_enabled"] as? Bool, | ||
| let lowPower = result["low_power_enabled"] as? Bool else { | ||
| throw MobileMacPowerError.invalidResponse | ||
| } | ||
| return MobileMacPowerStatus( | ||
| keepAwakeEnabled: keepAwake, | ||
| lowPowerEnabled: lowPower | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import CmuxMobileRPC | ||
| import Foundation | ||
|
|
||
| extension MobileShellComposite { | ||
| public var supportsMacPowerControl: Bool { | ||
| supportedHostCapabilities.contains(Self.macPowerCapability) | ||
| } | ||
|
|
||
| public func macPowerStatus() async throws -> MobileMacPowerStatus { | ||
| try await sendMacPowerRequest(method: "mac.power.status", params: [:]) | ||
| } | ||
|
|
||
| public func sleepMacDisplay() async throws { | ||
| guard supportsMacPowerControl, let remoteClient else { throw MobileMacPowerError.unavailable } | ||
| let request = try MobileCoreRPCClient.requestData(method: "mac.power.sleep_display", params: [:]) | ||
| _ = try await remoteClient.sendRequest(request) | ||
| } | ||
|
|
||
| public func setMacKeepAwake(_ enabled: Bool) async throws -> MobileMacPowerStatus { | ||
| try await sendMacPowerRequest( | ||
| method: "mac.power.keep_awake.set", params: ["enabled": enabled] | ||
| ) | ||
| } | ||
|
|
||
| public func setMacLowPowerMode(_ enabled: Bool) async throws -> MobileMacPowerStatus { | ||
| try await sendMacPowerRequest( | ||
| method: "mac.power.low_power.set", params: ["enabled": enabled] | ||
| ) | ||
| } | ||
|
|
||
| private func sendMacPowerRequest( | ||
| method: String, params: [String: Any] | ||
| ) async throws -> MobileMacPowerStatus { | ||
| guard supportsMacPowerControl, let remoteClient else { throw MobileMacPowerError.unavailable } | ||
| let request = try MobileCoreRPCClient.requestData(method: method, params: params) | ||
| let response = try await remoteClient.sendRequest(request) | ||
| return try MobileMacPowerStatus.decode(response) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import Foundation | ||
| import Testing | ||
| @testable import CmuxMobileShell | ||
|
|
||
| @Suite struct MobileMacPowerStatusTests { | ||
| @Test func decodesAuthoritativeStatus() throws { | ||
| let data = try JSONSerialization.data(withJSONObject: [ | ||
| "ok": true, | ||
| "result": [ | ||
| "keep_awake_enabled": true, | ||
| "low_power_enabled": false, | ||
| ], | ||
| ]) | ||
| let status = try MobileMacPowerStatus.decode(data) | ||
| #expect(status.keepAwakeEnabled) | ||
| #expect(!status.lowPowerEnabled) | ||
| } | ||
|
|
||
| @Test func rejectsIncompleteStatus() throws { | ||
| let data = try JSONSerialization.data(withJSONObject: [ | ||
| "ok": true, | ||
| "result": ["keep_awake_enabled": true], | ||
| ]) | ||
| #expect(throws: MobileMacPowerError.invalidResponse) { | ||
| try MobileMacPowerStatus.decode(data) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -35,6 +35,10 @@ struct MacComputerDetailView: View { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @State private var pendingCustomName: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @State private var pendingCustomColor: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @State private var pendingCustomIcon: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @State private var macPowerStatus: MobileMacPowerStatus? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @State private var macPowerError: String? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @State private var isMacPowerBusy = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @State private var pendingDisplaySleep = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Curated icon choices: a few computer/utility SF Symbols + emojis. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static let symbolChoices = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -70,6 +74,7 @@ struct MacComputerDetailView: View { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| presenceSection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| routesSection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identitySection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isForeground && store.supportsMacPowerControl { macPowerSection } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| actionsSection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .navigationTitle(displayTitle) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -86,6 +91,18 @@ struct MacComputerDetailView: View { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| customColorPick = color | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .confirmationDialog( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| L10n.string("mobile.macPower.sleep.confirm.title", defaultValue: "Sleep Mac display?"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isPresented: $pendingDisplaySleep, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| titleVisibility: .visible | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Button(L10n.string("mobile.macPower.sleep", defaultValue: "Sleep Display"), role: .destructive) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runPowerAction { try await store.sleepMacDisplay() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Button(L10n.string("mobile.common.cancel", defaultValue: "Cancel"), role: .cancel) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } message: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Text(L10n.string("mobile.macPower.sleep.confirm.message", defaultValue: "The Mac stays awake, but all connected displays turn off.")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .confirmationDialog( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "\(L10n.string("mobile.computers.removeTitlePrefix", defaultValue: "Remove")) \(displayTitle)?", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isPresented: $pendingRemoval, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -100,6 +117,10 @@ struct MacComputerDetailView: View { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } message: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Text(removeMessage) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .task(id: "\(isForeground)-\(store.supportsMacPowerControl)") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard isForeground, store.supportsMacPowerControl else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await refreshMacPower() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: - Appearance editing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -450,6 +471,91 @@ struct MacComputerDetailView: View { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @ViewBuilder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var macPowerSection: some View { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Section(L10n.string("mobile.macPower.section", defaultValue: "Mac Power")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Toggle( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| L10n.string("mobile.macPower.keepAwake", defaultValue: "Keep Mac Awake"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOn: Binding( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| get: { macPowerStatus?.keepAwakeEnabled ?? false }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set: { enabled in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runPowerStatusAction { try await store.setMacKeepAwake(enabled) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .disabled(isMacPowerBusy || macPowerStatus == nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .accessibilityIdentifier("MobileMacKeepAwakeSwitch") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Toggle( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| L10n.string("mobile.macPower.lowPower", defaultValue: "Low Power Mode"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isOn: Binding( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| get: { macPowerStatus?.lowPowerEnabled ?? false }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set: { enabled in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runPowerStatusAction { try await store.setMacLowPowerMode(enabled) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .disabled(isMacPowerBusy || macPowerStatus == nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .accessibilityIdentifier("MobileMacLowPowerSwitch") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Button { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingDisplaySleep = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } label: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Label(L10n.string("mobile.macPower.sleep", defaultValue: "Sleep Display"), systemImage: "moon.fill") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .disabled(isMacPowerBusy) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .accessibilityIdentifier("MobileMacSleepDisplayButton") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isMacPowerBusy { ProgressView() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let macPowerError { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Text(macPowerError).foregroundStyle(.red) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .accessibilityIdentifier("MobileMacPowerError") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func refreshMacPower() async { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isMacPowerBusy = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer { isMacPowerBusy = false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| do { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let status = try await store.macPowerStatus() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| macPowerStatus = status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| macPowerError = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| macPowerError = L10n.string("mobile.macPower.error", defaultValue: "Couldn't update Mac power controls.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func runPowerAction(_ action: @escaping @MainActor () async throws -> Void) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard !isMacPowerBusy else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isMacPowerBusy = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| macPowerError = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Task { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer { isMacPowerBusy = false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| do { try await action() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| catch { macPowerError = L10n.string("mobile.macPower.error", defaultValue: "Couldn't update Mac power controls.") } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func runPowerStatusAction( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ action: @escaping @MainActor () async throws -> MobileMacPowerStatus | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard !isMacPowerBusy else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isMacPowerBusy = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| macPowerError = nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Task { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer { isMacPowerBusy = false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| do { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let status = try await action() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| macPowerStatus = status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| macPowerError = L10n.string("mobile.macPower.error", defaultValue: "Couldn't update Mac power controls.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await refreshMacPower() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+541
to
+558
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Prevent If the backend mutation (e.g., Swap the order so the UI resynchronizes the toggle status before you assign the failure message for the mutation. 🐛 Proposed fix private func runPowerStatusAction(
_ action: `@escaping` `@MainActor` () async throws -> MobileMacPowerStatus
) {
guard !isMacPowerBusy else { return }
isMacPowerBusy = true
macPowerError = nil
Task {
defer { isMacPowerBusy = false }
do {
let status = try await action()
macPowerStatus = status
} catch {
- macPowerError = L10n.string("mobile.macPower.error", defaultValue: "Couldn't update Mac power controls.")
await refreshMacPower()
+ macPowerError = L10n.string("mobile.macPower.error", defaultValue: "Couldn't update Mac power controls.")
}
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private var connectionPhrase: String { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch connectionStatus { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case .connected: return L10n.string("mobile.deviceTree.connected", defaultValue: "Connected") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import Foundation | ||
|
|
||
| @MainActor | ||
| final class MacPowerService { | ||
| enum ServiceError: Error, Equatable { case assertionFailed, lowPowerMutationFailed } | ||
|
|
||
| private let powerControls: any SleepyPowerControlling | ||
| private let assertionHolder: any PowerAssertionHolding | ||
|
|
||
| init( | ||
| powerControls: any SleepyPowerControlling, | ||
| assertionHolder: any PowerAssertionHolding | ||
| ) { | ||
| self.powerControls = powerControls | ||
| self.assertionHolder = assertionHolder | ||
| } | ||
|
|
||
| convenience init() { | ||
| self.init( | ||
| powerControls: SleepyPowerControls(), | ||
| assertionHolder: PowerAssertionHolder() | ||
| ) | ||
| } | ||
|
|
||
| func status() async -> MacPowerStatus { | ||
| MacPowerStatus( | ||
| keepAwakeEnabled: assertionHolder.isEnabled, | ||
| lowPowerEnabled: await powerControls.isLowPowerOn() | ||
| ) | ||
| } | ||
|
|
||
| func sleepDisplay() async { await powerControls.sleepDisplayNow() } | ||
|
|
||
| func setKeepAwake(_ enabled: Bool) async throws -> MacPowerStatus { | ||
| do { try assertionHolder.setEnabled(enabled) } | ||
| catch { throw ServiceError.assertionFailed } | ||
| return await status() | ||
| } | ||
|
|
||
| func setLowPowerMode(_ enabled: Bool) async throws -> MacPowerStatus { | ||
| let applied = await powerControls.setLowPowerMode(enabled) | ||
| guard applied == enabled else { throw ServiceError.lowPowerMutationFailed } | ||
| let authoritative = await status() | ||
| guard authoritative.lowPowerEnabled == enabled else { | ||
| throw ServiceError.lowPowerMutationFailed | ||
| } | ||
| return authoritative | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import Foundation | ||
|
|
||
| struct MacPowerStatus: Equatable, Sendable { | ||
| let keepAwakeEnabled: Bool | ||
| let lowPowerEnabled: Bool | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1086,10 +1086,15 @@ final class MobileHostService { | |||||||||||||
| guard requiresAuthorization(method: request.method) else { return nil } | ||||||||||||||
| return await stackAuthorization(request) | ||||||||||||||
| case .irohAdmission: | ||||||||||||||
| return nil | ||||||||||||||
| guard isPrivilegedMacPowerMethod(request.method) else { return nil } | ||||||||||||||
| return await stackAuthorization(request) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| nonisolated private static func isPrivilegedMacPowerMethod(_ method: String) -> Bool { | ||||||||||||||
| method == "mac.power.status" || method.hasPrefix("mac.power.") | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+1094
to
+1096
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Comment on lines
+1094
to
+1096
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+1094
to
+1096
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value Remove redundant exact-match check. The 💡 Proposed refactor- nonisolated private static func isPrivilegedMacPowerMethod(_ method: String) -> Bool {
- method == "mac.power.status" || method.hasPrefix("mac.power.")
- }
+ nonisolated private static func isPrivilegedMacPowerMethod(_ method: String) -> Bool {
+ method.hasPrefix("mac.power.")
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| nonisolated static func connectionStatusResult( | ||||||||||||||
| for request: MobileHostRPCRequest, | ||||||||||||||
| authorization: MobileHostConnectionAuthorizationContext, | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||
| import IOKit.pwr_mgt | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @MainActor | ||||||||||||||||||||||||||
| final class PowerAssertionHolder: PowerAssertionHolding { | ||||||||||||||||||||||||||
| enum HolderError: Error { case createFailed(IOReturn) } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private var assertionID = IOPMAssertionID(0) | ||||||||||||||||||||||||||
| var isEnabled: Bool { assertionID != 0 } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| deinit { | ||||||||||||||||||||||||||
| if assertionID != 0 { IOPMAssertionRelease(assertionID) } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+8
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Fix Swift 6 strict concurrency violation in In Swift 6, Since 🛠 Proposed fix- private var assertionID = IOPMAssertionID(0)
+ private nonisolated(unsafe) var assertionID = IOPMAssertionID(0)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| func setEnabled(_ enabled: Bool) throws { | ||||||||||||||||||||||||||
| guard enabled != isEnabled else { return } | ||||||||||||||||||||||||||
| if enabled { | ||||||||||||||||||||||||||
| var newID = IOPMAssertionID(0) | ||||||||||||||||||||||||||
| let result = IOPMAssertionCreateWithName( | ||||||||||||||||||||||||||
| kIOPMAssertionTypePreventUserIdleSystemSleep as CFString, | ||||||||||||||||||||||||||
| IOPMAssertionLevel(kIOPMAssertionLevelOn), | ||||||||||||||||||||||||||
| "cmux remote keep-awake" as CFString, | ||||||||||||||||||||||||||
| &newID | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| guard result == kIOReturnSuccess else { throw HolderError.createFailed(result) } | ||||||||||||||||||||||||||
| assertionID = newID | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| let result = IOPMAssertionRelease(assertionID) | ||||||||||||||||||||||||||
| guard result == kIOReturnSuccess else { throw HolderError.createFailed(result) } | ||||||||||||||||||||||||||
| assertionID = IOPMAssertionID(0) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import Foundation | ||
|
|
||
| @MainActor | ||
| protocol PowerAssertionHolding: AnyObject { | ||
| var isEnabled: Bool { get } | ||
| func setEnabled(_ enabled: Bool) throws | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value
Prefer an array for the
.taskID to avoid string allocations.Using string interpolation for the
.taskidentifier allocates a new string on every render pass of this view. Since both properties are booleans, you can pass them as anEquatablearray to achieve the exact same invalidation semantics with less overhead.💡 Proposed refactor
📝 Committable suggestion
🤖 Prompt for AI Agents