karabiner-driverkit 0.4.0

Minimal Karabiner-DriverKit-VirtualHIDDevice wrapper for kanata
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
#include "driverkit.hpp"
#include <exception>

template<typename T>
int send_key(T& keyboard, struct DKEvent* e) {
    if(e->value == 1) keyboard.keys.insert(e->code);
    else if(e->value == 0) keyboard.keys.erase(e->code);
    else return 1;
    #ifdef USE_KEXT
    return pqrs::karabiner_virtual_hid_device_methods::post_keyboard_input_report(connect, keyboard);
    #else
    client->async_post_report(keyboard);
    return 0;
    #endif
}

#ifdef USE_KEXT

int init_sink() {
    kern_return_t kr;
    connect = IO_OBJECT_NULL;
    service = IOServiceGetMatchingService(kIOMasterPortDefault,
                                          IOServiceNameMatching(pqrs::karabiner_virtual_hid_device::get_virtual_hid_root_name()));
    if (!service) {
        print_iokit_error("IOServiceGetMatchingService");
        return 1;
    }
    kr = IOServiceOpen(service, mach_task_self(), kIOHIDServerConnectType, &connect);
    if (kr != KERN_SUCCESS) {
        print_iokit_error("IOServiceOpen", kr);
        return kr;
    }
    {
        pqrs::karabiner_virtual_hid_device::properties::keyboard_initialization properties;
        kr = pqrs::karabiner_virtual_hid_device_methods::initialize_virtual_hid_keyboard(connect, properties);
        if (kr != KERN_SUCCESS) {
            print_iokit_error("initialize_virtual_hid_keyboard", kr);
            return 1;
        }
        while (true) {
            bool ready;
            kr = pqrs::karabiner_virtual_hid_device_methods::is_virtual_hid_keyboard_ready(connect, ready);
            if (kr != KERN_SUCCESS) {
                print_iokit_error("is_virtual_hid_keyboard_ready", kr);
                return kr;
            } else {
                if (ready)
                    break;
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
        }
    }
    {
        pqrs::karabiner_virtual_hid_device::properties::keyboard_initialization properties;
        properties.country_code = 33;
        kr = pqrs::karabiner_virtual_hid_device_methods::initialize_virtual_hid_keyboard(connect, properties);
        if (kr != KERN_SUCCESS) {
            print_iokit_error("initialize_virtual_hid_keyboard", kr);
            return kr;
        }
    }
    return 0;
}

int exit_sink() {
    int retval = 0;
    kern_return_t kr = pqrs::karabiner_virtual_hid_device_methods::reset_virtual_hid_keyboard(connect);
    if (kr != KERN_SUCCESS) {
        print_iokit_error("reset_virtual_hid_keyboard", kr);
        retval = 1;
    }
    if (connect) {
        kr = IOServiceClose(connect);
        if(kr != KERN_SUCCESS) {
            print_iokit_error("IOServiceClose", kr);
            retval = 1;
        }
        connect = IO_OBJECT_NULL;
    }
    if (service) {
        kr = IOObjectRelease(service);
        if(kr != KERN_SUCCESS) {
            print_iokit_error("IOObjectRelease", kr);
            retval = 1;
        }
        service = IO_OBJECT_NULL;
    }
    return retval;
}

#else

int init_sink() {
    try {
        pqrs::dispatcher::extra::initialize_shared_dispatcher();

        client = new pqrs::karabiner::driverkit::virtual_hid_device_service::client();
        auto copy = client;

        client->connected.connect([copy] {
            std::cout << "connected" << std::endl;
            pqrs::karabiner::driverkit::virtual_hid_device_service::virtual_hid_keyboard_parameters parameters;
            parameters.set_country_code(pqrs::hid::country_code::us);
            copy->async_virtual_hid_keyboard_initialize(parameters);
            copy->async_virtual_hid_pointing_initialize();
        });

        client->virtual_hid_keyboard_ready.connect([](auto&& ready) {
            std::cout << "virtual_hid_keyboard_ready " << ready << std::endl;
            sink_ready.store(ready, std::memory_order_release);
        });

        client->virtual_hid_pointing_ready.connect([](auto&& ready) {
            std::cout << "virtual_hid_pointing_ready " << ready << std::endl;
            pointing_sink_ready.store(ready, std::memory_order_release);
        });

        client->closed.connect([] {
            std::cout << "closed" << std::endl;
            sink_ready.store(false, std::memory_order_release);
            pointing_sink_ready.store(false, std::memory_order_release);
        });

        client->connect_failed.connect([](auto&& error_code) {
            std::cout << "connect_failed " << error_code << std::endl;
            sink_ready.store(false, std::memory_order_release);
            pointing_sink_ready.store(false, std::memory_order_release);
        });

        client->error_occurred.connect([](auto&& error_code) {
            std::cout << "error_occurred " << error_code << std::endl;
            sink_ready.store(false, std::memory_order_release);
            pointing_sink_ready.store(false, std::memory_order_release);
        });

        client->driver_activated.connect([](auto&& driver_activated) {
            static std::optional<bool> previous_value;
            if (previous_value != driver_activated) {
                std::cout << "driver activated: " << std::boolalpha  << driver_activated << std::endl;
                previous_value = driver_activated;
            }
        });

        client->driver_connected.connect([](auto&& driver_connected) {
            static std::optional<bool> previous_value;
            if (previous_value != driver_connected) {
                std::cout << "driver connected: " << driver_connected << std::endl;
                previous_value = driver_connected;
            }
        });

        client->driver_version_mismatched.connect([](auto&& driver_version_mismatched) {
            static std::optional<bool> previous_value;
            if (previous_value != driver_version_mismatched) {
                std::cout << "driver version matched: " << !driver_version_mismatched << std::endl;
                previous_value = driver_version_mismatched;
            }
        });

        client->async_start();
        return 0;
    } catch (const std::exception& e) {
        std::cerr << "Exception in init_sink: " << e.what() << std::endl;
        return 1;
    }
}

int exit_sink() {
    if (client) {
        delete client;
        client = nullptr;
    }
    pqrs::dispatcher::extra::terminate_shared_dispatcher();
    return 0;
}

#endif

void fire_listener_thread() {
    if (!listener_thread.joinable())
        listener_thread = std::thread{
        [&]() {
            listener_loop = CFRunLoopGetCurrent();
            capture_registered_devices();
            CFRunLoopRun();
        } };
}

void input_callback(void* context, IOReturn result, void* sender, IOHIDValueRef value) {
    struct DKEvent e;
    IOHIDElementRef element = IOHIDValueGetElement(value);
    e.value = IOHIDValueGetIntegerValue(value);
    e.page = IOHIDElementGetUsagePage(element);
    e.code = IOHIDElementGetUsage(element);
    e.device_hash = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(context));
    write(fd[1], &e, sizeof(struct DKEvent));
}

