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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hid-keymap",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "macOS HID keyboard event viewer and key mapper",
5
5
  "bin": {
6
6
  "hid-keymap": "bin/hid-keymap.js"
@@ -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
- for (source, target) in mappings.sorted(by: { $0.key.description < $1.key.description }) {
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
- let mapper = HIDMapper(options: options)
470
- try mapper.start()
471
- mapper.run()
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)