diff --git a/Sources/KeyPathAppKit/Core/Contracts/EventTapping.swift b/Sources/KeyPathAppKit/Core/Contracts/EventTapping.swift deleted file mode 100644 index 766dbc4e6..000000000 --- a/Sources/KeyPathAppKit/Core/Contracts/EventTapping.swift +++ /dev/null @@ -1,106 +0,0 @@ -import CoreGraphics -import Foundation - -/// A handle representing an installed event tap that can be used for cleanup. -public struct TapHandle { - let machPort: CFMachPort - let runLoopSource: CFRunLoopSource? -} - -/// Protocol defining event tap installation and management capabilities. -/// -/// This protocol provides a clean interface for installing, managing, and removing -/// CGEvent taps within KeyPath. It abstracts the low-level CoreGraphics event tap -/// mechanics behind a service-oriented interface. -/// -/// ## Usage -/// -/// Event tap services should implement this protocol to provide consistent -/// tap lifecycle management: -/// -/// ```swift -/// class KeyboardTapService: EventTapping { -/// private var currentTap: TapHandle? -/// -/// var isInstalled: Bool { -/// return currentTap != nil -/// } -/// -/// func install() throws -> TapHandle { -/// guard !isInstalled else { throw TapError.alreadyInstalled } -/// -/// // Create CGEvent tap -/// let tap = CGEvent.tapCreate(...) -/// let source = CFMachPortCreateRunLoopSource(...) -/// -/// let handle = TapHandle(machPort: tap, runLoopSource: source) -/// currentTap = handle -/// return handle -/// } -/// } -/// ``` -/// -/// ## Thread Safety -/// -/// Implementations should ensure thread-safe access to tap state and handle -/// concurrent install/uninstall operations appropriately. -/// -/// ## Permissions -/// -/// Event tap installation requires appropriate system permissions (Accessibility -/// or Input Monitoring). Implementations should provide clear error messages -/// when permissions are insufficient. -@MainActor -protocol EventTapping { - /// Indicates whether an event tap is currently installed and active. - /// - /// This property should accurately reflect the tap's operational state. - var isInstalled: Bool { get } - - /// Installs and activates an event tap. - /// - /// Creates the necessary CGEvent tap, configures run loop integration, - /// and returns a handle for later cleanup. - /// - /// - Returns: A `TapHandle` representing the installed tap. - /// - Throws: `TapError` if installation fails or tap already exists. - func install() throws -> TapHandle - - /// Removes and deactivates the currently installed event tap. - /// - /// Cleans up CGEvent tap resources, removes run loop sources, and - /// ensures proper resource deallocation. - /// - /// This method should be idempotent - calling it when no tap is installed - /// should not cause errors. - func uninstall() -} - -/// Extension providing convenience methods for tap management. -extension EventTapping { - /// Reinstalls the event tap by removing the old one and installing a new one. - /// - /// This is useful for recovering from tap failures or updating tap configuration. - /// - /// - Returns: A new `TapHandle` representing the reinstalled tap. - /// - Throws: `TapError` if reinstallation fails. - func reinstall() throws -> TapHandle { - uninstall() - return try install() - } - - /// Ensures the event tap is in the desired installation state. - /// - /// - Parameter shouldBeInstalled: The desired installation state. - /// - Returns: A `TapHandle` if a tap was installed, nil otherwise. - /// - Throws: `TapError` if state change fails. - func ensureState(installed shouldBeInstalled: Bool) throws -> TapHandle? { - if shouldBeInstalled, !isInstalled { - return try install() - } else if !shouldBeInstalled, isInstalled { - uninstall() - return nil - } - return nil - } -} diff --git a/Sources/KeyPathAppKit/Core/Contracts/LifecycleControlling.swift b/Sources/KeyPathAppKit/Core/Contracts/LifecycleControlling.swift deleted file mode 100644 index b7e979eb9..000000000 --- a/Sources/KeyPathAppKit/Core/Contracts/LifecycleControlling.swift +++ /dev/null @@ -1,82 +0,0 @@ -import Foundation - -/// Protocol defining lifecycle management capabilities for system services. -/// -/// This protocol establishes a consistent interface for starting, stopping, and monitoring -/// the state of services within KeyPath. It provides the foundation for clean separation -/// between service coordination and implementation details. -/// -/// ## Usage -/// -/// Services implementing this protocol should handle their lifecycle operations asynchronously -/// and maintain accurate state reporting through the `isRunning` property. -/// -/// ```swift -/// class MyService: LifecycleControlling { -/// @Published private(set) var isRunning = false -/// -/// func start() async throws { -/// // Service startup logic -/// isRunning = true -/// } -/// -/// func stop() async throws { -/// // Service shutdown logic -/// isRunning = false -/// } -/// } -/// ``` -/// -/// ## Thread Safety -/// -/// Implementations must ensure thread-safe access to the `isRunning` property and handle -/// concurrent start/stop operations gracefully. -protocol LifecycleControlling { - /// Indicates whether the service is currently running. - /// - /// This property should accurately reflect the service's operational state and be - /// updated atomically during state transitions. - var isRunning: Bool { get } - - /// Starts the service. - /// - /// This method should be idempotent - calling it multiple times when already running - /// should not cause errors or undesired side effects. - /// - /// - Throws: `LifecycleError` or service-specific errors if startup fails. - func start() async throws - - /// Stops the service gracefully. - /// - /// This method should handle cleanup operations and ensure the service shuts down - /// in a clean state. Like `start()`, this should be idempotent. - /// - /// - Throws: `LifecycleError` or service-specific errors if shutdown fails. - func stop() async throws -} - -/// Extension providing default implementations and convenience methods. -extension LifecycleControlling { - /// Restarts the service by stopping and then starting it. - /// - /// This is a convenience method that combines stop and start operations - /// with appropriate error handling. - /// - /// - Throws: Errors from either the stop or start operations. - func restart() async throws { - try await stop() - try await start() - } - - /// Ensures the service is in the desired running state. - /// - /// - Parameter shouldBeRunning: The desired running state. - /// - Throws: Errors from start or stop operations if state change fails. - func ensureState(running shouldBeRunning: Bool) async throws { - if shouldBeRunning, !isRunning { - try await start() - } else if !shouldBeRunning, isRunning { - try await stop() - } - } -} diff --git a/Sources/KeyPathAppKit/Core/Contracts/Logging.swift b/Sources/KeyPathAppKit/Core/Contracts/Logging.swift deleted file mode 100644 index b6cbde3bc..000000000 --- a/Sources/KeyPathAppKit/Core/Contracts/Logging.swift +++ /dev/null @@ -1,244 +0,0 @@ -import Foundation - -/// Protocol defining logging capabilities for KeyPath components. -/// -/// This protocol establishes a consistent interface for logging throughout -/// the KeyPath application. It provides the foundation for clean separation -/// between logging functionality and business logic. -/// -/// ## Usage -/// -/// Logging services implement this protocol to provide different logging -/// backends and behaviors: -/// -/// ```swift -/// class FileLogger: Logging { -/// func log(_ message: String, level: LogLevel, category: String?) { -/// let timestamp = dateFormatter.string(from: Date()) -/// let formattedMessage = "[\(timestamp)] \(level.prefix) \(category ?? "General"): \(message)" -/// writeToFile(formattedMessage) -/// } -/// } -/// ``` -/// -/// ## Thread Safety -/// -/// All methods should be thread-safe as logging can occur from any queue -/// throughout the application. -protocol Logging { - /// Logs a message with the specified level and optional category. - /// - /// This is the core logging method that all other convenience methods - /// build upon. Implementations should handle formatting, buffering, - /// and output routing. - /// - /// - Parameters: - /// - message: The message to log. - /// - level: The severity level of the message. - /// - category: Optional category for organizing log messages. - func log(_ message: String, level: LogLevel, category: String?) - - /// Flushes any buffered log messages to their destination. - /// - /// This method ensures that buffered log messages are written to - /// their final destination (file, console, etc.) immediately. - func flush() - - /// Configures the logger with specific settings. - /// - /// This method allows runtime configuration of logging behavior - /// such as log levels, output destinations, and formatting options. - /// - /// - Parameter config: The logging configuration to apply. - func configure(_ config: LoggingConfiguration) -} - -/// Extension providing convenience methods and default behaviors. -extension Logging { - /// Logs a debug message. - /// - /// Debug messages are typically used for detailed diagnostic information - /// that is only relevant during development or troubleshooting. - /// - /// - Parameters: - /// - message: The debug message to log. - /// - category: Optional category for organizing messages. - func debug(_ message: String, category: String? = nil) { - log(message, level: .debug, category: category) - } - - /// Logs an informational message. - /// - /// Info messages provide general information about application operation - /// and are typically shown in production logs. - /// - /// - Parameters: - /// - message: The informational message to log. - /// - category: Optional category for organizing messages. - func info(_ message: String, category: String? = nil) { - log(message, level: .info, category: category) - } - - /// Logs a warning message. - /// - /// Warning messages indicate potential issues that don't prevent - /// operation but should be noted for investigation. - /// - /// - Parameters: - /// - message: The warning message to log. - /// - category: Optional category for organizing messages. - func warning(_ message: String, category: String? = nil) { - log(message, level: .warning, category: category) - } - - /// Logs an error message. - /// - /// Error messages indicate failures or issues that impact application - /// functionality and require attention. - /// - /// - Parameters: - /// - message: The error message to log. - /// - category: Optional category for organizing messages. - func error(_ message: String, category: String? = nil) { - log(message, level: .error, category: category) - } - - /// Logs a critical/fatal message. - /// - /// Critical messages indicate severe issues that may cause application - /// termination or major functionality loss. - /// - /// - Parameters: - /// - message: The critical message to log. - /// - category: Optional category for organizing messages. - func critical(_ message: String, category: String? = nil) { - log(message, level: .critical, category: category) - } - - /// Logs a message using the current AppLogger format (for compatibility). - /// - /// This method provides backward compatibility with existing AppLogger.shared.log() calls. - /// Messages are logged at info level with automatic formatting preservation. - /// - /// - Parameter message: The message in AppLogger format (with emojis and tags). - func log(_ message: String) { - log(message, level: .info, category: nil) - } -} - -/// Log severity levels. -enum LogLevel: Int, CaseIterable, Sendable { - case debug = 0 - case info = 1 - case warning = 2 - case error = 3 - case critical = 4 - - /// Display name for the log level. - var name: String { - switch self { - case .debug: "DEBUG" - case .info: "INFO" - case .warning: "WARNING" - case .error: "ERROR" - case .critical: "CRITICAL" - } - } - - /// Short prefix for log formatting. - var prefix: String { - switch self { - case .debug: "๐Ÿ›" - case .info: "โ„น๏ธ" - case .warning: "โš ๏ธ" - case .error: "โŒ" - case .critical: "๐Ÿ”ฅ" - } - } - - /// Whether this level should be logged based on minimum level. - func shouldLog(minimumLevel: LogLevel) -> Bool { - rawValue >= minimumLevel.rawValue - } -} - -/// Configuration for logging behavior. -struct LoggingConfiguration: Sendable { - let minimumLevel: LogLevel - let enableFileLogging: Bool - let enableConsoleLogging: Bool - let maxFileSize: Int - let maxFiles: Int - let flushInterval: TimeInterval - let dateFormat: String - - /// Default configuration matching AppLogger behavior. - static let `default` = LoggingConfiguration( - minimumLevel: .debug, - enableFileLogging: true, - enableConsoleLogging: true, - maxFileSize: 5 * 1024 * 1024, // 5MB - maxFiles: 3, - flushInterval: 5.0, - dateFormat: "yyyy-MM-dd HH:mm:ss.SSS" - ) - - /// Configuration for production use (less verbose). - static let production = LoggingConfiguration( - minimumLevel: .info, - enableFileLogging: true, - enableConsoleLogging: false, - maxFileSize: 10 * 1024 * 1024, // 10MB - maxFiles: 5, - flushInterval: 10.0, - dateFormat: "yyyy-MM-dd HH:mm:ss" - ) - - /// Configuration for development (most verbose). - static let development = LoggingConfiguration( - minimumLevel: .debug, - enableFileLogging: true, - enableConsoleLogging: true, - maxFileSize: 50 * 1024 * 1024, // 50MB - maxFiles: 2, - flushInterval: 1.0, - dateFormat: "HH:mm:ss.SSS" - ) -} - -/// Output destinations for log messages. -enum LogDestination: Sendable { - case file(String) - case console - case systemLog - case custom(String) - - var identifier: String { - switch self { - case let .file(path): "file:\(path)" - case .console: "console" - case .systemLog: "system" - case let .custom(name): "custom:\(name)" - } - } -} - -/// Specialized logging for component-specific needs. -protocol ComponentLogging: Logging { - /// The component name for this logger. - var componentName: String { get } - - /// Logs a message with the component name automatically added. - /// - /// - Parameters: - /// - message: The message to log. - /// - level: The log level. - func logForComponent(_ message: String, level: LogLevel) -} - -/// Default implementation for component logging. -extension ComponentLogging { - func logForComponent(_ message: String, level: LogLevel) { - log(message, level: level, category: componentName) - } -} diff --git a/Sources/KeyPathAppKit/Core/Contracts/PermissionChecking.swift b/Sources/KeyPathAppKit/Core/Contracts/PermissionChecking.swift deleted file mode 100644 index 2ab2b0b18..000000000 --- a/Sources/KeyPathAppKit/Core/Contracts/PermissionChecking.swift +++ /dev/null @@ -1,276 +0,0 @@ -import Foundation - -/// Protocol defining permission checking and management capabilities. -/// -/// This protocol establishes a consistent interface for checking system permissions -/// required by KeyPath components. It provides the foundation for clean separation -/// between permission logic and business functionality. -/// -/// ## Usage -/// -/// Permission services implement this protocol to provide centralized -/// permission management: -/// -/// ```swift -/// class SystemPermissionChecker: PermissionChecking { -/// func checkPermission(_ type: SystemPermissionType, for subject: PermissionSubject) async -> PermissionStatus { -/// switch type { -/// case .accessibility: -/// return checkAccessibilityPermission(for: subject) -/// case .inputMonitoring: -/// return checkInputMonitoringPermission(for: subject) -/// } -/// } -/// } -/// ``` -/// -/// ## Thread Safety -/// -/// All methods are async and implementations should ensure thread-safe -/// permission checking across concurrent requests. -protocol PermissionChecking { - /// Checks the status of a specific permission for a given subject. - /// - /// This method performs the actual permission check using the appropriate - /// system APIs and returns a detailed status result. - /// - /// - Parameters: - /// - type: The type of permission to check. - /// - subject: The subject (app, binary, etc.) to check permissions for. - /// - /// - Returns: The current permission status with confidence level. - func checkPermission(_ type: SystemPermissionType, for subject: PermissionSubject) async - -> PermissionStatus - - /// Checks multiple permissions at once for a given subject. - /// - /// This method provides an efficient way to check multiple permissions - /// and returns a comprehensive permission set. - /// - /// - Parameters: - /// - types: The types of permissions to check. - /// - subject: The subject to check permissions for. - /// - /// - Returns: A set containing all requested permission statuses. - func checkPermissions(_ types: [SystemPermissionType], for subject: PermissionSubject) async - -> PermissionSet - - /// Gets a snapshot of system-wide permission status. - /// - /// This method provides a comprehensive view of all relevant permissions - /// across all subjects, useful for diagnostic and UI purposes. - /// - /// - Returns: A snapshot containing system-wide permission information. - func getSystemSnapshot() async -> SystemPermissionSnapshot -} - -/// Extension providing convenience methods and default implementations. -extension PermissionChecking { - /// Checks if a subject has all required permissions. - /// - /// - Parameters: - /// - types: The required permission types. - /// - subject: The subject to check. - /// - /// - Returns: `true` if all permissions are granted, `false` otherwise. - func hasAllPermissions(_ types: [SystemPermissionType], for subject: PermissionSubject) async - -> Bool - { - let permissions = await checkPermissions(types, for: subject) - return permissions.hasAllGranted - } - - /// Checks if the system is ready (all critical permissions granted). - /// - /// This convenience method checks the most common permission combination - /// needed for KeyPath to function properly. - /// - /// - Returns: `true` if system is ready, `false` otherwise. - func isSystemReady() async -> Bool { - let snapshot = await getSystemSnapshot() - return snapshot.isSystemReady - } - - /// Gets a user-friendly message for the first blocking permission issue. - /// - /// - Returns: A descriptive message about permission issues, or `nil` if none. - func getBlockingIssueMessage() async -> String? { - let snapshot = await getSystemSnapshot() - return snapshot.blockingIssue - } -} - -/// The types of permissions that can be checked. -enum SystemPermissionType: CaseIterable, Sendable { - case accessibility - case inputMonitoring - - var displayName: String { - switch self { - case .accessibility: - "Accessibility" - case .inputMonitoring: - "Input Monitoring" - } - } - - var systemSettingsPath: String { - switch self { - case .accessibility: - "Privacy & Security > Accessibility" - case .inputMonitoring: - "Privacy & Security > Input Monitoring" - } - } -} - -/// Subjects that can have permissions checked. -enum PermissionSubject: Sendable { - case currentApp - case binary(path: String) - case bundleIdentifier(String) - - var displayName: String { - switch self { - case .currentApp: - return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? "KeyPath" - case let .binary(path): - return URL(fileURLWithPath: path).lastPathComponent - case let .bundleIdentifier(id): - return id - } - } -} - -/// The status of a permission check. -enum PermissionStatus: Sendable { - case granted - case denied - case unknown - case error(String) - - var isGranted: Bool { - if case .granted = self { return true } - return false - } - - var isBlocking: Bool { - switch self { - case .denied, .error: - true - case .granted, .unknown: - false - } - } - - var description: String { - switch self { - case .granted: - "Granted" - case .denied: - "Denied" - case .unknown: - "Unknown" - case let .error(message): - "Error: \(message)" - } - } -} - -/// Confidence level in a permission check result. -enum PermissionConfidence: Sendable { - case high - case medium - case low - - var description: String { - switch self { - case .high: "High" - case .medium: "Medium" - case .low: "Low" - } - } -} - -/// A set of permission statuses for a specific subject. -struct PermissionSet: Sendable { - let subject: PermissionSubject - let permissions: [SystemPermissionType: PermissionStatus] - let confidence: PermissionConfidence - let source: String - let timestamp: Date - - /// Whether all checked permissions are granted. - var hasAllGranted: Bool { - permissions.values.allSatisfy(\.isGranted) - } - - /// Whether any permission is blocking. - var hasBlockingIssues: Bool { - permissions.values.contains { $0.isBlocking } - } - - /// Get the status for a specific permission type. - func status(for type: SystemPermissionType) -> PermissionStatus? { - permissions[type] - } -} - -/// A system-wide snapshot of permission statuses. -struct SystemPermissionSnapshot: Sendable { - let keyPathPermissions: PermissionSet - let kanataPermissions: PermissionSet - let timestamp: Date - - /// Whether the entire system has all required permissions. - var isSystemReady: Bool { - keyPathPermissions.hasAllGranted && kanataPermissions.hasAllGranted - } - - /// The first blocking permission issue found, if any. - var blockingIssue: String? { - // Check KeyPath first (needed for UI) - if let accessibility = keyPathPermissions.status(for: .accessibility), - accessibility.isBlocking - { - return - "KeyPath needs Accessibility permission - enable in System Settings > \(SystemPermissionType.accessibility.systemSettingsPath)" - } - - if let inputMonitoring = keyPathPermissions.status(for: .inputMonitoring), - inputMonitoring.isBlocking - { - return - "KeyPath needs Input Monitoring permission - enable in System Settings > \(SystemPermissionType.inputMonitoring.systemSettingsPath)" - } - - // Check Kanata - if kanataPermissions.hasBlockingIssues { - return "Kanata needs permissions - use the Installation Wizard to grant required access" - } - - return nil - } - - /// Diagnostic information for troubleshooting. - var diagnosticSummary: String { - let age = String(format: "%.3f", Date().timeIntervalSince(timestamp)) - return """ - Permission System Snapshot (\(age)s ago) - - KeyPath [\(keyPathPermissions.source), \(keyPathPermissions.confidence.description)]: - \(formatPermissions(keyPathPermissions)) - - Kanata [\(kanataPermissions.source), \(kanataPermissions.confidence.description)]: - \(formatPermissions(kanataPermissions)) - - System Ready: \(isSystemReady) - """ - } - - private func formatPermissions(_ set: PermissionSet) -> String { - set.permissions.map { type, status in - " โ€ข \(type.displayName): \(status.description)" - }.joined(separator: "\n") - } -} diff --git a/Sources/KeyPathAppKit/Services/AI/ConfigRepairService.swift b/Sources/KeyPathAppKit/Services/AI/ConfigRepairService.swift index e8b5dd55a..bcc4b4862 100644 --- a/Sources/KeyPathAppKit/Services/AI/ConfigRepairService.swift +++ b/Sources/KeyPathAppKit/Services/AI/ConfigRepairService.swift @@ -10,13 +10,3 @@ public protocol ConfigRepairService: Sendable { /// - Returns: A corrected configuration string func repairConfig(config: String, errors: [String], mappings: [KeyMapping]) async throws -> String } - -/// Mock implementation for tests or when AI is disabled -public struct MockConfigRepairService: ConfigRepairService { - public init() {} - - public func repairConfig(config: String, errors _: [String], mappings _: [KeyMapping]) async throws -> String { - // Just return the original config to simulate inability to repair without AI - config - } -} diff --git a/Sources/KeyPathAppKit/Services/KeyboardCapture/EventTapController.swift b/Sources/KeyPathAppKit/Services/KeyboardCapture/EventTapController.swift deleted file mode 100644 index 0d7c9de0e..000000000 --- a/Sources/KeyPathAppKit/Services/KeyboardCapture/EventTapController.swift +++ /dev/null @@ -1,86 +0,0 @@ -import ApplicationServices -import Foundation -import KeyPathCore - -/// Controller for CGEvent/IOHID concerns. Provides a narrow surface for -/// installing a passive event tap and delegating emergency monitoring via -/// KeyboardCapture. -@MainActor -final class EventTapController: EventTapping { - private var currentTap: TapHandle? - private var keyboardCapture: KeyboardCapture? - - init(keyboardCapture: KeyboardCapture? = nil) { - self.keyboardCapture = keyboardCapture - } - - var isInstalled: Bool { - currentTap != nil - } - - /// Install a listen-only CGEvent tap for keyDown events. - func install() throws -> TapHandle { - guard currentTap == nil else { throw TapError.alreadyInstalled } - - // Avoid creating taps in test environments - if TestEnvironment.isRunningTests { - throw TapError.installationFailed(reason: "Test environment - event taps disabled") - } - - let eventMask = (1 << CGEventType.keyDown.rawValue) - guard - let machPort = CGEvent.tapCreate( - tap: .cgSessionEventTap, - place: .headInsertEventTap, - options: .listenOnly, - eventsOfInterest: CGEventMask(eventMask), - callback: { _, _, event, _ in - // Always pass event through - Unmanaged.passUnretained(event) - }, - userInfo: nil - ) - else { - throw TapError.installationFailed(reason: "CGEvent.tapCreate returned nil") - } - - let source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, machPort, 0) - CFRunLoopAddSource(CFRunLoopGetCurrent(), source, .commonModes) - CGEvent.tapEnable(tap: machPort, enable: true) - - let handle = TapHandle(machPort: machPort, runLoopSource: source) - currentTap = handle - return handle - } - - func uninstall() { - guard let handle = currentTap else { return } - if let source = handle.runLoopSource { - CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, .commonModes) - } - CFMachPortInvalidate(handle.machPort) - currentTap = nil - } - - // MARK: - Emergency Monitoring Convenience - - func startEmergencyMonitoring(_ callback: @escaping () -> Void) { - ensureKeyboardCapture() - keyboardCapture?.startEmergencyMonitoring(callback: callback) - } - - func stopEmergencyMonitoring() { - keyboardCapture?.stopEmergencyMonitoring() - } - - private func ensureKeyboardCapture() { - if keyboardCapture == nil { - keyboardCapture = KeyboardCapture() - } - } -} - -enum TapError: Error { - case alreadyInstalled - case installationFailed(reason: String) -} diff --git a/Sources/KeyPathAppKit/Services/Monitoring/SafetyAlertPresenter.swift b/Sources/KeyPathAppKit/Services/Monitoring/SafetyAlertPresenter.swift deleted file mode 100644 index cc0b2144f..000000000 --- a/Sources/KeyPathAppKit/Services/Monitoring/SafetyAlertPresenter.swift +++ /dev/null @@ -1,25 +0,0 @@ -import AppKit -import Foundation -import KeyPathCore - -enum SafetyAlertPresenter { - @MainActor - static func presentSafetyTimeoutAlert() { - if TestEnvironment.isRunningTests { - AppLogger.shared.debug("๐Ÿงช [Safety] Suppressing NSAlert in test environment") - return - } - - let alert = NSAlert() - alert.messageText = "Safety Timeout Activated" - alert.informativeText = """ - KeyPath automatically stopped the keyboard remapping service after 30 seconds as a safety precaution. - - If the service was working correctly, you can restart it from the main app window. - - If you experienced keyboard issues, this timeout prevented them from continuing. - """ - alert.alertStyle = .informational - alert.runModal() - } -} diff --git a/Sources/KeyPathAppKit/Services/System/ProcessKiller.swift b/Sources/KeyPathAppKit/Services/System/ProcessKiller.swift deleted file mode 100644 index 38c132e54..000000000 --- a/Sources/KeyPathAppKit/Services/System/ProcessKiller.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import KeyPathCore - -enum ProcessKiller { - static func kill(pid: Int) async { - let task = Process() - task.executableURL = URL(fileURLWithPath: "/usr/bin/sudo") - task.arguments = ["kill", "-TERM", String(pid)] - - do { - try task.run() - task.waitUntilExit() - - if task.terminationStatus == 0 { - AppLogger.shared.info("โœ… [Kill] Successfully killed process \(pid)") - } else { - AppLogger.shared.warn("โš ๏ธ [Kill] Failed to kill process \(pid) (may have already exited)") - } - } catch { - AppLogger.shared.error("โŒ [Kill] Exception killing process \(pid): \(error)") - } - } -} diff --git a/Sources/KeyPathAppKit/Services/VimIntegration/KindaVimDetector.swift b/Sources/KeyPathAppKit/Services/VimIntegration/KindaVimDetector.swift deleted file mode 100644 index b85691cc4..000000000 --- a/Sources/KeyPathAppKit/Services/VimIntegration/KindaVimDetector.swift +++ /dev/null @@ -1,12 +0,0 @@ -import AppKit - -enum KindaVimDetector { - static let bundleIdentifier = "mo.com.sleeplessmind.kindaVim" - static let downloadURL = URL(string: "https://kindavim.app")! - - static var isInstalled: Bool { - NSWorkspace.shared.urlForApplication( - withBundleIdentifier: bundleIdentifier - ) != nil - } -} diff --git a/Sources/KeyPathAppKit/UI/SimpleLogViewer.swift b/Sources/KeyPathAppKit/UI/SimpleLogViewer.swift deleted file mode 100644 index b9224625f..000000000 --- a/Sources/KeyPathAppKit/UI/SimpleLogViewer.swift +++ /dev/null @@ -1,155 +0,0 @@ -import SwiftUI - -/// Simple log viewer - shows last N lines with refresh and open buttons -struct SimpleLogViewer: View { - let logPath: String - let title: String - let maxLines: Int = 50 - - @State private var logContent: String = "" - @State private var isLoading = false - - var body: some View { - VStack(spacing: 0) { - // Header with actions - HStack { - Text(title) - .font(.headline) - - Spacer() - - // File path (clickable to open in Finder) - Button(action: openInFinder) { - Text(logPath) - .font(.caption.monospaced()) - .foregroundColor(.blue) - .lineLimit(1) - .truncationMode(.middle) - } - .buttonStyle(.plain) - .help("Click to open folder in Finder") - .accessibilityIdentifier("log-viewer-open-folder-button") - .accessibilityLabel("Open log folder in Finder") - - Button(action: refreshLog) { - Label("Refresh", systemImage: "arrow.clockwise") - } - .buttonStyle(.bordered) - .controlSize(.small) - .disabled(isLoading) - .accessibilityIdentifier("log-viewer-refresh-button") - .accessibilityLabel("Refresh log") - - Button(action: openInEditor) { - Label("Open Full Log", systemImage: "arrow.up.forward.app") - } - .buttonStyle(.bordered) - .controlSize(.small) - .accessibilityIdentifier("log-viewer-open-editor-button") - .accessibilityLabel("Open full log in editor") - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(Color(NSColor.controlBackgroundColor)) - - Divider() - - // Log content - ScrollView { - if isLoading { - HStack { - ProgressView() - .scaleEffect(0.7) - Text("Loading...") - .foregroundColor(.secondary) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding() - } else { - Text( - logContent.isEmpty - ? "No logs yet...\nLogs will appear here when KeyPath runs." : logContent - ) - .font(.system(.caption, design: .monospaced)) - .foregroundColor(logContent.isEmpty ? .secondary : .primary) - .textSelection(.enabled) - .frame(maxWidth: .infinity, alignment: .topLeading) - .padding(12) - } - } - .background(Color(NSColor.textBackgroundColor)) - } - .onAppear { - loadLog() - } - } - - private func loadLog() { - isLoading = true - Task { - let content = await loadLogContent() - await MainActor.run { - logContent = content - isLoading = false - } - } - } - - private func refreshLog() { - loadLog() - } - - private func loadLogContent() async -> String { - // Run file reading off main thread - await Task.detached { - guard let content = try? String(contentsOfFile: logPath, encoding: .utf8) else { - return "Log file not found at: \(logPath)" - } - - let lines = content.split(separator: "\n", omittingEmptySubsequences: false) - - // Show last N lines - if lines.count > maxLines { - let recentLines = lines.suffix(maxLines) - return "... (showing last \(maxLines) lines) ...\n\n" + recentLines.joined(separator: "\n") - } else { - return content - } - }.value - } - - private func openInFinder() { - // Open the containing folder in Finder and select the log file - let folderPath = (logPath as NSString).deletingLastPathComponent - NSWorkspace.shared.selectFile(logPath, inFileViewerRootedAtPath: folderPath) - } - - private func openInEditor() { - // Try common editors in order of preference - let editors = [ - // Zed (fast, modern) - ("/usr/local/bin/zed", [logPath]), - ("/opt/homebrew/bin/zed", [logPath]), - // VSCode - ("/usr/local/bin/code", [logPath]), - ("/opt/homebrew/bin/code", [logPath]), - // Sublime - ("/usr/local/bin/subl", [logPath]), - // Vim/Neovim in terminal - ("/usr/bin/open", ["-a", "Terminal", logPath]) - ] - - for (editorPath, args) in editors { - if Foundation.FileManager().fileExists(atPath: editorPath) { - let process = Process() - process.executableURL = URL(fileURLWithPath: editorPath) - process.arguments = args - try? process.run() - return - } - } - - // Fallback: open with default text editor - NSWorkspace.shared.open(URL(fileURLWithPath: logPath)) - } -} diff --git a/Sources/KeyPathCore/TestEnvironment.swift b/Sources/KeyPathCore/TestEnvironment.swift index 7590d6e73..045c18be5 100644 --- a/Sources/KeyPathCore/TestEnvironment.swift +++ b/Sources/KeyPathCore/TestEnvironment.swift @@ -195,58 +195,3 @@ public enum TestEnvironment { } } } - -/// Mock data providers for test environment -public enum MockSystemData { - /// Mock launchctl service status - public static func mockServiceStatus(loaded: Bool = true, running: Bool = false) -> String { - if loaded, running { - """ - { - "LimitLoadToSessionType" = "System"; - "Label" = "com.keypath.kanata"; - "OnDemand" = true; - "LastExitStatus" = 0; - "PID" = 12345; - "Program" = "/Applications/KeyPath.app/Contents/Library/KeyPath/kanata"; - "ProgramArguments" = ( - "/Applications/KeyPath.app/Contents/Library/KeyPath/kanata", - "--cfg", - "/Users/test/.config/keypath/keypath.kbd", - "--port", - "37000" - ); - } - """ - } else if loaded { - """ - { - "LimitLoadToSessionType" = "System"; - "Label" = "com.keypath.kanata"; - "OnDemand" = true; - "LastExitStatus" = 0; - "Program" = "/Applications/KeyPath.app/Contents/Library/KeyPath/kanata"; - "ProgramArguments" = ( - "/Applications/KeyPath.app/Contents/Library/KeyPath/kanata", - "--cfg", - "/Users/test/.config/keypath/keypath.kbd", - "--port", - "37000" - ); - } - """ - } else { - "launchctl: couldn't find service" - } - } - - /// Mock file system operations - public static let mockFileExists = true - public static let mockDirectoryExists = true - - /// Mock process list (no kanata processes) - public static let mockProcessList = "" - - /// Mock permission status - public static let mockHasPermissions = true -} diff --git a/Sources/KeyPathInstallationWizard/UI/Components/WizardPreflightView.swift b/Sources/KeyPathInstallationWizard/UI/Components/WizardPreflightView.swift deleted file mode 100644 index 996cf8ff0..000000000 --- a/Sources/KeyPathInstallationWizard/UI/Components/WizardPreflightView.swift +++ /dev/null @@ -1,29 +0,0 @@ -import SwiftUI - -/// Compact preflight view shown while initial checks run -public struct WizardPreflightView: View { - @Binding public var progress: Double - - public init(progress: Binding = .constant(0.0)) { - _progress = progress - } - - public var body: some View { - VStack(spacing: 12) { - GeometryReader { geo in - HStack { - Spacer() - ProgressView(value: progress) - .progressViewStyle(.linear) - .scaleEffect(x: 1.0, y: 1.6, anchor: .center) // thicker bar - .animation(.easeInOut(duration: 0.35), value: progress) - .frame(width: geo.size.width * 0.6) // 40% shorter than window - Spacer() - } - } - .frame(height: 18) - } - .padding(.vertical, 16) - .background(WizardDesign.Colors.wizardBackground) // Dark mode-aware background - } -} diff --git a/Sources/KeyPathInstallationWizard/UI/Helpers/DragToAuthorize/DragToAuthorizeProofOfConcept.swift b/Sources/KeyPathInstallationWizard/UI/Helpers/DragToAuthorize/DragToAuthorizeProofOfConcept.swift deleted file mode 100644 index 8b536baf2..000000000 --- a/Sources/KeyPathInstallationWizard/UI/Helpers/DragToAuthorize/DragToAuthorizeProofOfConcept.swift +++ /dev/null @@ -1,26 +0,0 @@ -import AppKit -import KeyPathCore -import SwiftUI - -/// Proof-of-concept entry point for testing drag-to-authorize. -/// Call from a debug button to validate the mechanism works. -/// -/// SUCCESS CRITERIA: Dragging the icon from the floating panel into the -/// System Settings privacy list actually adds kanata-launcher to the list. -@MainActor -public enum DragToAuthorizeProofOfConcept { - /// Launch the test for accessibility permissions. - public static func testAccessibility() { - DragToAuthorizeController.shared.present(for: .accessibility) - } - - /// Launch the test for input monitoring permissions. - public static func testInputMonitoring() { - DragToAuthorizeController.shared.present(for: .inputMonitoring) - } - - /// Dismiss the overlay. - public static func dismiss() { - DragToAuthorizeController.shared.dismiss() - } -}