void device_connected_callback(void* context, io_iterator_t iter) {
    uint64_t device_hash = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(context));
    for (mach_port_t curr = IOIteratorNext(iter); curr; curr = IOIteratorNext(iter)) {
        uint64_t curr_hash = hash_device(curr);
        if ( curr_hash == device_hash )
            capture_device(IOHIDDeviceCreate(kCFAllocatorDefault, curr), curr_hash);
        IOObjectRelease(curr);
    }
}

void device_connected_vidpid_callback(void* context, io_iterator_t iter) {
    uint64_t vidpid = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(context));
    uint32_t vid = static_cast<uint32_t>(vidpid >> 32);
    uint32_t pid = static_cast<uint32_t>(vidpid & 0xFFFFFFFF);
    CFStringRef karabiner = from_cstr("Karabiner");
    for (mach_port_t curr = IOIteratorNext(iter); curr; curr = IOIteratorNext(iter)) {
        uint32_t curr_vid = get_number_property(curr, kIOHIDVendorIDKey);
        uint32_t curr_pid = get_number_property(curr, kIOHIDProductIDKey);
        if (curr_vid == vid && curr_pid == pid) {
            CFStringRef name = get_product_name_cf(curr);
            if (name && !isSubstring(karabiner, name)) {
                uint64_t curr_hash = hash_device(curr);
                capture_device(IOHIDDeviceCreate(kCFAllocatorDefault, curr), curr_hash);
            }
            if (name) CFRelease(name);
        }
        IOObjectRelease(curr);
    }
    CFRelease(karabiner);
}

