diff --git a/README.md b/README.md index 04228a7..b70be97 100644 --- a/README.md +++ b/README.md @@ -116,12 +116,14 @@ If you can't enable mapping after installation: ### Log Files Location -Logs are stored at: `~/.config/uskey/logs/uskey-YYYY-MM-DD.log` +Logs are stored at: `~/.config/uskey/logs/uskey.log` + +The log file is cleared on each application start. You can view logs by: - **Menu Bar**: Click uskey icon → "View Current Log" - **Finder**: Click uskey icon → "Open Logs Folder" (⌘L) -- **Terminal**: `tail -f ~/.config/uskey/logs/uskey-$(date +%Y-%m-%d).log` +- **Terminal**: `tail -f ~/.config/uskey/logs/uskey.log` ### Common Issues diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index a31436b..ab74eba 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -53,11 +53,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { statusBarController?.setupStatusBar() - Logger.info("Attempting to start event monitoring...") - if eventTapManager!.start() { - Logger.info("Event monitoring started successfully") + if config.enabled { + Logger.info("Auto-enabling event monitoring (enabled: true in config)...") + if eventTapManager!.start() { + Logger.info("Event monitoring started successfully") + } else { + Logger.error("Failed to start event monitoring - check if app has accessibility permissions") + } } else { - Logger.error("Failed to start event monitoring - check if app has accessibility permissions") + Logger.info("Event monitoring not auto-enabled (enabled: false in config)") } Logger.info("Application ready") diff --git a/Sources/Config.swift b/Sources/Config.swift index 9aed78d..1d2047d 100644 --- a/Sources/Config.swift +++ b/Sources/Config.swift @@ -54,10 +54,12 @@ struct MappingConfig: Codable { struct Config: Codable { let log: LogConfig let mapping: MappingConfig + let enabled: Bool - init(log: LogConfig = LogConfig(), mapping: MappingConfig = MappingConfig()) { + init(log: LogConfig = LogConfig(), mapping: MappingConfig = MappingConfig(), enabled: Bool = true) { self.log = log self.mapping = mapping + self.enabled = enabled } static func load(from path: String) throws -> Config { @@ -67,13 +69,21 @@ struct Config: Codable { return try decoder.decode(Config.self, from: data) } + static func save(_ config: Config, to path: String) throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + let data = try encoder.encode(config) + try data.write(to: URL(fileURLWithPath: path)) + } + static func createDefault(at path: String) throws { let defaultConfig = Config( log: LogConfig(level: .info), mapping: MappingConfig(mappings: [ "backslash2backspace": KeyMapping(from: 42, to: 51), "backspace2backslash": KeyMapping(from: 51, to: 42) - ]) + ]), + enabled: true ) let encoder = JSONEncoder() @@ -90,4 +100,4 @@ struct Config: Codable { return configDir.appendingPathComponent("config.json").path } -} +} \ No newline at end of file diff --git a/Sources/EventTapManager.swift b/Sources/EventTapManager.swift index 5fb297a..d5d4abb 100644 --- a/Sources/EventTapManager.swift +++ b/Sources/EventTapManager.swift @@ -38,14 +38,7 @@ class EventTapManager { if manager.keyMapper.hasMappingFor(keyCode: keyCode) { if let mappedKey = manager.keyMapper.getMappedKey(for: keyCode) { Logger.debug("Remapping: \(keyCode) -> \(mappedKey)") - - let newEvent = CGEvent(keyboardEventSource: nil, virtualKey: CGKeyCode(mappedKey), keyDown: type == .keyDown) - if let newEvent = newEvent { - newEvent.flags = event.flags - newEvent.post(tap: .cghidEventTap) - } - - return nil + event.setIntegerValueField(.keyboardEventKeycode, value: mappedKey) } } diff --git a/Sources/Logger.swift b/Sources/Logger.swift index 801f72d..e32f476 100644 --- a/Sources/Logger.swift +++ b/Sources/Logger.swift @@ -9,18 +9,11 @@ struct Logger { let logsDir = getLogsDirectory() try? FileManager.default.createDirectory(at: URL(fileURLWithPath: logsDir), withIntermediateDirectories: true) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd" - let dateString = dateFormatter.string(from: Date()) + logFilePath = "\(logsDir)/uskey.log" - logFilePath = "\(logsDir)/uskey-\(dateString).log" - - if !FileManager.default.fileExists(atPath: logFilePath!) { - FileManager.default.createFile(atPath: logFilePath!, contents: nil) - } + FileManager.default.createFile(atPath: logFilePath!, contents: nil) logFileHandle = FileHandle(forWritingAtPath: logFilePath!) - logFileHandle?.seekToEndOfFile() info("Logger initialized, log file: \(logFilePath!)") } @@ -82,4 +75,4 @@ struct Logger { static func cleanup() { logFileHandle?.closeFile() } -} +} \ No newline at end of file diff --git a/Sources/StatusBarController.swift b/Sources/StatusBarController.swift index 5465950..a88acdd 100644 --- a/Sources/StatusBarController.swift +++ b/Sources/StatusBarController.swift @@ -6,6 +6,7 @@ class StatusBarController { private var statusItem: NSStatusItem? private var eventTapManager: EventTapManager private var config: Config + private var menu: NSMenu? init(eventTapManager: EventTapManager, config: Config) { self.eventTapManager = eventTapManager @@ -17,8 +18,6 @@ class StatusBarController { if let button = statusItem?.button { button.image = NSImage(systemSymbolName: "keyboard", accessibilityDescription: "uskey") - button.action = #selector(statusBarButtonClicked) - button.target = self } updateMenu() @@ -29,10 +28,10 @@ class StatusBarController { } private func updateMenu() { - let menu = NSMenu() + let newMenu = NSMenu() - menu.addItem(NSMenuItem(title: "uskey - Keyboard Remapper", action: nil, keyEquivalent: "")) - menu.addItem(NSMenuItem.separator()) + newMenu.addItem(NSMenuItem(title: "uskey - Keyboard Remapper", action: nil, keyEquivalent: "")) + newMenu.addItem(NSMenuItem.separator()) let isEnabled = eventTapManager.isRunning() let toggleItem = NSMenuItem( @@ -41,49 +40,56 @@ class StatusBarController { keyEquivalent: "" ) toggleItem.target = self - menu.addItem(toggleItem) + newMenu.addItem(toggleItem) - menu.addItem(NSMenuItem.separator()) - menu.addItem(NSMenuItem(title: "Current Mappings:", action: nil, keyEquivalent: "")) + newMenu.addItem(NSMenuItem.separator()) + newMenu.addItem(NSMenuItem(title: "Current Mappings:", action: nil, keyEquivalent: "")) let mappings = config.mapping.getAllMappings() if mappings.isEmpty { let item = NSMenuItem(title: " No mappings configured", action: nil, keyEquivalent: "") item.isEnabled = false - menu.addItem(item) + newMenu.addItem(item) } else { for (from, to) in mappings.sorted(by: { $0.0 < $1.0 }) { let item = NSMenuItem(title: " \(from) → \(to)", action: nil, keyEquivalent: "") item.isEnabled = false - menu.addItem(item) + newMenu.addItem(item) } } - menu.addItem(NSMenuItem.separator()) + newMenu.addItem(NSMenuItem.separator()) let reloadItem = NSMenuItem(title: "Reload Configuration", action: #selector(reloadConfig), keyEquivalent: "r") reloadItem.target = self - menu.addItem(reloadItem) + newMenu.addItem(reloadItem) - menu.addItem(NSMenuItem.separator()) + newMenu.addItem(NSMenuItem.separator()) - let openLogsItem = NSMenuItem(title: "Open Logs Folder", action: #selector(openLogsFolder), keyEquivalent: "l") - openLogsItem.target = self - menu.addItem(openLogsItem) + let openConfigItem = NSMenuItem(title: "Open Config Folder", action: #selector(openConfigFolder), keyEquivalent: "c") + openConfigItem.target = self + newMenu.addItem(openConfigItem) if let _ = Logger.getLogFilePath() { let viewLogItem = NSMenuItem(title: "View Current Log", action: #selector(viewCurrentLog), keyEquivalent: "") viewLogItem.target = self - menu.addItem(viewLogItem) + newMenu.addItem(viewLogItem) } - menu.addItem(NSMenuItem.separator()) + newMenu.addItem(NSMenuItem.separator()) + + let aboutItem = NSMenuItem(title: "⚠️ About Limitations", action: #selector(showLimitations), keyEquivalent: "") + aboutItem.target = self + newMenu.addItem(aboutItem) + + newMenu.addItem(NSMenuItem.separator()) let quitItem = NSMenuItem(title: "Quit", action: #selector(quitApp), keyEquivalent: "q") quitItem.target = self - menu.addItem(quitItem) + newMenu.addItem(quitItem) - self.statusItem?.menu = menu + self.menu = newMenu + self.statusItem?.menu = newMenu } @objc private func toggleMapping() { @@ -91,10 +97,12 @@ class StatusBarController { if eventTapManager.isRunning() { eventTapManager.stop() Logger.info("Mapping disabled by user") + saveEnabledState(false) } else { Logger.info("Attempting to enable mapping...") if eventTapManager.start() { Logger.info("Mapping enabled by user") + saveEnabledState(true) } else { Logger.error("Failed to enable mapping - check accessibility permissions") showAlert(title: "Error", message: "Failed to enable key mapping.\n\nPlease ensure:\n1. Accessibility permissions are granted\n2. The app is allowed in System Preferences > Privacy & Security > Accessibility\n\nCheck logs for details.") @@ -103,6 +111,18 @@ class StatusBarController { updateMenu() } + private func saveEnabledState(_ enabled: Bool) { + do { + let configPath = Config.getConfigPath() + let updatedConfig = Config(log: config.log, mapping: config.mapping, enabled: enabled) + try Config.save(updatedConfig, to: configPath) + config = updatedConfig + Logger.info("Saved enabled state: \(enabled)") + } catch { + Logger.error("Failed to save enabled state: \(error)") + } + } + @objc private func reloadConfig() { Logger.info("Reloading configuration...") do { @@ -118,10 +138,11 @@ class StatusBarController { } } - @objc private func openLogsFolder() { - let logsDir = Logger.getLogsDirectory() - Logger.info("Opening logs folder: \(logsDir)") - NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: logsDir) + @objc private func openConfigFolder() { + let homeDir = FileManager.default.homeDirectoryForCurrentUser + let configDir = homeDir.appendingPathComponent(".config/uskey").path + Logger.info("Opening config folder: \(configDir)") + NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: configDir) } @objc private func viewCurrentLog() { @@ -136,6 +157,26 @@ class StatusBarController { NSApplication.shared.terminate(nil) } + @objc private func showLimitations() { + let alert = NSAlert() + alert.messageText = "Known Limitations" + alert.informativeText = """ +Due to macOS security restrictions, key remapping may not work in the following scenarios: + +• Password input fields (Secure Input Mode) +• Lock screen / Login screen +• System dialogs and some secure contexts +• Some applications with enhanced security + +This is a macOS system limitation that affects all third-party keyboard remapping tools. + +For most regular input scenarios, remapping works as expected. +""" + alert.alertStyle = .informational + alert.addButton(withTitle: "OK") + alert.runModal() + } + private func showAlert(title: String, message: String) { let alert = NSAlert() alert.messageText = title