Skip to content
Merged
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
24 changes: 23 additions & 1 deletion Sources/KeyPathAppKit/Managers/RuntimeCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,9 @@
/// Persist exact-device Caps consent only after admitted runtime restoration.
/// The change stops the runtime to release the Caps lease; if it was running,
/// it is started again afterwards, whether the change committed or was refused.
func setCapsSelection(_ selection: SessionCapsSelection?, automaticSetupDisabled: Bool? = nil) async -> Bool {
func setCapsSelection(_ selection: SessionCapsSelection?, automaticSetupDisabled: Bool? = nil,
selectedKeyboardOnly: Bool? = nil) async -> Bool
{
let wasRunning = serviceLifecycleCoordinator.sessionWantsRunning
let changed: Bool
do {
Expand All @@ -700,6 +702,26 @@
if let automaticSetupDisabled {
UserDefaults.standard.set(automaticSetupDisabled, forKey: SessionCapsRuntimeSupport.automaticSetupDisabledKey)
}
if let selectedKeyboardOnly {
UserDefaults.standard.set(selectedKeyboardOnly, forKey: SessionCapsRuntimeSupport.selectedKeyboardOnlyKey)
}
})
}
} catch { changed = false }
await serviceLifecycleCoordinator.resumeAfterCapsSelectionChange(wasRunning: wasRunning)
return changed
}

/// Apply keyboard exceptions only after the current leases were restored.
/// A failed stop leaves both the preference and live mappings unchanged.
func setCapsExcludedKeyboardIDs(_ ids: Set<String>) async -> Bool {
guard ids.count <= 128, ids.allSatisfy({ !$0.isEmpty && $0.utf8.count <= 4096 }) else { return false }
let wasRunning = serviceLifecycleCoordinator.sessionWantsRunning
let changed: Bool
do {
changed = try await configurationService.operationGate.withOperation { [serviceLifecycleCoordinator] _ in
await serviceLifecycleCoordinator.changeCapsSelection(verify: {}, commit: {
UserDefaults.standard.set(ids.sorted(), forKey: SessionCapsRuntimeSupport.excludedKeyboardsKey)
})
}
} catch { changed = false }
Expand Down Expand Up @@ -1068,7 +1090,7 @@
}

/// Returns a snapshot of current UI state for ViewModel synchronization
/// This method allows KanataViewModel to read UI state without @Published properties

Check warning on line 1093 in Sources/KeyPathAppKit/Managers/RuntimeCoordinator.swift

View workflow job for this annotation

GitHub Actions / code-quality

