diff --git a/Sources/KeyPathAppKit/Managers/RuntimeCoordinator.swift b/Sources/KeyPathAppKit/Managers/RuntimeCoordinator.swift index 8d7181cdc..c3151d9bc 100644 --- a/Sources/KeyPathAppKit/Managers/RuntimeCoordinator.swift +++ b/Sources/KeyPathAppKit/Managers/RuntimeCoordinator.swift @@ -589,8 +589,10 @@ public class RuntimeCoordinator: SaveCoordinatorDelegate { await installerEngine.inspectSystem() } - func uninstall(deleteConfig: Bool) async -> InstallerReport { - await installerEngine.uninstall(deleteConfig: deleteConfig, using: privilegeBroker) + func uninstall(deleteConfig: Bool, removeVirtualHID: Bool = false) async -> InstallerReport { + await installerEngine.uninstall( + deleteConfig: deleteConfig, removeVirtualHID: removeVirtualHID, using: privilegeBroker + ) } func runFullRepair(reason: String = "RuntimeCoordinator repair request") async -> InstallerReport { diff --git a/Sources/KeyPathAppKit/UI/Dialogs/UninstallKeyPathDialog.swift b/Sources/KeyPathAppKit/UI/Dialogs/UninstallKeyPathDialog.swift index 7deeb139e..d43df5cb4 100644 --- a/Sources/KeyPathAppKit/UI/Dialogs/UninstallKeyPathDialog.swift +++ b/Sources/KeyPathAppKit/UI/Dialogs/UninstallKeyPathDialog.swift @@ -11,6 +11,7 @@ struct UninstallKeyPathDialog: View { @State private var didSucceed = false @State private var hasScheduledQuit = false @State private var autoQuitWorkItem: DispatchWorkItem? + @State private var removeVirtualHID = false var body: some View { VStack(spacing: 20) { @@ -36,6 +37,20 @@ struct UninstallKeyPathDialog: View { .foregroundColor(.secondary) .multilineTextAlignment(.center) + // Optional: also remove the shared Karabiner virtual keyboard driver. + Toggle(isOn: $removeVirtualHID) { + VStack(alignment: .leading, spacing: 2) { + Text("Also remove the virtual keyboard driver") + .font(.caption) + Text("Other tools (e.g. Karabiner-Elements) may rely on it.") + .font(.caption2) + .foregroundColor(.secondary) + } + } + .toggleStyle(.checkbox) + .disabled(isRunning) + .accessibilityIdentifier("uninstall-remove-vhid-toggle") + // Status if isRunning { ProgressView() @@ -90,7 +105,7 @@ struct UninstallKeyPathDialog: View { lastError = nil } - let report = await kanataManager.uninstall(deleteConfig: false) + let report = await kanataManager.uninstall(deleteConfig: false, removeVirtualHID: removeVirtualHID) await MainActor.run { isRunning = false diff --git a/Sources/KeyPathAppKit/UI/ViewModels/KanataViewModel.swift b/Sources/KeyPathAppKit/UI/ViewModels/KanataViewModel.swift index a5b5d891c..1dee4ce73 100644 --- a/Sources/KeyPathAppKit/UI/ViewModels/KanataViewModel.swift +++ b/Sources/KeyPathAppKit/UI/ViewModels/KanataViewModel.swift @@ -477,8 +477,8 @@ class KanataViewModel { await manager.inspectSystemContext() } - func uninstall(deleteConfig: Bool) async -> InstallerReport { - await manager.uninstall(deleteConfig: deleteConfig) + func uninstall(deleteConfig: Bool, removeVirtualHID: Bool = false) async -> InstallerReport { + await manager.uninstall(deleteConfig: deleteConfig, removeVirtualHID: removeVirtualHID) } func runFullRepair(reason: String) async -> InstallerReport { diff --git a/Sources/KeyPathInstallationWizard/Core/InstallerEngine.swift b/Sources/KeyPathInstallationWizard/Core/InstallerEngine.swift index 28e3d61a6..3ac2c2aaa 100644 --- a/Sources/KeyPathInstallationWizard/Core/InstallerEngine.swift +++ b/Sources/KeyPathInstallationWizard/Core/InstallerEngine.swift @@ -601,10 +601,23 @@ public final class InstallerEngine { return report } - /// Execute uninstall via the existing coordinator (placeholder until uninstall recipes exist) - public func uninstall(deleteConfig: Bool, using broker: PrivilegeBroker) async -> InstallerReport { - AppLogger.shared.log("🗑️ [InstallerEngine] Starting uninstall (deleteConfig: \(deleteConfig))") - _ = broker // Reserved for future privileged uninstall steps + /// Execute uninstall via the existing coordinator. + /// - Parameter removeVirtualHID: when true, also tears down the Karabiner VirtualHID + /// driver. Off by default because the driver is a shared component other tools may use. + public func uninstall(deleteConfig: Bool, removeVirtualHID: Bool = false, using broker: PrivilegeBroker) async -> InstallerReport { + AppLogger.shared.log("🗑️ [InstallerEngine] Starting uninstall (deleteConfig: \(deleteConfig), removeVirtualHID: \(removeVirtualHID))") + + // Remove the VirtualHID driver BEFORE the main teardown: the coordinator uninstall + // self-destructs the privileged helper that this step depends on. Best-effort — a + // VHID failure must not block removing KeyPath itself. + if removeVirtualHID { + do { + try await broker.uninstallVirtualHIDDrivers() + AppLogger.shared.log("🗑️ [InstallerEngine] Virtual HID driver removed") + } catch { + AppLogger.shared.log("⚠️ [InstallerEngine] Virtual HID removal failed (continuing uninstall): \(error)") + } + } let start = Date() guard let coordinator = WizardDependencies.createUninstallCoordinator?() else {