void close_registered_devices() {
    for(auto& [hash, device_ref] : opened_device_refs) {
        kern_return_t kr = IOHIDDeviceClose(device_ref, kIOHIDOptionsTypeSeizeDevice);
        if(kr != KERN_SUCCESS) { print_iokit_error("IOHIDDeviceClose", kr); }
        CFRelease(device_ref);
    }
    opened_device_refs.clear();
}

void init_keyboards_dictionary() {
    if( matching_dictionary ) return;
    matching_dictionary      = IOServiceMatching(kIOHIDDeviceKey);
    UInt32 generic_desktop   = kHIDPage_GenericDesktop;
    UInt32 gd_keyboard       = kHIDUsage_GD_Keyboard;
    CFNumberRef page_number  = CFNumberCreate( kCFAllocatorDefault, kCFNumberSInt32Type, &generic_desktop );
    CFNumberRef usage_number = CFNumberCreate( kCFAllocatorDefault, kCFNumberSInt32Type, &gd_keyboard );
    CFDictionarySetValue(matching_dictionary, CFSTR(kIOHIDDeviceUsagePageKey), page_number);
    CFDictionarySetValue(matching_dictionary, CFSTR(kIOHIDDeviceUsageKey),     usage_number);
    release_strings(page_number, usage_number);
}

io_iterator_t get_keyboards_iterator() {
    io_iterator_t iter = IO_OBJECT_NULL;
    CFRetain(matching_dictionary);
    IOServiceGetMatchingServices(kIOMainPortDefault, matching_dictionary, &iter);
    return iter;
}

template <typename Func>
bool consume_devices(Func consume) {
    init_keyboards_dictionary();
    io_iterator_t iter = get_keyboards_iterator();
    if(iter == IO_OBJECT_NULL) return false;
    bool result = false;
    for(mach_port_t curr = IOIteratorNext(iter); curr; curr = IOIteratorNext(iter))
        result = consume(curr) || result;
    IOObjectRelease(iter);
    return result;
}

void subscribe_to_notification(const char* notification_type, void* cb_arg, callback_type callback) {
    io_iterator_t iter = IO_OBJECT_NULL;
    CFRetain(matching_dictionary);
    kern_return_t kr = IOServiceAddMatchingNotification(notification_port, notification_type,
                       matching_dictionary, callback, cb_arg, &iter);
    if (kr != KERN_SUCCESS) { print_iokit_error(notification_type, kr); return; }
    for (io_object_t obj = IOIteratorNext(iter); obj; obj = IOIteratorNext(iter))
        IOObjectRelease(obj);
}

bool capture_device(IOHIDDeviceRef device_ref, uint64_t device_hash) {
    kern_return_t kr = IOHIDDeviceOpen(device_ref, kIOHIDOptionsTypeSeizeDevice);
    if(kr != kIOReturnSuccess) {
        print_iokit_error("IOHIDDeviceOpen", kr, CFStringToStdString(get_device_name(device_ref)));
        CFRelease(device_ref);
        return false;
    }
    void* ctx = reinterpret_cast<void*>(static_cast<uintptr_t>(device_hash));
    IOHIDDeviceRegisterInputValueCallback(device_ref, input_callback, ctx);
    IOHIDDeviceScheduleWithRunLoop(device_ref, listener_loop, kCFRunLoopDefaultMode);
    opened_device_refs[device_hash] = device_ref;
    return true;
}

