Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425#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_SYSRQ: return "printscreen"; case KEY_LEFTMETA: case KEY_RIGHTMETA: return "meta"; 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; } }}
// ── Multitouch surface ───────────────────────────────────────────────// MT-B delivers fingers as a slot cursor (ABS_MT_SLOT) followed by that// slot's fields, committed at SYN_REPORT. We keep every contact rather// than folding them into one pointer: a drum head needs the resting// fingers as much as the striking one, since they damp its modes.
static void surface_clear(ACTouchSurface *s) { for (int i = 0; i < MAX_TOUCH_SLOTS; i++) { s->slots[i].tracking_id = -1; s->slots[i].have_pos = 0; } s->slot = 0; s->contacts = 0; s->primary_id = -1;}
// Fold one ABS_MT_* field into the addressed slot. Nothing is normalized// or counted here — SYN_REPORT commits the frame.static void surface_abs(ACTouchSurface *s, const struct input_event *ev) { if (ev->code == ABS_MT_SLOT) { // A pad with more slots than we track addresses the extras into // -1, where every later field is discarded until the next slot. s->slot = (ev->value >= 0 && ev->value < MAX_TOUCH_SLOTS) ? ev->value : -1; return; } if (s->slot < 0) return; ACTouchSlot *t = &s->slots[s->slot];
switch (ev->code) { case ABS_MT_TRACKING_ID: if (ev->value < 0) { t->tracking_id = -1; t->have_pos = 0; } else if (t->tracking_id != ev->value) { t->tracking_id = ev->value; t->have_pos = 0; t->pressure = 0; t->down_at = monotonic_sec(); } break; case ABS_MT_POSITION_X: t->raw_x = ev->value; t->have_pos |= 1; break; case ABS_MT_POSITION_Y: t->raw_y = ev->value; t->have_pos |= 2; break; case ABS_MT_PRESSURE: { // Pads disagree wildly on full scale; 255 is the common ceiling // and clamping is kinder than reading EVIOCGABS per contact. float p = ev->value / 255.0f; t->pressure = p < 0 ? 0 : (p > 1 ? 1 : p); break; } default: break; }}
// Commit a frame: normalize live contacts, recount, and hand the pointer// its delta from whichever finger is driving it.static void surface_sync(ACInput *input) { ACTouchSurface *s = &input->surface; float span_x = (float)(s->x_max - s->x_min); float span_y = (float)(s->y_max - s->y_min); if (span_x <= 0) span_x = 1; if (span_y <= 0) span_y = 1;
int contacts = 0, primary_slot = -1; for (int i = 0; i < MAX_TOUCH_SLOTS; i++) { ACTouchSlot *t = &s->slots[i]; if (t->tracking_id < 0 || t->have_pos != 3) continue; float x = (t->raw_x - s->x_min) / span_x; float y = (t->raw_y - s->y_min) / span_y; t->x = x < 0 ? 0 : (x > 1 ? 1 : x); t->y = y < 0 ? 0 : (y > 1 ? 1 : y); contacts++; // Keep the finger that already owns the pointer; otherwise adopt // the lowest live slot, so a lift hands off without a cursor jump. if (t->tracking_id == s->primary_id) primary_slot = i; else if (primary_slot < 0 && s->primary_id < 0) primary_slot = i; } if (primary_slot < 0 && contacts > 0) { for (int i = 0; i < MAX_TOUCH_SLOTS; i++) { ACTouchSlot *t = &s->slots[i]; if (t->tracking_id >= 0 && t->have_pos == 3) { primary_slot = i; break; } } }
if (contacts != s->contacts) s->generation++; s->contacts = contacts;
if (primary_slot < 0) { s->primary_id = -1; return; }
// Relative pointer motion, from the primary contact only. The old path // summed every finger's absolute jumps into one delta, so a two-finger // rest made the cursor bolt across the screen. ACTouchSlot *p = &s->slots[primary_slot]; if (p->tracking_id == s->primary_id) { int dx_raw = p->raw_x - s->primary_x; int dy_raw = p->raw_y - s->primary_y; float dx = (float)dx_raw / (float)(s->x_res > 0 ? s->x_res : 92) * 30.0f; float dy = (float)dy_raw / (float)(s->y_res > 0 ? s->y_res : 91) * 30.0f; input->pointer_x += (int)dx; input->pointer_y += (int)dy; input->delta_x += (int)dx; input->delta_y += (int)dy; 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; } s->primary_id = p->tracking_id; s->primary_x = p->raw_x; s->primary_y = p->raw_y;}
int input_touch_contacts(const ACInput *input, ACTouchSlot *out, int max) { if (!input || !out || max <= 0) return 0; const ACTouchSurface *s = &input->surface; int n = 0; for (int i = 0; i < MAX_TOUCH_SLOTS && n < max; i++) { if (s->slots[i].tracking_id < 0 || s->slots[i].have_pos != 3) continue; out[n++] = s->slots[i]; } return n;}
float input_touch_aspect(const ACInput *input) { if (!input) return 1.0f; const ACTouchSurface *s = &input->surface; if (s->x_res <= 0 || s->y_res <= 0) return 1.0f; float w = (float)(s->x_max - s->x_min) / (float)s->x_res; float h = (float)(s->y_max - s->y_min) / (float)s->y_res; return (w > 0 && h > 0) ? w / h : 1.0f;}
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; input->surface.fd_index = -1; input->surface.aspect = 1.0f; surface_clear(&input->surface);
// 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; // INPUT_PROP_DIRECT marks a device you touch *on* the image // (touchscreen); INPUT_PROP_POINTER marks one that moves a // cursor. Convertibles carry both, and the old range test // called the touchscreen a trackpad. unsigned long props = 0; ioctl(fd, EVIOCGPROP(sizeof(props)), &props); int direct = (props & (1UL << INPUT_PROP_DIRECT)) != 0; if (!direct && 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) { 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;
// Claim the first trackpad as the playable surface. if (input->surface.fd_index < 0) { ACTouchSurface *s = &input->surface; s->fd_index = input->count; s->x_min = abs_x.minimum; s->x_max = abs_x.maximum; s->y_min = abs_y.minimum; s->y_max = abs_y.maximum; s->x_res = abs_x.resolution; s->y_res = abs_y.resolution; struct input_absinfo abs_slot; s->has_slots = ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &abs_slot) >= 0; s->aspect = input_touch_aspect(input); fprintf(stderr, "[input] Surface: %d slots, %.2f:1 aspect\n", s->has_slots ? abs_slot.maximum + 1 : 1, s->aspect); } 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;
// AC_TOUCH_DEBUG=1 logs every change to the contact set — the fastest way// to see what a new pad reports before any sound is wired to it.static int touch_debug = -1;
void input_poll(ACInput *input) { if (touch_debug < 0) { const char *e = getenv("AC_TOUCH_DEBUG"); touch_debug = (e && *e && *e != '0') ? 1 : 0; } 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; } }
// Under Wayland, cage grabs keyboard/mouse/touchpad via libinput. // Do NOT also read those devices via evdev — double input causes // progressive cursor drift (Wayland sets absolute pos, evdev adds // relative deltas on top). Only poll NuPhy hidraw for analog keys. 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. Raw deltas are in PHYSICAL pixels // and event coords are later divided by `scale`, so on a // hi-DPI panel the pointer crawled (1 mouse unit = 1/scale // logical px). Apply gain = scale * MOUSE_SENS so motion feels // natural in logical space, with a fractional accumulator to // preserve sub-pixel precision (no lost slow movement). static const float MOUSE_SENS = 1.6f; static float relacc_x = 0.f, relacc_y = 0.f; float gain = (input->scale > 0 ? input->scale : 1) * MOUSE_SENS; if (ev.code == REL_X) { relacc_x += ev.value * gain; int d = (int)relacc_x; relacc_x -= d; input->pointer_x += d; input->delta_x += d; } if (ev.code == REL_Y) { relacc_y += ev.value * gain; int d = (int)relacc_y; relacc_y -= d; input->pointer_y += d; input->delta_y += d; } // 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_SYN) { // Commit the surface frame. Contacts only make sense as a // set — a strike and the fingers damping it arrive in the // same report, and reading mid-frame would mix them up. if (ev.code == SYN_REPORT && d == input->surface.fd_index && input->surface.has_slots) { int before = input->surface.generation; int px = input->pointer_x, py = input->pointer_y; surface_sync(input); if (input->pointer_down && (input->pointer_x != px || input->pointer_y != py)) { ae->type = AC_EVENT_DRAW; ae->x = input->pointer_x / input->scale; ae->y = input->pointer_y / input->scale; input->event_count++; } if (touch_debug && input->surface.generation != before) { ACTouchSlot pts[MAX_TOUCH_SLOTS]; int n = input_touch_contacts(input, pts, MAX_TOUCH_SLOTS); fprintf(stderr, "[touch] %d contact%s:", n, n == 1 ? "" : "s"); for (int i = 0; i < n; i++) fprintf(stderr, " (%.3f,%.3f p%.2f)", pts[i].x, pts[i].y, pts[i].pressure); fprintf(stderr, "\n"); } } } else if (ev.type == EV_ABS) { if (d == input->surface.fd_index && input->surface.has_slots) { // MT-B pad: slots own the contacts, and surface_sync // drives the pointer from the primary one at SYN. surface_abs(&input->surface, &ev); } else 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->fd_is_trackpad[j] = input->fd_is_trackpad[j + 1]; } input->count--; // The surface indexes into the same array, so a device leaving // below it slides it down; losing the pad itself drops it. if (input->surface.fd_index == d) { input->surface.fd_index = -1; input->surface.has_slots = 0; surface_clear(&input->surface); } else if (input->surface.fd_index > d) { input->surface.fd_index--; } 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 // No raw surface under Wayland — the compositor owns the pad. Clear the // slots anyway so a zeroed tracking id never reads as a live finger. input->surface.fd_index = -1; input->surface.aspect = 1.0f; surface_clear(&input->surface);
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);}