#define _DEFAULT_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define NPFDS 4096 struct binding { int code; int value; char *cmd; }; struct pollfd pfds[NPFDS] = {0}; int test_mode = 0; size_t nbindings = 0; struct binding *bindings = NULL; void add_device(const char name[static 1]) { if (strncmp(name, "event", strlen("event")) != 0) { 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; } for (size_t i = 1; i < NPFDS; i++) { if (pfds[i].fd < 0) { pfds[i].fd = fd; return; } } } 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); } add_device(event->name); } } void skip_space(char **c) { for (; **c && isspace(**c); (*c)++); } int read_int(char **c, int out[static 1]) { if (!isdigit(**c)) { return -1; } errno = 0; char *end = NULL; *out = strtol(*c, &end, 10); if (errno) { return -1; } *c = end; return 0; } void parse_bindings_file(const char path[static 1]) { size_t cap = 512; bindings = malloc(sizeof (struct binding) * cap); FILE *f = fopen(path, "r"); if (f == NULL) { fprintf(stderr, "failed to open bindings file: %s: %s\n", path, strerror(errno)); exit(EXIT_FAILURE); } size_t line_nb = 0; char *line = NULL; size_t n; while (getline(&line, &n, f) != -1) { line_nb++; struct binding binding; char *end = strchr(line, '#'); if (end == NULL) { end = line + strlen(line); } *end = '\0'; char *c = line; skip_space(&c); if (c == end) continue; /* code */ if (read_int(&c, &binding.code) < 0 || c >= end) { goto fail; } /* press | release */ skip_space(&c); if (strncmp(c, "press", strlen("press")) == 0) { c += strlen("press"); binding.value = 1; } else if (strncmp(c, "release", strlen("release")) == 0) { c += strlen("release"); binding.value = 0; } else { goto fail; } /* cmd */ skip_space(&c); if (c >= end) { goto fail; } binding.cmd = malloc(end - c + 1); memcpy(binding.cmd, c, end - c + 1); if (nbindings >= cap) { cap *= 2; bindings = realloc(bindings, cap * sizeof (struct binding)); } bindings[nbindings++] = binding; } if (!feof(f)) { fprintf(stderr, "failed to read bindings file: %s: %s\n", path, strerror(errno)); } free(line); fclose(f); return; fail: fprintf(stderr, "syntax error at line %ld\n", line_nb); exit(EXIT_FAILURE); } void run_binding(int code, int value) { for (size_t i = 0; i < nbindings; i++) { if (bindings[i].code == code && bindings[i].value == value) { if (fork() == 0) { execl("/bin/sh", "/bin/sh", "-c", bindings[i].cmd); } } } } 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 { run_binding(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); 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); }