bool capture_registered_devices() {
    // Register the notification port to the run loop, essential for receiving re-connect events so we can re-capture devices
    CFRunLoopAddSource(listener_loop, IONotificationPortGetRunLoopSource(notification_port), kCFRunLoopDefaultMode);
    // Try to capture devices that are already connected
    bool any_captured = consume_devices([](mach_port_t c) {
        uint64_t device_hash = hash_device(c);
        // Match by hash
        if ( registered_devices_hashes.find(device_hash) != registered_devices_hashes.end() ) {
            return capture_device(IOHIDDeviceCreate(kCFAllocatorDefault, c), device_hash);
        }
        // Match by VID:PID
        uint32_t vid = get_number_property(c, kIOHIDVendorIDKey);
        uint32_t pid = get_number_property(c, kIOHIDProductIDKey);
        uint64_t vidpid = (static_cast<uint64_t>(vid) << 32) | pid;
        if ( registered_device_vidpids.find(vidpid) != registered_device_vidpids.end() ) {
            CFStringRef name = get_product_name_cf(c);
            CFStringRef karabiner = from_cstr("Karabiner");
            bool is_karabiner = name && isSubstring(karabiner, name);
            release_strings(karabiner, name);
            if (!is_karabiner) {
                return capture_device(IOHIDDeviceCreate(kCFAllocatorDefault, c), device_hash);
            }
        }
        return false;
    });
    // Subscribe to notifications for ALL registered devices, regardless of whether they are currently connected.
    // This ensures that devices which appear later (e.g. plugged in after startup) are automatically captured.
    for (auto hash : registered_devices_hashes) {
        void* dev_hash = reinterpret_cast<void*>(static_cast<uintptr_t>(hash));
        subscribe_to_notification(kIOMatchedNotification, dev_hash, device_connected_callback);
    }
    for (auto vidpid : registered_device_vidpids) {
        void* ctx = reinterpret_cast<void*>(static_cast<uintptr_t>(vidpid));
        subscribe_to_notification(kIOMatchedNotification, ctx, device_connected_vidpid_callback);
    }
    return any_captured;
}

IOHIDDeviceRef get_device_by_hash(uint64_t device_hash) {
    init_keyboards_dictionary();
    io_iterator_t iter = get_keyboards_iterator();
    if(iter == IO_OBJECT_NULL) return nullptr;
    for(mach_port_t curr = IOIteratorNext(iter); curr; curr = IOIteratorNext(iter))
        if ( hash_device(curr) == device_hash )
            return IOHIDDeviceCreate(kCFAllocatorDefault, curr);
    IOObjectRelease(iter);
    return nullptr;
}

uint64_t fnv_hash(const std::string& key) {
    const uint64_t FNV_OFFSET = 14695981039346656037ull;
    const uint64_t FNV_PRIME  = 1099511628211ull;
    uint64_t hash = FNV_OFFSET;
    for (unsigned char c : key) {
        hash ^= c;
        hash *= FNV_PRIME;
    }
    return hash;
}

uint64_t hash_device(mach_port_t device) {
    std::string product_key = get_product_name(device);
    uint32_t vendor_id      = get_number_property(device, kIOHIDVendorIDKey);
    uint32_t product_id     = get_number_property(device, kIOHIDProductIDKey);
    std::string key         = std::to_string(vendor_id) + ":" + std::to_string(product_id) + ":" + product_key;
    return fnv_hash(key);
}

