ohmu
·
2026-10-15
1#include "keyboard.h"
2#include "common/debug.h"
3#include "common/io.h"
4#include "common/strutil.h"
5
6#include <linux/input-event-codes.h>
7
8const char* ffDetectKeyboard(FFlist* devices /* List of FFKeyboardDevice */) {
9 // Parse /proc/bus/input/devices to find keyboards with a "kbd" handler.
10 // This detects both wired and Bluetooth keyboards uniformly.
11 FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
12 if (!ffAppendFileBuffer("/proc/bus/input/devices", &content)) {
13 FF_DEBUG("ffAppendFileBuffer(\"/proc/bus/input/devices\") failed");
14 return "ffAppendFileBuffer(\"/proc/bus/input/devices\") == nullptr";
15 }
16
17 FFstrbuf kbd = ffStrbufCreateStatic("kbd");
18
19 FFKeyboardDevice device = {
20 .name = ffStrbufCreate(),
21 .serial = ffStrbufCreate()
22 };
23
24 char* line = nullptr;
25 size_t len = 0;
26 while (ffStrbufGetline(&line, &len, &content)) {
27 switch (line[0]) {
28 case 'N': {
29 const uint32_t prefixLen = strlen("N: Name=");
30 if (__builtin_expect(len <= prefixLen, false)) {
31 continue;
32 }
33 const char* name = line + prefixLen;
34 const uint32_t nameLen = (uint32_t) len - prefixLen;
35 ffStrbufSetNS(&device.name, nameLen, name);
36 ffStrbufTrim(&device.name, '"');
37 continue;
38 }
39 case 'H': {
40 const uint32_t prefixLen = strlen("H: Handlers=");
41 if (__builtin_expect(len <= prefixLen, false)) {
42 continue;
43 }
44 const char* handlers = line + prefixLen;
45 const uint32_t handlersLen = (uint32_t) len - prefixLen;
46 if (!ffStrbufMatchSeparatedNS(&kbd, handlersLen, handlers, ' ')) {
47 goto skipDevice;
48 }
49 continue;
50 }
51 case 'B': {
52 const char* bits = line + strlen("B: ");
53 if (ffStrStartsWith(bits, "EV=")) {
54 // Check EV_REP (auto-repeat, bit 20) to filter pseudo-keyboards (Power Button, PC Speaker).
55 const char* evBits = bits + strlen("EV=");
56 uint64_t val = strtoull(evBits, nullptr, 16);
57 if (!(val & (1ULL << EV_REP))) {
58 goto skipDevice;
59 }
60 } else if (ffStrStartsWith(bits, "KEY=")) {
61 // Check KEY_A (bit 30) to filter media remotes and headset controls.
62 // The key capability bitmap is space-separated hex longs, MSB first;
63 // KEY_A falls in the last (least significant) word on all architectures.
64 const char* keyBits = bits + strlen("KEY=");
65 const char* lastWord = memrchr(keyBits, ' ', len - (size_t) (keyBits - line));
66 lastWord = lastWord ? lastWord + 1 : keyBits;
67
68 uint64_t val = strtoull(lastWord, nullptr, 16);
69 if (!(val & (1ULL << KEY_A))) {
70 goto skipDevice;
71 }
72 }
73 continue;
74 }
75 case 'U': {
76 const uint32_t prefixLen = strlen("U: Uniq=");
77 if (__builtin_expect(len <= prefixLen, false)) {
78 continue;
79 }
80 const char* uniq = line + prefixLen;
81 const uint32_t uniqLen = (uint32_t) len - prefixLen;
82 ffStrbufSetNS(&device.serial, uniqLen, uniq);
83 continue;
84 }
85 case '\0':
86 // End of device entry; add to list if it has a name.
87 if (device.name.length > 0) {
88 FFKeyboardDevice* added = FF_LIST_ADD(FFKeyboardDevice, *devices);
89 ffStrbufInitMove(&added->name, &device.name);
90 ffStrbufInitMove(&added->serial, &device.serial);
91 }
92 continue;
93 default:
94 continue;
95 }
96
97 skipDevice:
98 // Skip to the end of the current device entry.
99 while (line[0] != '\0' && ffStrbufGetline(&line, &len, &content));
100 // Despite the fn name, it resets the string buffer to initial state
101 ffStrbufDestroy(&device.name);
102 ffStrbufDestroy(&device.serial);
103 }
104
105 return nullptr;
106}