hid-keymap 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/native/hid-keymap +0 -0
- package/package.json +1 -1
- package/src/hid-keymap.swift +137 -6
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ hid-keymap --map 0x01/0x97=key:escape
|
|
|
22
22
|
hid-keymap --map 0x01/0x97=left_command+key:space
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Mapping mode stays running as a menu bar utility. Open its keyboard icon to see the configured source-to-target mappings, current status, and controls to stop, restart, or quit. The `--monitor` Event Viewer remains in the terminal and prints raw HID usage/value events.
|
|
26
|
+
|
|
25
27
|
Use `hid-keymap --list-targets` to list accepted target names. By default, it listens to all keyboard collections; add `--device VID/PID` to scope a monitor or mapping to one keyboard.
|
|
26
28
|
|
|
27
29
|
## Permissions and behavior
|
package/native/hid-keymap
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/hid-keymap.swift
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ApplicationServices
|
|
2
|
+
import AppKit
|
|
2
3
|
import CoreGraphics
|
|
3
4
|
import Foundation
|
|
4
5
|
import IOKit.hid
|
|
@@ -305,6 +306,8 @@ private final class HIDMapper {
|
|
|
305
306
|
private var activeTargets = Set<KeyTarget>()
|
|
306
307
|
private var connectedDevices = Set<String>()
|
|
307
308
|
private let formatter = ISO8601DateFormatter()
|
|
309
|
+
var onStatusChange: ((String) -> Void)?
|
|
310
|
+
private(set) var status = "Not started"
|
|
308
311
|
|
|
309
312
|
init(options: CLIOptions) {
|
|
310
313
|
eventViewer = options.monitor
|
|
@@ -356,9 +359,7 @@ private final class HIDMapper {
|
|
|
356
359
|
print("Listening for HID changes from \(scope). Press Ctrl-C to stop.")
|
|
357
360
|
print("Timestamp Device Usage page Usage Value")
|
|
358
361
|
} else {
|
|
359
|
-
|
|
360
|
-
print("Mapping \(source) → \(target). Press Ctrl-C to stop.")
|
|
361
|
-
}
|
|
362
|
+
setStatus("Ready · \(mappings.count) mapping\(mappings.count == 1 ? "" : "s")")
|
|
362
363
|
}
|
|
363
364
|
fflush(stdout)
|
|
364
365
|
}
|
|
@@ -386,6 +387,9 @@ private final class HIDMapper {
|
|
|
386
387
|
guard changed else { return }
|
|
387
388
|
|
|
388
389
|
updateTargets()
|
|
390
|
+
if !eventViewer, let target = mappings[code] {
|
|
391
|
+
setStatus(isDown ? "Pressed · \(target)" : "Ready · \(mappings.count) mapping\(mappings.count == 1 ? "" : "s")")
|
|
392
|
+
}
|
|
389
393
|
}
|
|
390
394
|
|
|
391
395
|
func setConnected(_ rawDevice: IOHIDDevice, connected: Bool) {
|
|
@@ -399,6 +403,7 @@ private final class HIDMapper {
|
|
|
399
403
|
if eventViewer { print("[Disconnected] \(device.label)") }
|
|
400
404
|
activeSources = activeSources.filter { $0.deviceID != device.id }
|
|
401
405
|
updateTargets()
|
|
406
|
+
if !eventViewer { setStatus("Keyboard disconnected · waiting for input") }
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
|
|
@@ -429,6 +434,7 @@ private final class HIDMapper {
|
|
|
429
434
|
IOHIDManagerUnscheduleFromRunLoop(manager, CFRunLoopGetMain(), CFRunLoopMode.defaultMode.rawValue)
|
|
430
435
|
}
|
|
431
436
|
manager = nil
|
|
437
|
+
if !eventViewer { setStatus("Stopped") }
|
|
432
438
|
}
|
|
433
439
|
|
|
434
440
|
private func releaseAllTargets() {
|
|
@@ -459,6 +465,124 @@ private final class HIDMapper {
|
|
|
459
465
|
event.flags = target.modifiers.reduce(flags) { $0.union($1.flag) }
|
|
460
466
|
event.post(tap: .cghidEventTap)
|
|
461
467
|
}
|
|
468
|
+
|
|
469
|
+
func reportError(_ error: Error) {
|
|
470
|
+
setStatus(String(describing: error))
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private func setStatus(_ newStatus: String) {
|
|
474
|
+
status = newStatus
|
|
475
|
+
onStatusChange?(newStatus)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private final class MappingMenuApp: NSObject, NSApplicationDelegate {
|
|
480
|
+
private let mapper: HIDMapper
|
|
481
|
+
private let mappings: [HIDMapping]
|
|
482
|
+
private let deviceFilter: DeviceFilter?
|
|
483
|
+
private var statusItem: NSStatusItem?
|
|
484
|
+
private var statusMenuItem: NSMenuItem?
|
|
485
|
+
private var interruptSource: DispatchSourceSignal?
|
|
486
|
+
private var terminateSource: DispatchSourceSignal?
|
|
487
|
+
|
|
488
|
+
init(options: CLIOptions) {
|
|
489
|
+
mapper = HIDMapper(options: options)
|
|
490
|
+
mappings = options.mappings ?? []
|
|
491
|
+
deviceFilter = options.deviceFilter
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
495
|
+
NSApp.setActivationPolicy(.accessory)
|
|
496
|
+
buildMenu()
|
|
497
|
+
mapper.onStatusChange = { [weak self] status in
|
|
498
|
+
self?.statusMenuItem?.title = status
|
|
499
|
+
self?.statusItem?.button?.toolTip = "hid-keymap · \(status)"
|
|
500
|
+
}
|
|
501
|
+
installSignalHandlers()
|
|
502
|
+
startMapping()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
func applicationWillTerminate(_ notification: Notification) {
|
|
506
|
+
mapper.stop()
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@objc private func stopMapping() {
|
|
510
|
+
mapper.stop()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
@objc private func restartMapping() {
|
|
514
|
+
mapper.stop()
|
|
515
|
+
startMapping()
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
@objc private func quit() {
|
|
519
|
+
mapper.stop()
|
|
520
|
+
NSApp.terminate(nil)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private func startMapping() {
|
|
524
|
+
do {
|
|
525
|
+
try mapper.start()
|
|
526
|
+
} catch {
|
|
527
|
+
mapper.reportError(error)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private func buildMenu() {
|
|
532
|
+
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
533
|
+
item.button?.title = "⌨"
|
|
534
|
+
item.button?.toolTip = "hid-keymap"
|
|
535
|
+
|
|
536
|
+
let menu = NSMenu()
|
|
537
|
+
let status = NSMenuItem(title: mapper.status, action: nil, keyEquivalent: "")
|
|
538
|
+
status.isEnabled = false
|
|
539
|
+
menu.addItem(status)
|
|
540
|
+
statusMenuItem = status
|
|
541
|
+
menu.addItem(.separator())
|
|
542
|
+
|
|
543
|
+
for mapping in mappings.sorted(by: { $0.source.description < $1.source.description }) {
|
|
544
|
+
let line = NSMenuItem(title: "\(mapping.source) → \(mapping.target)", action: nil, keyEquivalent: "")
|
|
545
|
+
line.isEnabled = false
|
|
546
|
+
menu.addItem(line)
|
|
547
|
+
}
|
|
548
|
+
if let deviceFilter {
|
|
549
|
+
let scope = NSMenuItem(title: String(format: "Device · 0x%04x/0x%04x", deviceFilter.vendorID, deviceFilter.productID), action: nil, keyEquivalent: "")
|
|
550
|
+
scope.isEnabled = false
|
|
551
|
+
menu.addItem(scope)
|
|
552
|
+
} else {
|
|
553
|
+
let scope = NSMenuItem(title: "Device · all keyboards", action: nil, keyEquivalent: "")
|
|
554
|
+
scope.isEnabled = false
|
|
555
|
+
menu.addItem(scope)
|
|
556
|
+
}
|
|
557
|
+
menu.addItem(.separator())
|
|
558
|
+
|
|
559
|
+
let stopItem = NSMenuItem(title: "Stop Mapping", action: #selector(stopMapping), keyEquivalent: "")
|
|
560
|
+
stopItem.target = self
|
|
561
|
+
menu.addItem(stopItem)
|
|
562
|
+
let restartItem = NSMenuItem(title: "Restart Mapping", action: #selector(restartMapping), keyEquivalent: "")
|
|
563
|
+
restartItem.target = self
|
|
564
|
+
menu.addItem(restartItem)
|
|
565
|
+
menu.addItem(.separator())
|
|
566
|
+
let quitItem = NSMenuItem(title: "Quit hid-keymap", action: #selector(quit), keyEquivalent: "q")
|
|
567
|
+
quitItem.target = self
|
|
568
|
+
menu.addItem(quitItem)
|
|
569
|
+
item.menu = menu
|
|
570
|
+
statusItem = item
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
private func installSignalHandlers() {
|
|
574
|
+
signal(SIGINT, SIG_IGN)
|
|
575
|
+
let interrupt = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
|
|
576
|
+
interrupt.setEventHandler { [weak self] in self?.quit() }
|
|
577
|
+
interrupt.resume()
|
|
578
|
+
interruptSource = interrupt
|
|
579
|
+
|
|
580
|
+
signal(SIGTERM, SIG_IGN)
|
|
581
|
+
let terminate = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main)
|
|
582
|
+
terminate.setEventHandler { [weak self] in self?.quit() }
|
|
583
|
+
terminate.resume()
|
|
584
|
+
terminateSource = terminate
|
|
585
|
+
}
|
|
462
586
|
}
|
|
463
587
|
|
|
464
588
|
@main
|
|
@@ -466,9 +590,16 @@ enum FnHIDMapMain {
|
|
|
466
590
|
static func main() {
|
|
467
591
|
do {
|
|
468
592
|
let options = try parseArguments(Array(CommandLine.arguments.dropFirst()))
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
593
|
+
if options.monitor {
|
|
594
|
+
let mapper = HIDMapper(options: options)
|
|
595
|
+
try mapper.start()
|
|
596
|
+
mapper.run()
|
|
597
|
+
} else {
|
|
598
|
+
let app = NSApplication.shared
|
|
599
|
+
let delegate = MappingMenuApp(options: options)
|
|
600
|
+
app.delegate = delegate
|
|
601
|
+
app.run()
|
|
602
|
+
}
|
|
472
603
|
} catch {
|
|
473
604
|
fputs("hid-keymap: \(error)\nUse --help for usage.\n", stderr)
|
|
474
605
|
exit(EXIT_FAILURE)
|