extern "C" {

    /*
     * current device is karabiner => return, shouldn't be registered nor captured, avoid at all costs!!!
     * product_kye is null         => register all devices
     * product_key specified       => register the device that matches product_key  */
    bool register_device(const char* product_key) {
        return consume_devices([product_key](mach_port_t current_device) {
            CFStringRef product_key_cfstring = product_key ? from_cstr(product_key) : from_cstr("");
            CFStringRef karabiner            = from_cstr("Karabiner"); //Karabiner DriverKit VirtualHIDKeyboard 1.7.0
            CFStringRef current_product_key  = get_product_name_cf(current_device);
            // Don't open karabiner devices or devices without a name
            if(!current_product_key || isSubstring(karabiner, current_product_key) ) {
                release_strings(karabiner, current_product_key, product_key_cfstring);
                return false;
            }
            bool registered = false;
            if(!product_key || (CFStringCompare(current_product_key, product_key_cfstring, 0) == kCFCompareEqualTo)) {
                registered_devices_hashes.insert(hash_device(current_device));
                registered = true;
            }
            release_strings(karabiner, current_product_key, product_key_cfstring);
            return registered;
        });
    }

    void list_keyboards() { // CFStringGetCStringPtr(cfString, kCFStringEncodingUTF8)
        consume_devices([](mach_port_t c) { std::cout << get_product_name(c) << std::endl; return true;});
    }

    void list_keyboards_with_ids() {
        consume_devices([](mach_port_t current_device) {
            // TODO: filter out duplicates (same vendor_id, product_id, name)
            // Also, print as decimal instad of hex?
            std::printf("vendor id: 0x%04X\t product id: 0x%04X\t Product key (name): %s hash: %llu\n",
                        get_number_property(current_device, kIOHIDVendorIDKey),
                        get_number_property(current_device, kIOHIDProductIDKey),
                        get_product_name(current_device).c_str(),
                        hash_device(current_device));
            return true;
        });
    }

    #ifdef USE_KEXT
    bool driver_activated() {
        // FIXME: should we have anything here?
        return true;
    }
    #else
    bool driver_activated() {
        auto service = IOServiceGetMatchingService(type_safe::get(pqrs::osx::iokit_mach_port::null),
                       IOServiceNameMatching("org_pqrs_Karabiner_DriverKit_VirtualHIDDeviceRoot"));
        if (!service) return false;
        IOObjectRelease(service);
        return true;
    }
    #endif

    // Reads a new key event from the pipe, blocking until a new event is ready.
    int wait_key(struct DKEvent* e) { return read(fd[0], e, sizeof(struct DKEvent)) == sizeof(struct DKEvent); }

    bool device_matches(const char* product) {
        if (!product) return true;
        init_keyboards_dictionary();
        io_iterator_t iter = get_keyboards_iterator();
        CFStringRef device = from_cstr(product);
        for(mach_port_t curr = IOIteratorNext(iter); curr; curr = IOIteratorNext(iter)) {
            CFStringRef current_device = get_product_name_cf(curr);
            if( current_device == NULL || CFStringCompare(current_device, device, 0) != kCFCompareEqualTo ) {
                if (current_device) CFRelease(current_device);
                continue;
            } else {
                release_strings(device, current_device);
                return true;
            }
        }
        CFRelease(device);
        IOObjectRelease(iter);
        return false;
    }

    /*
     * Opens and seizes input from each keyboard device whose product name
     * matches the parameter (if NULL is received, then it opens all
     * keyboard devices). Spawns a thread to receive asynchronous input
     * and opens a pipe for this thread to send key event data to the main
     * thread.
     *
     * Loads a the karabiner kernel extension that will send key events
     * back to the OS.
     */
    bool register_device_vidpid(uint32_t vendor_id, uint32_t product_id) {
        uint64_t vidpid = (static_cast<uint64_t>(vendor_id) << 32) | product_id;
        registered_device_vidpids.insert(vidpid);
        return consume_devices([vendor_id, product_id](mach_port_t current_device) {
            CFStringRef name = get_product_name_cf(current_device);
            CFStringRef karabiner = from_cstr("Karabiner");
            if (name && isSubstring(karabiner, name)) {
                release_strings(karabiner, name);
                return false;
            }
            release_strings(karabiner, name);
            return get_number_property(current_device, kIOHIDVendorIDKey) == vendor_id
                && get_number_property(current_device, kIOHIDProductIDKey) == product_id;
        });
    }

    int grab() {
        if (!registered_devices_hashes.size() && !registered_device_vidpids.size()) {
            std::cout << "At least one device has to be registered via register_device()" << std::endl;
            return 1;
        }
        if (pipe(fd) == -1) { std::cerr << "pipe error: " << errno << std::endl; return errno; }
        // Connect output before seizing input — ensures we can emit keystrokes
        // before taking exclusive control of the keyboard.
        int sink_err = init_sink();
        if (sink_err) return sink_err;
        fire_listener_thread();
        return 0;
    }

    /*
     * Releases the resources needed to receive key events from and send
     * key events to the OS.
     */
    void release() {
        std::cout << "release called" << std::endl;
        if(listener_thread.joinable()) { CFRunLoopStop(listener_loop); listener_thread.join(); }
        close_registered_devices();
        keyboard.keys.clear();
        close(fd[0]); close(fd[1]);
        exit_sink();
    }

    /*
     * Rust calls this with a new key event to send back to the OS. It
     * posts the information to the karabiner kernel extension (which
     * represents a virtual keyboard).
     */
    int send_key(struct DKEvent* e) {
        #ifdef USE_KEXT
        auto usage_page = pqrs::karabiner_virtual_hid_device::usage_page(e->page);
        if(usage_page == pqrs::karabiner_virtual_hid_device::usage_page::keyboard_or_keypad)
            return send_key(keyboard, e);
        else if(usage_page == pqrs::karabiner_virtual_hid_device::usage_page::apple_vendor_top_case)
            return send_key(top_case, e);
        else if(usage_page == pqrs::karabiner_virtual_hid_device::usage_page::apple_vendor_keyboard)
            return send_key(apple_keyboard, e);
        else if(usage_page == pqrs::karabiner_virtual_hid_device::usage_page::consumer)
            return send_key(consumer, e);
        else if(usage_page == pqrs::karabiner_virtual_hid_device::usage_page::generic_desktop)
            return send_key(generic_desktop, e);
        else
            return 1;
        #else
        if(!sink_ready.load(std::memory_order_acquire)) return 2;
        auto usage_page = pqrs::hid::usage_page::value_t(e->page);
        if(usage_page == pqrs::hid::usage_page::keyboard_or_keypad)
            return send_key(keyboard, e);
        else if(usage_page == pqrs::hid::usage_page::apple_vendor_top_case)
            return send_key(top_case, e);
        else if(usage_page == pqrs::hid::usage_page::apple_vendor_keyboard)
            return send_key(apple_keyboard, e);
        else if(usage_page == pqrs::hid::usage_page::consumer)
            return send_key(consumer, e);
        else if(usage_page == pqrs::hid::usage_page::generic_desktop)
            return send_key(generic_desktop, e);
        else return 1;
        #endif
    }

    const DeviceData* get_device_list(size_t* array_length) {
        static std::vector<std::string> products;   // to own the strings
        static std::vector<DeviceData>  devices;
        products.clear(); devices.clear();
        consume_devices([](mach_port_t current_device) {
                DeviceData d;
                products.emplace_back(get_product_name(current_device));
                d.vendor_id   = get_number_property(current_device, kIOHIDVendorIDKey);
                d.product_id  = get_number_property(current_device, kIOHIDProductIDKey);
                devices.push_back({ products.back().c_str(), d.vendor_id, d.product_id });
            return true;
        });
        *array_length = devices.size();
        return devices.data();
    }

    /*
     * Returns true when the DriverKit virtual keyboard is ready for output.
     * On the kext path, always returns true (kext has no async connection).
     */
    bool is_sink_ready() {
        #ifdef USE_KEXT
        return true;
        #else
        return sink_ready.load(std::memory_order_acquire);
        #endif
    }

    /*
     * Releases seized input devices and closes the pipe, but keeps the
     * output (sink) connection alive. This allows the pqrs client to
     * continue its heartbeat and auto-reconnect while the keyboard
     * returns to normal (unseized) operation.
     *
     * After this call, wait_key() on the read end of the pipe will
     * return 0 (EOF), which the caller can use to detect the release.
     */
    void release_input_only() {
        #ifndef USE_KEXT
        if(listener_thread.joinable()) {
            CFRunLoopRemoveSource(listener_loop, IONotificationPortGetRunLoopSource(notification_port), kCFRunLoopDefaultMode);
            CFRunLoopStop(listener_loop);
            listener_thread.join();
        }
        close_registered_devices();
        keyboard.keys.clear();
        close(fd[0]); close(fd[1]);
        #endif
    }

    /*
     * Re-seizes previously registered input devices after a recovery.
     * Requires that register_device() was called before (hashes are retained).
     * Returns true if at least one device was seized.
     */
    bool regrab_input() {
        #ifdef USE_KEXT
        return true;
        #else
        if (!registered_devices_hashes.size() && !registered_device_vidpids.size()) return false;
        if (pipe(fd) == -1) { std::cerr << "pipe error: " << errno << std::endl; return false; }
        fire_listener_thread();
        return true;
        #endif
    }

    // Virtual HID pointing device functions (DriverKit/dext only)
    // Buttons are 1-indexed: 1 = primary, 2 = secondary, 3 = middle, etc.
    void pointing_button_press(uint8_t button) {
        #ifndef USE_KEXT
        if (!pointing_sink_ready.load(std::memory_order_acquire)) return;
        pointing.buttons.insert(button);
        client->async_post_report(pointing);
        #endif
    }

    void pointing_button_release(uint8_t button) {
        #ifndef USE_KEXT
        if (!pointing_sink_ready.load(std::memory_order_acquire)) return;
        pointing.buttons.erase(button);
        client->async_post_report(pointing);
        #endif
    }

    void pointing_post_motion(int8_t x, int8_t y, int8_t vertical_wheel, int8_t horizontal_wheel) {
        #ifndef USE_KEXT
        if (!pointing_sink_ready.load(std::memory_order_acquire)) return;
        pointing.x = static_cast<uint8_t>(x);
        pointing.y = static_cast<uint8_t>(y);
        pointing.vertical_wheel = static_cast<uint8_t>(vertical_wheel);
        pointing.horizontal_wheel = static_cast<uint8_t>(horizontal_wheel);
        client->async_post_report(pointing);
        // Reset motion fields after posting, matching Karabiner-Elements behavior:
        // each motion report is a delta, not an absolute position.
        pointing.x = 0;
        pointing.y = 0;
        pointing.vertical_wheel = 0;
        pointing.horizontal_wheel = 0;
        #endif
    }

    bool is_pointing_ready() {
        #ifdef USE_KEXT
        return true;
        #else
        return pointing_sink_ready.load(std::memory_order_acquire);
        #endif
    }

}

