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)
114 lines
3.5 KiB
Swift
114 lines
3.5 KiB
Swift
@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()
|
|
}
|
|
}
|
|
} |