Initial commit: uskey - macOS keyboard remapper

Features:
- Menu bar GUI with enable/disable toggle
- JSON-based configuration system
- File-based logging with debug support
- CGEventTap-based key remapping
- Custom app icon support
- DMG installer packaging

Core Components:
- AppDelegate: Application lifecycle and initialization
- EventTapManager: Event tap creation and management with proper pointer lifetime
- KeyMapper: Key mapping logic and configuration loading
- StatusBarController: Menu bar UI and user interactions
- Logger: File and console logging with configurable levels
- Config: JSON configuration parser with default creation

Build System:
- build-app.sh: Creates macOS .app bundle with icon
- build-dmg.sh: Generates distributable DMG installer
- create-icon.sh: Converts PNG to .icns format

Documentation:
- README.md: User guide and troubleshooting
- BUILD.md: Build instructions and packaging
- DEBUG.md: Debugging guide with log access

🤖 Generated with [Qoder](https://qoder.com)
This commit is contained in:
loveuer
2025-12-02 17:51:56 +08:00
commit 1e8b79585f
18 changed files with 1229 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
@preconcurrency import Cocoa
@preconcurrency import CoreGraphics
@preconcurrency import ApplicationServices
class EventTapManager {
private var eventTap: CFMachPort?
private var runLoopSource: CFRunLoopSource?
private var isEnabled: Bool = false
var keyMapper: KeyMapper
init(keyMapper: KeyMapper) {
self.keyMapper = keyMapper
}
func start() -> Bool {
guard !isEnabled else { return true }
Logger.debug("Creating event tap...")
let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
let selfPtr = Unmanaged.passUnretained(self).toOpaque()
let callback: CGEventTapCallBack = { proxy, type, event, refcon in
guard type == .keyDown || type == .keyUp else {
return Unmanaged.passRetained(event)
}
guard let refcon = refcon else {
Logger.error("Event callback: refcon is nil")
return Unmanaged.passRetained(event)
}
let manager = Unmanaged<EventTapManager>.fromOpaque(refcon).takeUnretainedValue()
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
if manager.keyMapper.hasMappingFor(keyCode: keyCode) {
if let mappedKey = manager.keyMapper.getMappedKey(for: keyCode) {
Logger.debug("Remapping: \(keyCode) -> \(mappedKey)")
event.setIntegerValueField(.keyboardEventKeycode, value: mappedKey)
}
}
return Unmanaged.passRetained(event)
}
eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask(eventMask),
callback: callback,
userInfo: selfPtr
)
guard let eventTap = eventTap else {
Logger.error("Failed to create event tap - check accessibility permissions")
return false
}
Logger.debug("Event tap created successfully")
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
guard let runLoopSource = runLoopSource else {
Logger.error("Failed to create run loop source")
return false
}
Logger.debug("Adding event tap to run loop...")
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
isEnabled = true
Logger.info("Event tap started")
return true
}
func stop() {
guard isEnabled else { return }
Logger.debug("Stopping event tap...")
if let eventTap = eventTap {
CGEvent.tapEnable(tap: eventTap, enable: false)
if let runLoopSource = runLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
}
}
eventTap = nil
runLoopSource = nil
isEnabled = false
Logger.info("Event tap stopped")
}
func isRunning() -> Bool {
return isEnabled
}
func reload(config: Config) {
let wasEnabled = isEnabled
if wasEnabled {
stop()
}
keyMapper.loadFromConfig(config)
if wasEnabled {
_ = start()
}
}
}