Use @observable macro instead of @published. Properties on @observable classes are automatically tracked. (no_published)
func getCurrentUIState() -> KanataUIState {
buildUIState()
}
Expand Down Expand Up @@ -1428,7 +1450,7 @@
) async throws {
let result = await saveCoordinator.saveAppKeymaps(store: store, mutate: mutate, runtimeDidApply: {
await AppContextService.shared.reloadMappings(afterRuntimeReload: true)
}) { [weak self] in

Check warning on line 1453 in Sources/KeyPathAppKit/Managers/RuntimeCoordinator.swift

View workflow job for this annotation

GitHub Actions / code-quality

Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
guard let self else {
return ReloadResult(success: false, response: nil, errorMessage: "Coordinator deallocated", protocol: nil, disposition: .failed)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,22 @@
{
// The keyboard went away between the choice and the
// worker's own check, so the worker runs paused.
sessionCapsIntermediate = nil
if SessionCapsRuntimeSupport.selectedKeyboardOnly() {
sessionCapsIntermediate = nil
}
sessionCapsNotice = nil
sessionCapsPaused = Self.capsPausedMessage(
disconnected: SessionCapsSelectionStore.savedName ?? "The keyboard chosen for Caps Lock",
setupOff: false
)
sessionCapsPaused = SessionCapsRuntimeSupport.selectedKeyboardOnly()
? Self.capsPausedMessage(
disconnected: SessionCapsSelectionStore.savedName ?? "The keyboard chosen for Caps Lock",
setupOff: false
)
: "Caps Lock rules could not start on a connected keyboard. Your other rules keep working. Check keyboard details in Settings > General."
}
if !SessionCapsRuntimeSupport.selectedKeyboardOnly(),
let coverage = report.capsCoverage,
let warning = Self.capsCoverageWarning(coverage)
{
sessionCapsPaused = warning
}
onError?(nil)
onWarning?(sessionCapsNotice ?? sessionCapsPaused)
Expand Down Expand Up @@ -389,12 +399,25 @@
guard sessionStartIsCurrent(generation), sessionNonce == expectedNonce,
let application = sessionApplication, application.processIdentifier == pid else { return false }
if !application.isTerminated {
if currentSessionReport() == nil {
guard let report = currentSessionReport() else {
_ = try? await sessionOperationGate.withOperation { [self] _ in
await stopUnresponsiveSession(expectedGeneration: generation, nonce: expectedNonce)
}
return false
}
if !SessionCapsRuntimeSupport.selectedKeyboardOnly(),
sessionCapsIntermediate != nil
{
let coverageWarning = report.capsCoverage.flatMap(Self.capsCoverageWarning)
let warning = report.managedCapsGeneration == expectedNonce ? coverageWarning
: (coverageWarning ?? sessionCapsPaused
?? "Caps Lock rules are waiting for a compatible keyboard. Your other rules keep working.")
if warning != sessionCapsPaused {
sessionCapsPaused = warning
onWarning?(warning)
onStateChanged?()
}
}
return true
}
let terminalReport = recoverSessionOutputs(for: application)
Expand Down Expand Up @@ -454,7 +477,7 @@
if let terminalReport {
AppLogger.shared
.warnUnlessQuietTest(
"[SessionRuntime] Session ended: state=\(terminalReport.state) failure=\(terminalReport.failure ?? "none") retirementReason=\(terminalReport.retirementReason ?? "none") at=\(terminalReport.timestamp)"

Check warning on line 480 in Sources/KeyPathAppKit/Managers/ServiceLifecycleCoordinator+SessionRuntime.swift

View workflow job for this annotation

GitHub Actions / code-quality

Line should be 200 characters or less; currently it has 220 characters (line_length)
)
} else {
AppLogger.shared.warnUnlessQuietTest("[SessionRuntime] Session ended without a terminal report")
Expand Down Expand Up @@ -635,7 +658,7 @@
case SessionCapsRuntimeSupport.keyboardDisconnectedReason:
return "The keyboard chosen for Caps Lock was disconnected. Choose a keyboard again in Settings > General."
case SessionCapsRuntimeSupport.ownershipLostReason:
return "KeyPath could no longer verify its Caps Lock mapping on the chosen keyboard, so it stopped and restored it. If another app changed Caps Lock, or a second identical keyboard is connected, fix that, then quit and reopen KeyPath."

Check warning on line 661 in Sources/KeyPathAppKit/Managers/ServiceLifecycleCoordinator+SessionRuntime.swift

View workflow job for this annotation

GitHub Actions / code-quality

Line should be 200 characters or less; currently it has 247 characters (line_length)
case "tap-disabled-by-timeout":
return "macOS stopped keyboard capture because it timed out. Quit and reopen KeyPath to recover."
default:
Expand Down Expand Up @@ -748,7 +771,14 @@
// Always say explicitly; the worker must not fall back to a saved
// selection this launch declined.
var passed = SessionCapsRuntimeSupport.noSelection
if let selection = try await chooseCapsSelection() {
if !SessionCapsRuntimeSupport.selectedKeyboardOnly(),
let usage = await chooseAllCapsIntermediate()
{
passed = SessionCapsRuntimeSupport.allKeyboardLaunchValue(intermediateUsage: usage)
sessionCapsIntermediate = usage
} else if SessionCapsRuntimeSupport.selectedKeyboardOnly(),
let selection = try await chooseCapsSelection()
{
passed = try String(decoding: JSONEncoder().encode(selection), as: UTF8.self)
sessionCapsIntermediate = selection.intermediateUsage
}
Expand Down Expand Up @@ -784,6 +814,38 @@
/// mapped, a keyboard shortcut, or a new rule since the last start moves
/// the reservation to the next free key, saved so Settings shows it.
/// nil when the profile does not need managed Caps.
private func chooseAllCapsIntermediate() async -> UInt32? {
let path = KeyPathConstants.Config.mainConfigPath
guard !UserDefaults.standard.bool(forKey: SessionCapsRuntimeSupport.automaticSetupDisabledKey) else {
let admission = SessionCapsRuntimeSupport.admit(configPath: path, runtimeHost: .current())
if admission.capsPaused {
sessionCapsPaused = Self.capsPausedMessage(disconnected: nil, setupOff: true)
}
return nil
}
let occupied: Set<UInt32>
do {
occupied = try await Task.detached { try SessionCapsRuntimeSupport.occupiedIntermediates() }.value
} catch {
AppLogger.shared.warn("[SessionRuntime] Could not read keyboard mappings for the Caps intermediate: \(error)")
occupied = []
}
let admission = SessionCapsRuntimeSupport.admit(configPath: path, runtimeHost: .current(),
requireSelection: false, occupied: occupied)
onAdmissionEnvironment?(.init(intermediateUsage: admission.managedCaps ? admission.intermediateUsage : nil,
occupied: occupied, foreignCapsOwner: nil))
guard admission.managedCaps else {
if case let .invalid(reason) = admission.result {
sessionCapsPaused = "Caps Lock rules are off: \(reason). Your other rules keep working."
}
return nil
}
UserDefaults.standard.set(admission.intermediateUsage, forKey: SessionCapsRuntimeSupport.allIntermediateKey)
return admission.intermediateUsage
}

/// The advanced single-keyboard path retains exact-device consent and
/// rechecks its intermediate before each worker launch.
private func chooseCapsSelection() async throws -> SessionCapsSelection? {
let path = KeyPathConstants.Config.mainConfigPath
let needed = SessionCapsRuntimeSupport.admit(configPath: path, runtimeHost: .current())
Expand Down Expand Up @@ -862,6 +924,19 @@
intended && managedCapsGeneration != nonce
}

nonisolated static func capsCoverageWarning(_ coverage: SessionRuntimeReport.CapsCoverage) -> String? {
if coverage.inventoryUnavailable == true {
return "KeyPath cannot check newly connected keyboards for Caps Lock rules right now. Existing keyboard rules keep working; check Settings > General."
}
if coverage.covered < coverage.eligible {
return "Caps Lock rules work on \(coverage.covered) of \(coverage.eligible) selected keyboards. Check keyboard details in Settings > General. Your other rules keep working."
}
if coverage.withheld > 0 {
return "Caps Lock rules could not be verified on \(coverage.withheld) connected keyboard\(coverage.withheld == 1 ? "" : "s"). Check Settings > General. Your other rules keep working."
}
return nil
}

/// While a disconnected keyboard's choice is kept (`displaced`), check
/// every two seconds for it at the same port and restart onto it. Polls
/// hidutil only in that state; any other start or a new boot ends it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ final class ServiceLifecycleCoordinator {
}
#endif
guard !TestEnvironment.isTestHostProcess,
SessionCapsRuntimeSupport.selectedKeyboardOnly(),
SessionCapsRuntimeSupport.validate(configPath: KeyPathConstants.Config.mainConfigPath,
runtimeHost: .current(), requireSelection: false).managedCaps,
!UserDefaults.standard.bool(forKey: SessionCapsRuntimeSupport.automaticSetupDisabledKey) else { return }
Expand Down
Loading
Loading