Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186#include "input.h"#include <stdio.h>#include <stdlib.h>#include <string.h>#include <limits.h>#include <fcntl.h>#include <unistd.h>#include <dirent.h>#include <linux/input.h>#include <linux/hidraw.h>#include <sys/ioctl.h>#include <sys/stat.h>#include <errno.h>#include <time.h>#include <math.h>#include <poll.h>#ifdef USE_WAYLAND#include "wayland-display.h"#endif
static double monotonic_sec(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec * 1e-9;}
const char *input_key_name(int code) { switch (code) { case KEY_A: return "a"; case KEY_B: return "b"; case KEY_C: return "c"; case KEY_D: return "d"; case KEY_E: return "e"; case KEY_F: return "f"; case KEY_G: return "g"; case KEY_H: return "h"; case KEY_I: return "i"; case KEY_J: return "j"; case KEY_K: return "k"; case KEY_L: return "l"; case KEY_M: return "m"; case KEY_N: return "n"; case KEY_O: return "o"; case KEY_P: return "p"; case KEY_Q: return "q"; case KEY_R: return "r"; case KEY_S: return "s"; case KEY_T: return "t"; case KEY_U: return "u"; case KEY_V: return "v"; case KEY_W: return "w"; case KEY_X: return "x"; case KEY_Y: return "y"; case KEY_Z: return "z"; case KEY_0: return "0"; case KEY_1: return "1"; case KEY_2: return "2"; case KEY_3: return "3"; case KEY_4: return "4"; case KEY_5: return "5"; case KEY_6: return "6"; case KEY_7: return "7"; case KEY_8: return "8"; case KEY_9: return "9"; case KEY_SPACE: return "space"; case KEY_ENTER: return "enter"; case KEY_BACKSPACE: return "backspace"; case KEY_TAB: return "tab"; case KEY_ESC: return "escape"; case KEY_UP: return "arrowup"; case KEY_DOWN: return "arrowdown"; case KEY_LEFT: return "arrowleft"; case KEY_RIGHT: return "arrowright"; case KEY_LEFTSHIFT: case KEY_RIGHTSHIFT: return "shift"; case KEY_LEFTCTRL: case KEY_RIGHTCTRL: return "control"; case KEY_LEFTALT: case KEY_RIGHTALT: return "alt"; case KEY_MINUS: return "-"; case KEY_EQUAL: return "="; case KEY_LEFTBRACE: return "["; case KEY_RIGHTBRACE: return "]"; case KEY_SEMICOLON: return ";"; case KEY_APOSTROPHE: return "'"; case KEY_COMMA: return ","; case KEY_DOT: return "."; case KEY_SLASH: return "/"; case KEY_BACKSLASH: return "\\"; case KEY_GRAVE: return "`"; case KEY_DELETE: return "delete"; case KEY_HOME: return "home"; case KEY_END: return "end"; case KEY_PAGEUP: return "pageup"; case KEY_PAGEDOWN: return "pagedown"; case KEY_F1: return "f1"; case KEY_F2: return "f2"; case KEY_F3: return "f3"; case KEY_F4: return "f4"; case KEY_F5: return "f5"; case KEY_F6: return "f6"; case KEY_F7: return "f7"; case KEY_F8: return "f8"; case KEY_F9: return "f9"; case KEY_F10: return "f10"; case KEY_F11: return "f11"; case KEY_F12: return "f12"; case KEY_MUTE: return "audiomute"; case KEY_VOLUMEDOWN: return "audiovolumedown"; case KEY_VOLUMEUP: return "audiovolumeup"; case KEY_POWER: return "power"; case KEY_BRIGHTNESSDOWN: return "brightnessdown"; case KEY_BRIGHTNESSUP: return "brightnessup"; case KEY_MICMUTE: return "micmute"; case KEY_SWITCHVIDEOMODE: return "switchvideo"; case KEY_WLAN: return "wlan"; case KEY_MEDIA: return "media"; case KEY_KBDILLUMTOGGLE: return "kbdlight"; default: return NULL; }}
// NuPhy HE scancode → Linux keycode mapping// NuPhy uses HID scancodes; modifiers use special bitmask codesstatic int nuphy_scancode_to_keycode(int sc) { // Modifier keys (NuPhy bitmask codes) switch (sc) { case 0x100: return KEY_LEFTCTRL; case 0x200: return KEY_LEFTSHIFT; case 0x400: return KEY_LEFTALT; case 0x800: return KEY_LEFTMETA; case 0x1000: return KEY_RIGHTCTRL; case 0x2000: return KEY_RIGHTSHIFT; case 0x4000: return KEY_RIGHTALT; case 0x8000: return KEY_RIGHTMETA; case 0xff05: return KEY_FN; // Fn key } // Standard HID usage → Linux keycode (HID usage page 0x07) // Letters A-Z: HID 0x04-0x1D → KEY_A-KEY_Z if (sc >= 0x04 && sc <= 0x1D) { static const int letter_map[] = { KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J, KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T, KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z }; return letter_map[sc - 0x04]; } // Numbers 1-0: HID 0x1E-0x27 if (sc >= 0x1E && sc <= 0x27) { static const int num_map[] = { KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0 }; return num_map[sc - 0x1E]; } // Common keys switch (sc) { case 0x28: return KEY_ENTER; case 0x29: return KEY_ESC; case 0x2A: return KEY_BACKSPACE; case 0x2B: return KEY_TAB; case 0x2C: return KEY_SPACE; case 0x2D: return KEY_MINUS; case 0x2E: return KEY_EQUAL; case 0x2F: return KEY_LEFTBRACE; case 0x30: return KEY_RIGHTBRACE; case 0x31: return KEY_BACKSLASH; case 0x33: return KEY_SEMICOLON; case 0x34: return KEY_APOSTROPHE; case 0x35: return KEY_GRAVE; case 0x36: return KEY_COMMA; case 0x37: return KEY_DOT; case 0x38: return KEY_SLASH; // Arrow keys case 0x4F: return KEY_RIGHT; case 0x50: return KEY_LEFT; case 0x51: return KEY_DOWN; case 0x52: return KEY_UP; // F-keys case 0x3A: return KEY_F1; case 0x3B: return KEY_F2; case 0x3C: return KEY_F3; case 0x3D: return KEY_F4; case 0x3E: return KEY_F5; case 0x3F: return KEY_F6; case 0x40: return KEY_F7; case 0x41: return KEY_F8; case 0x42: return KEY_F9; case 0x43: return KEY_F10; case 0x44: return KEY_F11; case 0x45: return KEY_F12; case 0x46: return KEY_SYSRQ; case 0x47: return KEY_SCROLLLOCK; case 0x48: return KEY_PAUSE; case 0x49: return KEY_INSERT; case 0x4A: return KEY_HOME; case 0x4B: return KEY_PAGEUP; case 0x4C: return KEY_DELETE; case 0x4D: return KEY_END; case 0x4E: return KEY_PAGEDOWN; } return -1; // Unknown}
// Send analog activation commands to a NuPhy HE keyboardstatic void nuphy_activate_analog(int fd, const char *path) { // NuPhy HE requires two output reports to start streaming analog data: // [0x55, 0xA8] — enable analog mode // [0x55, 0xA0] — start analog streaming unsigned char buf[64]; for (int i = 0; i < 2; i++) { memset(buf, 0, sizeof(buf)); buf[0] = 0x55; buf[1] = (i == 0) ? 0xA8 : 0xA0; int ret = write(fd, buf, sizeof(buf)); fprintf(stderr, "[input] NuPhy activate [0x55, 0x%02X] on %s: %s\n", buf[1], path, ret > 0 ? "OK" : "failed"); }}
// Scan /dev/hidraw* for NuPhy analog keyboardsstatic void hidraw_scan(ACInput *input) { input->hidraw_count = 0; input->has_analog = 0;
DIR *dir = opendir("/dev"); if (!dir) return;
struct dirent *ent; while ((ent = readdir(dir)) && input->hidraw_count < MAX_HIDRAW_DEVICES) { if (strncmp(ent->d_name, "hidraw", 6) != 0) continue;
char path[64]; snprintf(path, sizeof(path), "/dev/%s", ent->d_name); // Open read-write so we can send activation commands int fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC); if (fd < 0) { // Fall back to read-only if RW fails fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (fd < 0) continue; }
// Check vendor ID via HIDIOCGRAWINFO struct hidraw_devinfo info; if (ioctl(fd, HIDIOCGRAWINFO, &info) >= 0) { fprintf(stderr, "[input] hidraw: %s vendor=0x%04x product=0x%04x\n", path, info.vendor, info.product); if (info.vendor == NUPHY_VENDOR_ID) { // Send analog activation commands to all NuPhy interfaces nuphy_activate_analog(fd, path); input->hidraw_fds[input->hidraw_count++] = fd; input->has_analog = 1; fprintf(stderr, "[input] NuPhy HE analog keyboard found: %s\n", path); continue; } } close(fd); } closedir(dir);
if (input->has_analog) fprintf(stderr, "[input] Analog keyboard: %d hidraw device(s)\n", input->hidraw_count);}
static int hidraw_debug_count = 0;
// Process a NuPhy HID report and generate AC events// Protocol (reverse-engineered from NuPhy Air60 HE):// Byte 0: 0xA0 — analog report marker// Byte 1: 0x10 — normal report (0xF0 = special/config)// Bytes 2-3: HID scancode (big-endian), e.g. 0x000E = K key// Bytes 4-5: 16-bit pressure (big-endian), 0x0000=released, ~0x0640=full press// Byte 9: 0x01 = pressing down, 0xFF = releasingstatic void hidraw_process_nuphy(ACInput *input, unsigned char *buf, int len) { // Log first 20 reports for debugging if (hidraw_debug_count < 20) { fprintf(stderr, "[hidraw] report len=%d:", len); for (int i = 0; i < len && i < 16; i++) fprintf(stderr, " %02x", buf[i]); fprintf(stderr, "\n"); hidraw_debug_count++; }
if (len < 10 || buf[0] != 0xA0) return; // Not an analog report if (buf[1] == 0xF0) return; // Config/special report, skip
int scancode = (buf[2] << 8) | buf[3]; int raw_pressure = (buf[4] << 8) | buf[5]; // 0 to ~1600 (0x0640) int direction = buf[9]; // 0x01 = pressing, 0xFF = releasing
// Normalize pressure to 0.0-1.0 (max observed ~1600 = 0x0640) float pressure = raw_pressure / 1600.0f; if (pressure > 1.0f) pressure = 1.0f;
int keycode = nuphy_scancode_to_keycode(scancode); if (keycode < 0) return; // Unknown scancode
// Find existing analog key slot int slot = -1; for (int i = 0; i < MAX_ANALOG_KEYS; i++) { if (input->analog_keys[i].active && input->analog_keys[i].key_code == keycode) { slot = i; break; } }
// Activation threshold: low to capture light touches // raw_pressure ~1600 = full press #define ANALOG_ACTIVATE_THRESHOLD 30 // ~1.9% — very light touch triggers #define ANALOG_DEACTIVATE_THRESHOLD 10 // ~0.6% — hysteresis band
if (raw_pressure >= ANALOG_ACTIVATE_THRESHOLD && slot < 0) { // New key press above threshold — find empty slot for (int i = 0; i < MAX_ANALOG_KEYS; i++) { if (!input->analog_keys[i].active) { slot = i; break; } } if (slot < 0) return; // No slots
input->analog_keys[slot].active = 1; input->analog_keys[slot].releasing = 0; input->analog_keys[slot].key_code = keycode; input->analog_keys[slot].pressure = pressure; input->analog_keys[slot].target = pressure; input->analog_keys[slot].raw_accum = 0; input->analog_keys[slot].raw_count = 0;
// Generate key down event with initial pressure if (input->event_count < MAX_EVENTS_PER_FRAME) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = AC_EVENT_KEYBOARD_DOWN; ae->key_code = keycode; ae->pressure = pressure; const char *name = input_key_name(keycode); if (name) strncpy(ae->key_name, name, sizeof(ae->key_name) - 1); else snprintf(ae->key_name, sizeof(ae->key_name), "?%d", keycode); input->event_count++; } } else if (raw_pressure >= ANALOG_DEACTIVATE_THRESHOLD && slot >= 0) { // Key still pressed — accumulate raw pressure for per-frame averaging input->analog_keys[slot].raw_accum += pressure; input->analog_keys[slot].raw_count++; input->analog_keys[slot].target = pressure; // Track latest raw for when reports stop // Cancel any in-progress release fade if (input->analog_keys[slot].releasing) input->analog_keys[slot].releasing = 0; } else { // Pressure below deactivate threshold or zero — mark as releasing if (slot >= 0 && !input->analog_keys[slot].releasing) { input->analog_keys[slot].releasing = 1; } }}
ACInput *input_init(int screen_w, int screen_h, int scale) { ACInput *input = calloc(1, sizeof(ACInput)); if (!input) return NULL;
input->screen_w = screen_w; input->screen_h = screen_h; input->scale = scale > 0 ? scale : 1; input->last_poll_time = monotonic_sec(); input->abs_prev_x = INT_MIN; input->abs_prev_y = INT_MIN;
// Scan /dev/input/ for event devices DIR *dir = opendir("/dev/input"); if (!dir) { fprintf(stderr, "[input] Cannot open /dev/input\n"); return input; }
struct dirent *ent; while ((ent = readdir(dir)) && input->count < MAX_INPUT_DEVICES) { if (strncmp(ent->d_name, "event", 5) != 0) continue;
char path[64]; snprintf(path, sizeof(path), "/dev/input/%s", ent->d_name); int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (fd < 0) continue;
// Check if this device has keys or touch unsigned long evbits = 0; ioctl(fd, EVIOCGBIT(0, sizeof(evbits)), &evbits); if (evbits & ((1 << EV_KEY) | (1 << EV_ABS) | (1 << EV_REL) | (1 << EV_SW))) { // Check for tablet mode switch support and read initial state if (evbits & (1 << EV_SW)) { unsigned long sw_bits = 0; ioctl(fd, EVIOCGBIT(EV_SW, sizeof(sw_bits)), &sw_bits); if (sw_bits & (1 << SW_TABLET_MODE)) { // Read current switch state unsigned long sw_state = 0; ioctl(fd, EVIOCGSW(sizeof(sw_state)), &sw_state); input->tablet_mode = (sw_state & (1 << SW_TABLET_MODE)) ? 1 : 0; fprintf(stderr, "[input] Opened %s (tablet mode switch, initial=%d)\n", path, input->tablet_mode); } } // Check if this is a NuPhy device (will be handled via hidraw for analog) struct input_id devid; int is_nuphy = 0; if (ioctl(fd, EVIOCGID, &devid) >= 0 && devid.vendor == NUPHY_VENDOR_ID) { is_nuphy = 1; fprintf(stderr, "[input] Opened %s (NuPhy — evdev keys suppressed, using hidraw)\n", path); } else if (!(evbits & (1 << EV_SW)) || (evbits & ((1 << EV_KEY) | (1 << EV_ABS) | (1 << EV_REL)))) { fprintf(stderr, "[input] Opened %s\n", path); } input->fd_is_analog[input->count] = is_nuphy; // Detect absolute trackpads (BCM5974, etc.) — query axis ranges input->fd_is_trackpad[input->count] = 0; if (evbits & (1 << EV_ABS)) { struct input_absinfo abs_x, abs_y; if (ioctl(fd, EVIOCGABS(ABS_MT_POSITION_X), &abs_x) >= 0 && ioctl(fd, EVIOCGABS(ABS_MT_POSITION_Y), &abs_y) >= 0 && (abs_x.maximum - abs_x.minimum) > 1000) { // Wide absolute range = trackpad (not a touchscreen) input->fd_is_trackpad[input->count] = 1; input->abs_x_min = abs_x.minimum; input->abs_x_max = abs_x.maximum; input->abs_y_min = abs_y.minimum; input->abs_y_max = abs_y.maximum; input->abs_x_res = abs_x.resolution > 0 ? abs_x.resolution : 92; input->abs_y_res = abs_y.resolution > 0 ? abs_y.resolution : 91; fprintf(stderr, "[input] Trackpad detected: X[%d..%d] Y[%d..%d] res=%d/%d\n", abs_x.minimum, abs_x.maximum, abs_y.minimum, abs_y.maximum, input->abs_x_res, input->abs_y_res); } } input->fds[input->count++] = fd; } else { close(fd); } } closedir(dir);
fprintf(stderr, "[input] %d evdev devices\n", input->count);
// Sysfs fallback for tablet mode (ThinkPad ACPI) if (!input->tablet_mode) { char tbuf[16] = {0}; FILE *f = fopen("/sys/devices/platform/thinkpad_acpi/hotkey_tablet_mode", "r"); if (f) { if (fgets(tbuf, sizeof(tbuf), f)) input->tablet_mode = (atoi(tbuf) != 0) ? 1 : 0; fclose(f); fprintf(stderr, "[input] sysfs tablet mode: %d\n", input->tablet_mode); } }
// Scan for analog keyboards (NuPhy HE via hidraw) hidraw_scan(input);
return input;}
static int input_debug_frames = 0;
void input_poll(ACInput *input) { if (!input) return; input->event_count = 0; input->delta_x = 0; input->delta_y = 0;
// Debug: log analog state every 5 seconds input_debug_frames++; if (input_debug_frames % 300 == 0 && input->has_analog) { int active = 0, releasing = 0; for (int i = 0; i < MAX_ANALOG_KEYS; i++) { if (input->analog_keys[i].active) active++; if (input->analog_keys[i].releasing) releasing++; } fprintf(stderr, "[input] analog: %d active, %d releasing, has_analog=%d, hidraw=%d, evdev=%d\n", active, releasing, input->has_analog, input->hidraw_count, input->count); }
#ifdef USE_WAYLAND // Wayland path: dispatch Wayland events (keyboard/pointer/touch listeners fire) // then skip evdev polling (compositor grabbed those devices) if (input->is_wayland) { ACWaylandDisplay *wd = input->wayland_display; if (wd && wd->display) { // Full Wayland dispatch: read events from socket then fire listeners. // Must happen AFTER event_count=0 so keyboard/pointer events aren't lost. while (wl_display_prepare_read(wd->display) != 0) wl_display_dispatch_pending(wd->display); wl_display_flush(wd->display); struct pollfd pfd = { .fd = wl_display_get_fd(wd->display), .events = POLLIN }; if (poll(&pfd, 1, 0) > 0) wl_display_read_events(wd->display); else wl_display_cancel_read(wd->display); wl_display_dispatch_pending(wd->display); }
// Software key repeat (Wayland doesn't send value=2 repeat events) if (input->repeat_key >= 0 && input->repeat_rate > 0) { double now = monotonic_sec(); if (now >= input->repeat_next) { if (input->event_count < MAX_EVENTS_PER_FRAME) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = AC_EVENT_KEYBOARD_DOWN; ae->key_code = input->repeat_key; ae->repeat = 1; const char *name = input_key_name(input->repeat_key); if (name) strncpy(ae->key_name, name, sizeof(ae->key_name) - 1); input->event_count++; } input->repeat_next = now + 1.0 / input->repeat_rate; } }
// If we have evdev devices (no udev fallback), poll them too if (input->count > 0) goto poll_evdev;
// Otherwise just poll NuPhy hidraw goto poll_hidraw; }#endif
poll_evdev: ; struct input_event ev; for (int d = 0; d < input->count; d++) { ssize_t rr; while ((rr = read(input->fds[d], &ev, sizeof(ev))) == sizeof(ev)) { if (input->event_count >= MAX_EVENTS_PER_FRAME) break;
ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent));
if (ev.type == EV_KEY) { // Mouse/touch button if (ev.code == BTN_LEFT || ev.code == BTN_TOUCH) { if (ev.value == 1) { ae->type = AC_EVENT_TOUCH; input->pointer_down = 1; } else if (ev.value == 0) { ae->type = AC_EVENT_LIFT; input->pointer_down = 0; input->abs_prev_x = INT_MIN; // Reset trackpad on lift input->abs_prev_y = INT_MIN; } ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; } // Keyboard else if (ev.code < BTN_MISC) { // Skip duplicate evdev events from NuPhy (handled via hidraw analog) if (input->fd_is_analog[d] && input->has_analog) continue; const char *name = input_key_name(ev.code); ae->type = ev.value ? AC_EVENT_KEYBOARD_DOWN : AC_EVENT_KEYBOARD_UP; ae->key_code = ev.code; ae->repeat = (ev.value == 2); if (name) { strncpy(ae->key_name, name, sizeof(ae->key_name) - 1); } else { // Unknown key — store code as name for debug snprintf(ae->key_name, sizeof(ae->key_name), "?%d", ev.code); } input->event_count++; } } else if (ev.type == EV_REL) { // Relative mouse movement if (ev.code == REL_X) { input->pointer_x += ev.value; input->delta_x += ev.value; } if (ev.code == REL_Y) { input->pointer_y += ev.value; input->delta_y += ev.value; } // Clamp to display bounds if (input->pointer_x < 0) input->pointer_x = 0; if (input->pointer_y < 0) input->pointer_y = 0; if (input->pointer_x >= input->screen_w) input->pointer_x = input->screen_w - 1; if (input->pointer_y >= input->screen_h) input->pointer_y = input->screen_h - 1;
if (input->pointer_down) { ae->type = AC_EVENT_DRAW; ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; } } else if (ev.type == EV_SW) { // Switch events (tablet mode, lid, etc.) if (ev.code == SW_TABLET_MODE) { input->tablet_mode = ev.value ? 1 : 0; fprintf(stderr, "[input] tablet mode: %s\n", input->tablet_mode ? "ON" : "OFF"); } } else if (ev.type == EV_ABS) { if (input->fd_is_trackpad[d]) { // Trackpad: convert absolute coords to relative deltas. // BCM5974 reports ~(-4415..5050) for X, ~(-55..6680) for Y. // Use INT_MIN as sentinel for "no previous value this touch". if (ev.code == ABS_MT_POSITION_X || ev.code == ABS_X) { if (input->abs_prev_x != INT_MIN) { int raw_dx = ev.value - input->abs_prev_x; float dx = (float)raw_dx / (float)(input->abs_x_res > 0 ? input->abs_x_res : 92) * 30.0f; input->pointer_x += (int)dx; input->delta_x += (int)dx; } input->abs_prev_x = ev.value; } if (ev.code == ABS_MT_POSITION_Y || ev.code == ABS_Y) { if (input->abs_prev_y != INT_MIN) { int raw_dy = ev.value - input->abs_prev_y; float dy = (float)raw_dy / (float)(input->abs_y_res > 0 ? input->abs_y_res : 91) * 30.0f; input->pointer_y += (int)dy; input->delta_y += (int)dy; } input->abs_prev_y = ev.value; } // Finger lift — reset per-axis tracking if ((ev.code == ABS_MT_TRACKING_ID && ev.value == -1) || (ev.code == BTN_TOUCH && ev.value == 0)) { input->abs_prev_x = INT_MIN; input->abs_prev_y = INT_MIN; } // Clamp to display bounds if (input->pointer_x < 0) input->pointer_x = 0; if (input->pointer_y < 0) input->pointer_y = 0; if (input->pointer_x >= input->screen_w) input->pointer_x = input->screen_w - 1; if (input->pointer_y >= input->screen_h) input->pointer_y = input->screen_h - 1;
if (input->pointer_down) { ae->type = AC_EVENT_DRAW; ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; } } else { // Touchscreen or tablet — map absolute coords to display if (ev.code == ABS_X || ev.code == ABS_MT_POSITION_X) { input->pointer_x = ev.value; if (input->pointer_x >= input->screen_w) input->pointer_x = input->screen_w - 1; if (input->pointer_x < 0) input->pointer_x = 0; } if (ev.code == ABS_Y || ev.code == ABS_MT_POSITION_Y) { input->pointer_y = ev.value; if (input->pointer_y >= input->screen_h) input->pointer_y = input->screen_h - 1; if (input->pointer_y < 0) input->pointer_y = 0; } } } } // Detect dead evdev fd (device unplugged) if (rr < 0 && errno == ENODEV) { fprintf(stderr, "[input] evdev device %d disconnected\n", d); close(input->fds[d]); for (int j = d; j < input->count - 1; j++) { input->fds[j] = input->fds[j + 1]; input->fd_is_analog[j] = input->fd_is_analog[j + 1]; } input->count--; d--; } }
#ifdef USE_WAYLAND poll_hidraw:#endif // Poll NuPhy hidraw devices for analog key data for (int d = 0; d < input->hidraw_count; d++) { unsigned char buf[64]; int n; while ((n = read(input->hidraw_fds[d], buf, sizeof(buf))) > 0) { hidraw_process_nuphy(input, buf, n); if (input->event_count >= MAX_EVENTS_PER_FRAME) break; } // Detect disconnected hidraw (read returns -1 with ENODEV) if (n < 0 && errno == ENODEV) { fprintf(stderr, "[input] NuPhy hidraw device %d disconnected\n", d); close(input->hidraw_fds[d]); // Shift remaining fds down for (int j = d; j < input->hidraw_count - 1; j++) input->hidraw_fds[j] = input->hidraw_fds[j + 1]; input->hidraw_count--; if (input->hidraw_count == 0) { input->has_analog = 0; // Release all analog keys immediately on disconnect for (int i = 0; i < MAX_ANALOG_KEYS; i++) { if (input->analog_keys[i].active) { input->analog_keys[i].active = 0; input->analog_keys[i].releasing = 0; input->analog_keys[i].pressure = 0; if (input->event_count < MAX_EVENTS_PER_FRAME) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = AC_EVENT_KEYBOARD_UP; ae->key_code = input->analog_keys[i].key_code; const char *name = input_key_name(ae->key_code); if (name) strncpy(ae->key_name, name, sizeof(ae->key_name) - 1); input->event_count++; } } } // Also close dead NuPhy evdev fds and rescan for (int i = input->count - 1; i >= 0; i--) { if (input->fd_is_analog[i]) { close(input->fds[i]); for (int j = i; j < input->count - 1; j++) { input->fds[j] = input->fds[j + 1]; input->fd_is_analog[j] = input->fd_is_analog[j + 1]; } input->count--; } } } d--; } }
// Time-based analog key processing (frame-rate independent) double now = monotonic_sec(); double dt = now - input->last_poll_time; if (dt <= 0.0) dt = 0.001; if (dt > 0.1) dt = 0.1; input->last_poll_time = now;
// Linear slide: full 0→1 range in SLIDE_TIME seconds (no ramps, just raw data + linear interp) #define SLIDE_TIME 0.020 // 20ms
for (int i = 0; i < MAX_ANALOG_KEYS; i++) { if (!input->analog_keys[i].active) continue;
// Use latest raw sensor average as target if (input->analog_keys[i].raw_count > 0) { float avg = input->analog_keys[i].raw_accum / input->analog_keys[i].raw_count; input->analog_keys[i].target = avg; input->analog_keys[i].raw_accum = 0; input->analog_keys[i].raw_count = 0; }
float tgt = input->analog_keys[i].releasing ? 0.0f : input->analog_keys[i].target; float prev = input->analog_keys[i].pressure; float diff = tgt - prev; float max_step = (float)(dt / SLIDE_TIME); // max distance per tick if (diff > max_step) diff = max_step; else if (diff < -max_step) diff = -max_step; input->analog_keys[i].pressure = prev + diff;
// Release complete — fire key-up if (input->analog_keys[i].releasing && input->analog_keys[i].pressure <= 0.005f) { input->analog_keys[i].active = 0; input->analog_keys[i].releasing = 0; input->analog_keys[i].pressure = 0; if (input->event_count < MAX_EVENTS_PER_FRAME) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = AC_EVENT_KEYBOARD_UP; ae->key_code = input->analog_keys[i].key_code; ae->pressure = 0; const char *name = input_key_name(ae->key_code); if (name) strncpy(ae->key_name, name, sizeof(ae->key_name) - 1); else snprintf(ae->key_name, sizeof(ae->key_name), "?%d", ae->key_code); input->event_count++; } } }
// Late evdev rescan: pick up ACPI power button and other late devices (once, ~3s after boot) if (!input->evdev_rescan_done) { input->evdev_rescan_counter++; if (input->evdev_rescan_counter >= 180) { // ~3 seconds at 60fps input->evdev_rescan_done = 1; DIR *dir = opendir("/dev/input"); if (dir) { struct dirent *ent; while ((ent = readdir(dir)) && input->count < MAX_INPUT_DEVICES) { if (strncmp(ent->d_name, "event", 5) != 0) continue; char path[64]; snprintf(path, sizeof(path), "/dev/input/%s", ent->d_name); // Check if we already have this fd open int already_open = 0; // Compare by trying to open and checking device identity int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (fd < 0) continue; // Check if any existing fd points to same device (by dev number) struct stat st_new; fstat(fd, &st_new); for (int i = 0; i < input->count; i++) { struct stat st_old; fstat(input->fds[i], &st_old); if (st_new.st_rdev == st_old.st_rdev) { already_open = 1; break; } } if (already_open) { close(fd); continue; } unsigned long evbits = 0; ioctl(fd, EVIOCGBIT(0, sizeof(evbits)), &evbits); if (evbits & ((1 << EV_KEY) | (1 << EV_SW))) { char name[256] = ""; ioctl(fd, EVIOCGNAME(sizeof(name)), name); fprintf(stderr, "[input] Late device: %s (%s)\n", path, name); // Read initial tablet mode state from switch devices if (evbits & (1 << EV_SW)) { unsigned long sw_bits = 0; ioctl(fd, EVIOCGBIT(EV_SW, sizeof(sw_bits)), &sw_bits); if (sw_bits & (1 << SW_TABLET_MODE)) { unsigned long sw_state = 0; ioctl(fd, EVIOCGSW(sizeof(sw_state)), &sw_state); input->tablet_mode = (sw_state & (1 << SW_TABLET_MODE)) ? 1 : 0; fprintf(stderr, "[input] Late tablet mode switch, state=%d\n", input->tablet_mode); } } input->fds[input->count++] = fd; } else { close(fd); } } closedir(dir); } } }
// Hot-plug: periodically re-scan for NuPhy if not connected if (!input->has_analog) { input->hotplug_counter++; if (input->hotplug_counter >= 120) { // ~2 seconds at 60fps input->hotplug_counter = 0; hidraw_scan(input); // If NuPhy reconnected, also rescan evdev for its new event devices if (input->has_analog) { DIR *dir = opendir("/dev/input"); if (dir) { struct dirent *ent; while ((ent = readdir(dir)) && input->count < MAX_INPUT_DEVICES) { if (strncmp(ent->d_name, "event", 5) != 0) continue; char path[64]; snprintf(path, sizeof(path), "/dev/input/%s", ent->d_name); int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (fd < 0) continue; struct stat st_new; fstat(fd, &st_new); int already_open = 0; for (int i = 0; i < input->count; i++) { struct stat st_old; fstat(input->fds[i], &st_old); if (st_new.st_rdev == st_old.st_rdev) { already_open = 1; break; } } if (already_open) { close(fd); continue; } unsigned long evbits = 0; ioctl(fd, EVIOCGBIT(0, sizeof(evbits)), &evbits); if (evbits & (1 << EV_KEY)) { struct input_id devid; int is_nuphy = 0; if (ioctl(fd, EVIOCGID, &devid) >= 0 && devid.vendor == NUPHY_VENDOR_ID) is_nuphy = 1; input->fd_is_analog[input->count] = is_nuphy; input->fds[input->count++] = fd; char name[256] = ""; ioctl(fd, EVIOCGNAME(sizeof(name)), name); fprintf(stderr, "[input] Hotplug evdev: %s (%s)%s\n", path, name, is_nuphy ? " [NuPhy]" : ""); } else { close(fd); } } closedir(dir); } } } }}
// ── Wayland input backend ──
#ifdef USE_WAYLAND
// Wayland keyboard listener — keycodes are evdev keycodes (no offset)static void wl_keyboard_keymap(void *data, struct wl_keyboard *kb, uint32_t format, int32_t fd, uint32_t size) { (void)data; (void)kb; (void)format; (void)size; close(fd); // We don't use xkb keymap — we map evdev codes directly}
static void wl_keyboard_enter(void *data, struct wl_keyboard *kb, uint32_t serial, struct wl_surface *surface, struct wl_array *keys) { (void)data; (void)kb; (void)serial; (void)surface; (void)keys;}
static void wl_keyboard_leave(void *data, struct wl_keyboard *kb, uint32_t serial, struct wl_surface *surface) { (void)data; (void)kb; (void)serial; (void)surface;}
static void wl_keyboard_key(void *data, struct wl_keyboard *kb, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { (void)kb; (void)serial; (void)time; ACInput *input = data; if (input->event_count >= MAX_EVENTS_PER_FRAME) return;
// Skip evdev keys from NuPhy (handled via hidraw analog) if (input->has_analog) { int kc = (int)key; for (int i = 0; i < MAX_ANALOG_KEYS; i++) { if (input->analog_keys[i].active && input->analog_keys[i].key_code == kc) return; // This key is being tracked via analog } }
ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = state ? AC_EVENT_KEYBOARD_DOWN : AC_EVENT_KEYBOARD_UP; ae->key_code = key; ae->pressure = state ? 1.0f : 0.0f; const char *name = input_key_name(key); if (name) strncpy(ae->key_name, name, sizeof(ae->key_name) - 1); else snprintf(ae->key_name, sizeof(ae->key_name), "?%d", key); input->event_count++;
// Key repeat tracking if (state) { input->repeat_key = key; double now = monotonic_sec(); input->repeat_start = now; input->repeat_next = now + input->repeat_delay / 1000.0; } else if ((int)key == input->repeat_key) { input->repeat_key = -1; }}
static void wl_keyboard_modifiers(void *data, struct wl_keyboard *kb, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { (void)data; (void)kb; (void)serial; (void)mods_depressed; (void)mods_latched; (void)mods_locked; (void)group;}
static void wl_keyboard_repeat_info(void *data, struct wl_keyboard *kb, int32_t rate, int32_t delay) { (void)kb; ACInput *input = data; input->repeat_rate = rate; // keys per second input->repeat_delay = delay; // ms fprintf(stderr, "[input] Wayland key repeat: rate=%d/s delay=%dms\n", rate, delay);}
static const struct wl_keyboard_listener keyboard_listener = { .keymap = wl_keyboard_keymap, .enter = wl_keyboard_enter, .leave = wl_keyboard_leave, .key = wl_keyboard_key, .modifiers = wl_keyboard_modifiers, .repeat_info = wl_keyboard_repeat_info,};
// Wayland pointer listenerstatic void wl_pointer_enter(void *data, struct wl_pointer *ptr, uint32_t serial, struct wl_surface *surface, wl_fixed_t sx, wl_fixed_t sy) { (void)surface; ACInput *input = data; input->pointer_x = wl_fixed_to_int(sx); input->pointer_y = wl_fixed_to_int(sy); // Hide the Wayland compositor cursor — ac-native renders its own. wl_pointer_set_cursor(ptr, serial, NULL, 0, 0);}
static void wl_pointer_leave(void *data, struct wl_pointer *ptr, uint32_t serial, struct wl_surface *surface) { (void)data; (void)ptr; (void)serial; (void)surface;}
static void wl_pointer_motion(void *data, struct wl_pointer *ptr, uint32_t time, wl_fixed_t sx, wl_fixed_t sy) { (void)ptr; (void)time; ACInput *input = data; int old_x = input->pointer_x, old_y = input->pointer_y; input->pointer_x = wl_fixed_to_int(sx); input->pointer_y = wl_fixed_to_int(sy); input->delta_x += input->pointer_x - old_x; input->delta_y += input->pointer_y - old_y;
if (input->pointer_down && input->event_count < MAX_EVENTS_PER_FRAME) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = AC_EVENT_DRAW; ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; }}
static void wl_pointer_button(void *data, struct wl_pointer *ptr, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { (void)ptr; (void)serial; (void)time; ACInput *input = data; if (input->event_count >= MAX_EVENTS_PER_FRAME) return;
// BTN_LEFT = 0x110 (272) if (button == 272 || button == 0x110) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); if (state) { ae->type = AC_EVENT_TOUCH; input->pointer_down = 1; } else { ae->type = AC_EVENT_LIFT; input->pointer_down = 0; } ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; }}
static void wl_pointer_axis(void *data, struct wl_pointer *ptr, uint32_t time, uint32_t axis, wl_fixed_t value) { (void)data; (void)ptr; (void)time; (void)axis; (void)value;}
static void wl_pointer_frame(void *data, struct wl_pointer *ptr) { (void)data; (void)ptr;}
static void wl_pointer_axis_source(void *data, struct wl_pointer *ptr, uint32_t source) { (void)data; (void)ptr; (void)source;}
static void wl_pointer_axis_stop(void *data, struct wl_pointer *ptr, uint32_t time, uint32_t axis) { (void)data; (void)ptr; (void)time; (void)axis;}
static void wl_pointer_axis_discrete(void *data, struct wl_pointer *ptr, uint32_t axis, int32_t discrete) { (void)data; (void)ptr; (void)axis; (void)discrete;}
static const struct wl_pointer_listener pointer_listener = { .enter = wl_pointer_enter, .leave = wl_pointer_leave, .motion = wl_pointer_motion, .button = wl_pointer_button, .axis = wl_pointer_axis, .frame = wl_pointer_frame, .axis_source = wl_pointer_axis_source, .axis_stop = wl_pointer_axis_stop, .axis_discrete = wl_pointer_axis_discrete,};
// Wayland touch listenerstatic void wl_touch_down(void *data, struct wl_touch *touch, uint32_t serial, uint32_t time, struct wl_surface *surface, int32_t id, wl_fixed_t x, wl_fixed_t y) { (void)touch; (void)serial; (void)time; (void)surface; (void)id; ACInput *input = data; input->pointer_x = wl_fixed_to_int(x); input->pointer_y = wl_fixed_to_int(y); input->pointer_down = 1;
if (input->event_count < MAX_EVENTS_PER_FRAME) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = AC_EVENT_TOUCH; ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; }}
static void wl_touch_up(void *data, struct wl_touch *touch, uint32_t serial, uint32_t time, int32_t id) { (void)touch; (void)serial; (void)time; (void)id; ACInput *input = data; input->pointer_down = 0;
if (input->event_count < MAX_EVENTS_PER_FRAME) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = AC_EVENT_LIFT; ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; }}
static void wl_touch_motion(void *data, struct wl_touch *touch, uint32_t time, int32_t id, wl_fixed_t x, wl_fixed_t y) { (void)touch; (void)time; (void)id; ACInput *input = data; input->pointer_x = wl_fixed_to_int(x); input->pointer_y = wl_fixed_to_int(y);
if (input->pointer_down && input->event_count < MAX_EVENTS_PER_FRAME) { ACEvent *ae = &input->events[input->event_count]; memset(ae, 0, sizeof(ACEvent)); ae->type = AC_EVENT_DRAW; ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; }}
static void wl_touch_frame(void *data, struct wl_touch *touch) { (void)data; (void)touch;}
static void wl_touch_cancel(void *data, struct wl_touch *touch) { (void)touch; ACInput *input = data; input->pointer_down = 0;}
static const struct wl_touch_listener touch_listener = { .down = wl_touch_down, .up = wl_touch_up, .motion = wl_touch_motion, .frame = wl_touch_frame, .cancel = wl_touch_cancel,};
// Seat capabilities listenerstatic void seat_capabilities(void *data, struct wl_seat *seat, uint32_t caps) { ACInput *input = data; ACWaylandDisplay *wd = input->wayland_display;
if ((caps & WL_SEAT_CAPABILITY_KEYBOARD) && !wd->keyboard) { wd->keyboard = wl_seat_get_keyboard(seat); wl_keyboard_add_listener(wd->keyboard, &keyboard_listener, input); fprintf(stderr, "[input] Wayland keyboard bound\n"); } if ((caps & WL_SEAT_CAPABILITY_POINTER) && !wd->pointer) { wd->pointer = wl_seat_get_pointer(seat); wl_pointer_add_listener(wd->pointer, &pointer_listener, input); fprintf(stderr, "[input] Wayland pointer bound\n"); } if ((caps & WL_SEAT_CAPABILITY_TOUCH) && !wd->touch) { wd->touch = wl_seat_get_touch(seat); wl_touch_add_listener(wd->touch, &touch_listener, input); fprintf(stderr, "[input] Wayland touch bound\n"); }}
static void seat_name(void *data, struct wl_seat *seat, const char *name) { (void)data; (void)seat; fprintf(stderr, "[input] Wayland seat: %s\n", name);}
static const struct wl_seat_listener seat_listener = { .capabilities = seat_capabilities, .name = seat_name,};
ACInput *input_init_wayland(void *wayland_display, int screen_w, int screen_h, int scale) { ACWaylandDisplay *wd = wayland_display; ACInput *input = calloc(1, sizeof(ACInput)); if (!input) return NULL;
input->screen_w = screen_w; input->screen_h = screen_h; input->scale = scale > 0 ? scale : 1; input->last_poll_time = monotonic_sec(); input->is_wayland = 1; input->wayland_display = wd; input->repeat_key = -1; input->repeat_rate = 25; // default: 25 keys/sec input->repeat_delay = 600; // default: 600ms
wd->input = input;
// Bind keyboard/pointer/touch from cached seat capabilities. // The seat listener was already added in registry_global (init_seat_listener) // which cached caps in wd->seat_caps during wayland_display_init roundtrips. // We can't replace that listener (Wayland allows only one per proxy), // so we manually bind devices here using the cached caps value. if (wd->seat && wd->seat_caps) { // This calls the same logic as seat_capabilities but with our ACInput* seat_capabilities(input, wd->seat, wd->seat_caps); wl_display_roundtrip(wd->display); } extern void ac_log(const char *fmt, ...); ac_log("[input] Wayland seat: caps=0x%x kb=%p ptr=%p touch=%p\n", wd->seat_caps, (void*)wd->keyboard, (void*)wd->pointer, (void*)wd->touch);
// If compositor didn't advertise input (no udev/libinput), fall back to evdev if (!wd->seat_caps) { ac_log("[input] No Wayland seat caps — using evdev directly\n"); DIR *dir = opendir("/dev/input"); if (dir) { struct dirent *ent; while ((ent = readdir(dir)) && input->count < MAX_INPUT_DEVICES) { if (strncmp(ent->d_name, "event", 5) != 0) continue; char path[64]; snprintf(path, sizeof(path), "/dev/input/%s", ent->d_name); int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); if (fd < 0) continue; unsigned long evbits = 0; ioctl(fd, EVIOCGBIT(0, sizeof(evbits)), &evbits); if (evbits & ((1 << EV_KEY) | (1 << EV_ABS) | (1 << EV_REL) | (1 << EV_SW))) { if (evbits & (1 << EV_SW)) { unsigned long sw_bits = 0; ioctl(fd, EVIOCGBIT(EV_SW, sizeof(sw_bits)), &sw_bits); if (sw_bits & (1 << SW_TABLET_MODE)) { unsigned long sw_state = 0; ioctl(fd, EVIOCGSW(sizeof(sw_state)), &sw_state); input->tablet_mode = (sw_state & (1 << SW_TABLET_MODE)) ? 1 : 0; } } struct input_id devid; int is_nuphy = 0; if (ioctl(fd, EVIOCGID, &devid) >= 0 && devid.vendor == NUPHY_VENDOR_ID) is_nuphy = 1; input->fd_is_analog[input->count] = is_nuphy; input->fds[input->count++] = fd; } else { close(fd); } } closedir(dir); ac_log("[input] evdev fallback: %d devices\n", input->count); } }
// Scan for NuPhy hidraw hidraw_scan(input);
return input;}#endif // USE_WAYLAND
void input_destroy(ACInput *input) { if (!input) return; for (int i = 0; i < input->count; i++) close(input->fds[i]); for (int i = 0; i < input->hidraw_count; i++) close(input->hidraw_fds[i]); free(input);}