// main function is just for testing
// build as binary command:
// g++ c_src/driverkit.cpp -DBUILD_AS_BINARY -Ic_src/Karabiner-DriverKit-VirtualHIDDevice/include/pqrs/karabiner/driverkit -Ic_src/Karabiner-DriverKit-VirtualHIDDevice/src/Client/vendor/include -Ic_src/Karabiner-DriverKit-VirtualHIDDevice/vendor/vendor/include -std=c++2b -framework IOKit -framework CoreFoundation -o driverkit -g -O0
#ifdef BUILD_AS_BINARY
int main() {
    list_keyboards();
    list_keyboards_with_ids();
    std::cout << "test device_matches:" << std::boolalpha << std::endl <<
                 "device_matches(NULL): " << device_matches(NULL) << std::endl <<
                 "device_matches(appl): " << device_matches("Apple Internal Keyboard / Trackpad") << std::endl <<
                 "device_matches(____): " << device_matches("nano") << std::noboolalpha << std::endl;

    const char* keeb = "Apple Internal Keyboard / Trackpad";
    const char* othr = "DZ60RGB_ANSI";


    // register_device(keeb);
    // register_device(nullptr);
    register_device(othr);

    for ( uint64_t hash : registered_devices_hashes )
        std::cout << "registered device: " << CFStringToStdString( get_device_name( get_device_by_hash(hash) ) ) <<
                  std::hex << " hash: " << hash << std::dec << " dev: " << get_device_by_hash(hash) << std::endl;

    grab();
    listener_thread.join();
    release();

    return 0;
}
#endif