3 Commits

Author SHA1 Message Date
loveuer
6285cabdd1 Major improvements and bug fixes for v1.1.0
Some checks failed
Build and Release / Build DMG for arm64 (push) Has been cancelled
Build and Release / Build DMG for x86_64 (push) Has been cancelled
Build and Release / Create Release (push) Has been cancelled
Features:
- Add auto-enable on startup based on config
- Add enabled state persistence in config.json
- Add limitations notice in GUI menu
- Simplify logging to single file (uskey.log)
- Change "Open Logs Folder" to "Open Config Folder"

Bug Fixes:
- Fix GUI status display mismatch on startup
- Fix first toggle click not working issue
- Revert password field changes that caused key blocking

UI Changes:
- Add ⚠️ emoji to "About Limitations" menu item
- Remove button action to fix menu refresh

🤖 Generated with [Qoder][https://qoder.com]
2025-12-05 13:48:15 +08:00
loveuer
99bbbbc73a Fix keyboard remapping in password fields and secure input contexts
Some checks failed
Build and Release / Build DMG for arm64 (push) Has been cancelled
Build and Release / Build DMG for x86_64 (push) Has been cancelled
Build and Release / Create Release (push) Has been cancelled
Changed event handling to create and post new keyboard events instead of
modifying existing events, which resolves issues with password fields in
web browsers and other secure input contexts that ignore modified events.

🤖 Generated with [Qoder][https://qoder.com]
2025-12-03 19:07:25 +08:00
loveuer
333bbc56f9 chore: add KEY_MAPPING.md, update README.md 2025-12-03 10:26:38 +08:00
6 changed files with 244 additions and 44 deletions

148
KEY_MAPPING.md Normal file
View File

@@ -0,0 +1,148 @@
# macOS Key Code Reference
This document lists common macOS key codes for keyboard remapping configuration.
## Alphanumeric Keys
| Key | Code | Description |
|-----|------|-------------|
| A | 0 | Letter A |
| S | 1 | Letter S |
| D | 2 | Letter D |
| F | 3 | Letter F |
| H | 4 | Letter H |
| G | 5 | Letter G |
| Z | 6 | Letter Z |
| X | 7 | Letter X |
| C | 8 | Letter C |
| V | 9 | Letter V |
| B | 11 | Letter B |
| Q | 12 | Letter Q |
| W | 13 | Letter W |
| E | 14 | Letter E |
| R | 15 | Letter R |
| Y | 16 | Letter Y |
| T | 17 | Letter T |
| 1 | 18 | Number 1 |
| 2 | 19 | Number 2 |
| 3 | 20 | Number 3 |
| 4 | 21 | Number 4 |
| 6 | 22 | Number 6 |
| 5 | 23 | Number 5 |
| = | 24 | Equal sign |
| 9 | 25 | Number 9 |
| 7 | 26 | Number 7 |
| - | 27 | Minus/Hyphen |
| 8 | 28 | Number 8 |
| 0 | 29 | Number 0 |
| ] | 30 | Right bracket |
| O | 31 | Letter O |
| U | 32 | Letter U |
| [ | 33 | Left bracket |
| I | 34 | Letter I |
| P | 35 | Letter P |
| L | 37 | Letter L |
| J | 38 | Letter J |
| ' | 39 | Quote/Apostrophe |
| K | 40 | Letter K |
| ; | 41 | Semicolon |
| \ | 42 | Backslash |
| , | 43 | Comma |
| / | 44 | Slash |
| N | 45 | Letter N |
| M | 46 | Letter M |
| . | 47 | Period/Dot |
| ` | 50 | Grave accent/Backtick |
## Function Keys
| Key | Code | Description |
|-----|------|-------------|
| Return | 36 | Enter/Return key |
| Tab | 48 | Tab key |
| Space | 49 | Space bar |
| Delete | 51 | Backspace/Delete |
| Escape | 53 | Escape key |
| Command | 55 | Command/Cmd key |
| Shift | 56 | Shift key |
| CapsLock | 57 | Caps Lock |
| Option | 58 | Option/Alt key |
| Control | 59 | Control key |
| RightShift | 60 | Right Shift key |
| RightOption | 61 | Right Option/Alt key |
| RightControl | 62 | Right Control key |
| Function | 63 | Function/Fn key |
## Special Keys
| Key | Code | Description |
|-----|------|-------------|
| F5 | 96 | F5 key |
| F6 | 97 | F6 key |
| F7 | 98 | F7 key |
| F3 | 99 | F3 key |
| F8 | 100 | F8 key |
| F9 | 101 | F9 key |
| F11 | 103 | F11 key |
| F13 | 105 | F13 key |
| F14 | 107 | F14 key |
| F10 | 109 | F10 key |
| F12 | 111 | F12 key |
| F15 | 113 | F15 key |
| Help | 114 | Help key |
| Home | 115 | Home key |
| PageUp | 116 | Page Up |
| ForwardDelete | 117 | Forward Delete (Del) |
| F4 | 118 | F4 key |
| End | 119 | End key |
| F2 | 120 | F2 key |
| PageDown | 121 | Page Down |
| F1 | 122 | F1 key |
| LeftArrow | 123 | Left Arrow |
| RightArrow | 124 | Right Arrow |
| DownArrow | 125 | Down Arrow |
| UpArrow | 126 | Up Arrow |
## Numpad Keys
| Key | Code | Description |
|-----|------|-------------|
| Keypad . | 65 | Decimal point |
| Keypad * | 67 | Multiply |
| Keypad + | 69 | Add |
| Keypad Clear | 71 | Clear |
| Keypad / | 75 | Divide |
| Keypad Enter | 76 | Enter |
| Keypad - | 78 | Subtract |
| Keypad = | 81 | Equal |
| Keypad 0 | 82 | Number 0 |
| Keypad 1 | 83 | Number 1 |
| Keypad 2 | 84 | Number 2 |
| Keypad 3 | 85 | Number 3 |
| Keypad 4 | 86 | Number 4 |
| Keypad 5 | 87 | Number 5 |
| Keypad 6 | 88 | Number 6 |
| Keypad 7 | 89 | Number 7 |
| Keypad 8 | 91 | Number 8 |
| Keypad 9 | 92 | Number 9 |
## Media and System Keys
| Key | Code | Description |
|-----|------|-------------|
| Mute | 74 | Volume Mute |
| VolumeUp | 72 | Volume Up |
| VolumeDown | 73 | Volume Down |
| Brightness Down | 145 | Display brightness down |
| Brightness Up | 144 | Display brightness up |
| Play/Pause | 16 | Media play/pause (F8) |
| Previous Track | 17 | Previous track (F7) |
| Next Track | 19 | Next track (F9) |
## Notes
- Key codes are decimal values used by macOS CGEvent API
- Some media keys may require special handling or may not be remappable
- Function keys (F1-F15) can have default system behaviors that may need to be overridden
- Modifier keys (Command, Shift, Control, Option) can be combined with other keys
- The codes listed are for US keyboard layout; other layouts may vary

View File

@@ -70,7 +70,9 @@ The configuration file is automatically created at `~/.config/uskey/config.json`
### Key Codes ### Key Codes
Common macOS key codes: For a complete list of macOS key codes, see [KEY_MAPPING.md](KEY_MAPPING.md).
Common examples:
- Backspace: 51 - Backspace: 51
- Backslash `\`: 42 - Backslash `\`: 42
- Enter: 36 - Enter: 36
@@ -114,12 +116,14 @@ If you can't enable mapping after installation:
### Log Files Location ### 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: You can view logs by:
- **Menu Bar**: Click uskey icon → "View Current Log" - **Menu Bar**: Click uskey icon → "View Current Log"
- **Finder**: Click uskey icon → "Open Logs Folder" (⌘L) - **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 ### Common Issues

View File

@@ -53,11 +53,15 @@ class AppDelegate: NSObject, NSApplicationDelegate {
statusBarController?.setupStatusBar() statusBarController?.setupStatusBar()
Logger.info("Attempting to start event monitoring...") if config.enabled {
if eventTapManager!.start() { Logger.info("Auto-enabling event monitoring (enabled: true in config)...")
Logger.info("Event monitoring started successfully") if eventTapManager!.start() {
Logger.info("Event monitoring started successfully")
} else {
Logger.error("Failed to start event monitoring - check if app has accessibility permissions")
}
} else { } 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") Logger.info("Application ready")

View File

@@ -54,10 +54,12 @@ struct MappingConfig: Codable {
struct Config: Codable { struct Config: Codable {
let log: LogConfig let log: LogConfig
let mapping: MappingConfig 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.log = log
self.mapping = mapping self.mapping = mapping
self.enabled = enabled
} }
static func load(from path: String) throws -> Config { static func load(from path: String) throws -> Config {
@@ -67,13 +69,21 @@ struct Config: Codable {
return try decoder.decode(Config.self, from: data) 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 { static func createDefault(at path: String) throws {
let defaultConfig = Config( let defaultConfig = Config(
log: LogConfig(level: .info), log: LogConfig(level: .info),
mapping: MappingConfig(mappings: [ mapping: MappingConfig(mappings: [
"backslash2backspace": KeyMapping(from: 42, to: 51), "backslash2backspace": KeyMapping(from: 42, to: 51),
"backspace2backslash": KeyMapping(from: 51, to: 42) "backspace2backslash": KeyMapping(from: 51, to: 42)
]) ]),
enabled: true
) )
let encoder = JSONEncoder() let encoder = JSONEncoder()
@@ -90,4 +100,4 @@ struct Config: Codable {
return configDir.appendingPathComponent("config.json").path return configDir.appendingPathComponent("config.json").path
} }
} }

View File

@@ -9,18 +9,11 @@ struct Logger {
let logsDir = getLogsDirectory() let logsDir = getLogsDirectory()
try? FileManager.default.createDirectory(at: URL(fileURLWithPath: logsDir), withIntermediateDirectories: true) try? FileManager.default.createDirectory(at: URL(fileURLWithPath: logsDir), withIntermediateDirectories: true)
let dateFormatter = DateFormatter() logFilePath = "\(logsDir)/uskey.log"
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateString = dateFormatter.string(from: Date())
logFilePath = "\(logsDir)/uskey-\(dateString).log" FileManager.default.createFile(atPath: logFilePath!, contents: nil)
if !FileManager.default.fileExists(atPath: logFilePath!) {
FileManager.default.createFile(atPath: logFilePath!, contents: nil)
}
logFileHandle = FileHandle(forWritingAtPath: logFilePath!) logFileHandle = FileHandle(forWritingAtPath: logFilePath!)
logFileHandle?.seekToEndOfFile()
info("Logger initialized, log file: \(logFilePath!)") info("Logger initialized, log file: \(logFilePath!)")
} }
@@ -82,4 +75,4 @@ struct Logger {
static func cleanup() { static func cleanup() {
logFileHandle?.closeFile() logFileHandle?.closeFile()
} }
} }

View File

@@ -6,6 +6,7 @@ class StatusBarController {
private var statusItem: NSStatusItem? private var statusItem: NSStatusItem?
private var eventTapManager: EventTapManager private var eventTapManager: EventTapManager
private var config: Config private var config: Config
private var menu: NSMenu?
init(eventTapManager: EventTapManager, config: Config) { init(eventTapManager: EventTapManager, config: Config) {
self.eventTapManager = eventTapManager self.eventTapManager = eventTapManager
@@ -17,8 +18,6 @@ class StatusBarController {
if let button = statusItem?.button { if let button = statusItem?.button {
button.image = NSImage(systemSymbolName: "keyboard", accessibilityDescription: "uskey") button.image = NSImage(systemSymbolName: "keyboard", accessibilityDescription: "uskey")
button.action = #selector(statusBarButtonClicked)
button.target = self
} }
updateMenu() updateMenu()
@@ -29,10 +28,10 @@ class StatusBarController {
} }
private func updateMenu() { private func updateMenu() {
let menu = NSMenu() let newMenu = NSMenu()
menu.addItem(NSMenuItem(title: "uskey - Keyboard Remapper", action: nil, keyEquivalent: "")) newMenu.addItem(NSMenuItem(title: "uskey - Keyboard Remapper", action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem.separator()) newMenu.addItem(NSMenuItem.separator())
let isEnabled = eventTapManager.isRunning() let isEnabled = eventTapManager.isRunning()
let toggleItem = NSMenuItem( let toggleItem = NSMenuItem(
@@ -41,49 +40,56 @@ class StatusBarController {
keyEquivalent: "" keyEquivalent: ""
) )
toggleItem.target = self toggleItem.target = self
menu.addItem(toggleItem) newMenu.addItem(toggleItem)
menu.addItem(NSMenuItem.separator()) newMenu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Current Mappings:", action: nil, keyEquivalent: "")) newMenu.addItem(NSMenuItem(title: "Current Mappings:", action: nil, keyEquivalent: ""))
let mappings = config.mapping.getAllMappings() let mappings = config.mapping.getAllMappings()
if mappings.isEmpty { if mappings.isEmpty {
let item = NSMenuItem(title: " No mappings configured", action: nil, keyEquivalent: "") let item = NSMenuItem(title: " No mappings configured", action: nil, keyEquivalent: "")
item.isEnabled = false item.isEnabled = false
menu.addItem(item) newMenu.addItem(item)
} else { } else {
for (from, to) in mappings.sorted(by: { $0.0 < $1.0 }) { for (from, to) in mappings.sorted(by: { $0.0 < $1.0 }) {
let item = NSMenuItem(title: " \(from)\(to)", action: nil, keyEquivalent: "") let item = NSMenuItem(title: " \(from)\(to)", action: nil, keyEquivalent: "")
item.isEnabled = false 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") let reloadItem = NSMenuItem(title: "Reload Configuration", action: #selector(reloadConfig), keyEquivalent: "r")
reloadItem.target = self 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") let openConfigItem = NSMenuItem(title: "Open Config Folder", action: #selector(openConfigFolder), keyEquivalent: "c")
openLogsItem.target = self openConfigItem.target = self
menu.addItem(openLogsItem) newMenu.addItem(openConfigItem)
if let _ = Logger.getLogFilePath() { if let _ = Logger.getLogFilePath() {
let viewLogItem = NSMenuItem(title: "View Current Log", action: #selector(viewCurrentLog), keyEquivalent: "") let viewLogItem = NSMenuItem(title: "View Current Log", action: #selector(viewCurrentLog), keyEquivalent: "")
viewLogItem.target = self 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") let quitItem = NSMenuItem(title: "Quit", action: #selector(quitApp), keyEquivalent: "q")
quitItem.target = self quitItem.target = self
menu.addItem(quitItem) newMenu.addItem(quitItem)
self.statusItem?.menu = menu self.menu = newMenu
self.statusItem?.menu = newMenu
} }
@objc private func toggleMapping() { @objc private func toggleMapping() {
@@ -91,10 +97,12 @@ class StatusBarController {
if eventTapManager.isRunning() { if eventTapManager.isRunning() {
eventTapManager.stop() eventTapManager.stop()
Logger.info("Mapping disabled by user") Logger.info("Mapping disabled by user")
saveEnabledState(false)
} else { } else {
Logger.info("Attempting to enable mapping...") Logger.info("Attempting to enable mapping...")
if eventTapManager.start() { if eventTapManager.start() {
Logger.info("Mapping enabled by user") Logger.info("Mapping enabled by user")
saveEnabledState(true)
} else { } else {
Logger.error("Failed to enable mapping - check accessibility permissions") 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.") 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() 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() { @objc private func reloadConfig() {
Logger.info("Reloading configuration...") Logger.info("Reloading configuration...")
do { do {
@@ -118,10 +138,11 @@ class StatusBarController {
} }
} }
@objc private func openLogsFolder() { @objc private func openConfigFolder() {
let logsDir = Logger.getLogsDirectory() let homeDir = FileManager.default.homeDirectoryForCurrentUser
Logger.info("Opening logs folder: \(logsDir)") let configDir = homeDir.appendingPathComponent(".config/uskey").path
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: logsDir) Logger.info("Opening config folder: \(configDir)")
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: configDir)
} }
@objc private func viewCurrentLog() { @objc private func viewCurrentLog() {
@@ -136,6 +157,26 @@ class StatusBarController {
NSApplication.shared.terminate(nil) 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) { private func showAlert(title: String, message: String) {
let alert = NSAlert() let alert = NSAlert()
alert.messageText = title alert.messageText = title