#define _DEFAULT_SOURCE #include "binding.h" #include "bindings_file.h" #include #include #include #include #include #include #include #include #include #include #include #include #define NPFDS 4096 struct pollfd pfds[NPFDS] = {0}; int open_devices[NPFDS] = {0}; int test_mode = 0; uint64_t keys[KEYS_SIZE] = {0}; int parse_device_name(const char name[static 1], unsigned long *device_id) { if (strncmp(name, "event", strlen("event")) != 0) { return -1; } char *endptr; *device_id = strtoul(name + strlen("event"), &endptr, 10); if (*endptr != '\0') { return -1; } return 0; } void add_device(const char name[static 1]) { unsigned long device_id; if (parse_device_name(name, &device_id) == -1) { return; } if (device_id >= NPFDS) { fprintf(stderr, "exceeded device limit\n"); return; } if (open_devices[device_id]) { return; } char path[NAME_MAX + 1 + 11] = "/dev/input/"; /* 11 == strlen("/dev/input/") */ strcat(path, name); int fd = open(path, O_RDONLY | O_NONBLOCK); if (fd < 0) { fprintf(stderr, "failed to open device: %s: %s\n", path, strerror(errno)); return; } open_devices[device_id] = 1; for (size_t i = 1; i < NPFDS; i++) { if (pfds[i].fd < 0) { pfds[i].fd = fd; return; } } } void del_device(const char name[static 1]) { unsigned long device_id; if (parse_device_name(name, &device_id) == -1) { return; } if (device_id >= NPFDS) { return; } open_devices[device_id] = 0; } void process_inotify_watch(int inotify_fd) { char buf[sizeof (struct inotify_event) + NAME_MAX + 1] = {0}; ssize_t len = read(inotify_fd, buf, sizeof buf); if (len < 0) { fprintf(stderr, "read on inotify fd failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } const struct inotify_event *event = NULL; for (char *ptr = buf; ptr < buf + len; ptr += sizeof(struct inotify_event) + event->len) { event = (const struct inotify_event *) ptr; if (event->mask & (IN_IGNORED | IN_UNMOUNT | IN_Q_OVERFLOW)) { fputs("inotify error (filesystem unmounted?)", stderr); exit(EXIT_FAILURE); } else if (event->mask & IN_DELETE) { del_device(event->name); } else { add_device(event->name); } } } void run_binding(struct binding *binding) { if (fork() == 0) { execl("/bin/sh", "/bin/sh", "-c", binding->cmd); } } void process_key_event(int code, int value) { uint64_t old_keys[KEYS_SIZE]; for (size_t i = 0; i < KEYS_SIZE; i++) { old_keys[i] = keys[i]; } if (value == RELEASED) { clear_key(keys, code); } else if (value == PRESSED) { set_key(keys, code); } for (size_t i = 0; i < nbindings; i++) { if (bindings[i].value == value) { uint64_t *match_target = value == PRESSED ? keys : old_keys; if (is_keys_subset_of(bindings[i].keys, match_target) && has_key(bindings[i].keys, code)) { run_binding(bindings + i); } } } } void process_input_device(int fd) { struct input_event event; if (read(fd, &event, sizeof event) != sizeof event) { fputs("invalid read on input device\n", stderr); return; } if (event.type == EV_KEY && event.value != 2) { if (test_mode) { printf("%d %s\n", event.code, event.value == 0 ? "release" : "press"); } else { process_key_event(event.code, event.value); } } } void add_existing_devices() { DIR *dirp = opendir("/dev/input"); if (dirp == NULL) { fprintf(stderr, "opendir failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } errno = 0; struct dirent *dirent = NULL; while ((dirent = readdir(dirp)) != NULL) { if (dirent->d_type == DT_CHR) { add_device(dirent->d_name); } } if (errno != 0) { fprintf(stderr, "readdir failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } } int run(const char *bindings_file_path) { if (!test_mode) { parse_bindings_file(bindings_file_path); } int inotify_fd = inotify_init1(IN_CLOEXEC); if (inotify_fd < 0) { fprintf(stderr, "inotify_init1 failed: %s\n", strerror(errno)); return EXIT_FAILURE; } int watch_fd = inotify_add_watch(inotify_fd, "/dev/input/", IN_CREATE | IN_ATTRIB | IN_DELETE); if (watch_fd < 0) { fprintf(stderr, "inotify_add_watch failed: %s\n", strerror(errno)); return EXIT_FAILURE; } for (size_t i = 0; i < NPFDS; i++) { pfds[i].fd = -1; pfds[i].events = POLLIN; } pfds[0].fd = inotify_fd; add_existing_devices(); for (;;) { int ready = poll(pfds, NPFDS, -1); if (ready < 0) { fprintf(stderr, "poll failed: %s\n", strerror(errno)); return EXIT_FAILURE; } if (pfds[0].revents == POLLIN) { process_inotify_watch(inotify_fd); } else if (pfds[0].revents != 0) { fprintf(stderr, "inotify watch error\n"); return EXIT_FAILURE; } for (size_t i = 1; i < NPFDS; i++) { if (pfds[i].fd >= 0) { if (pfds[i].revents == POLLIN) { process_input_device(pfds[i].fd); } else if (pfds[i].revents != 0) { close(pfds[i].fd); pfds[i].fd = -1; } } } } return EXIT_SUCCESS; } char *get_home_dir() { char *home = getenv("HOME"); if (home == NULL) { errno = 0; home = getpwuid(getuid())->pw_dir; if (errno != 0) { fprintf(stderr, "could not find home dir: %s\n", strerror(errno)); exit(EXIT_FAILURE); } } char *ret = malloc(strlen(home) + 1); strcpy(ret, home); return ret; } char *default_bindings_file_path() { char *config_home = getenv("XDG_CONFIG_HOME"); char *path = NULL; if (config_home != NULL) { path = malloc(strlen(config_home) + 1); strcpy(path, config_home); } else { path = get_home_dir(); path = realloc(path, strlen(path) + strlen("/.config") + 1); strcat(path, "/.config"); } path = realloc(path, strlen(path) + strlen("/shibari/bindings.conf") + 1); strcat(path, "/shibari/bindings.conf"); return path; } void usage(const char *argv0) { printf("Usage: %s [-ht] [-c file]\n" "Reads linux evdev inputs and executes the configured user bindings." " -h Show this help\n" " -t Test mode: no binding is run and all inputs are printed\n" " -c file Use the specified bindings file\n", argv0); } int main(int argc, char *argv[static argc]) { int opt; char *bindings_file_path = NULL; while ((opt = getopt(argc, argv, "hc:t")) != -1) { switch (opt) { case 'h': usage(argv[0]); return EXIT_SUCCESS; case '?': usage(argv[0]); return EXIT_FAILURE; case 't': test_mode = 1; break; case 'c': bindings_file_path = optarg; break; } } if (optind != argc) { fprintf(stderr, "unrecognized option: %s\n", argv[optind]); usage(argv[0]); return EXIT_FAILURE; } if (bindings_file_path == NULL && !test_mode) { bindings_file_path = default_bindings_file_path(); } if (test_mode) { struct termios t; tcgetattr(0, &t); t.c_lflag &= ~ECHO; tcsetattr(0, TCSANOW, &t); } return run(bindings_file_path); }