diff options
-rw-r--r-- | CMakeLists.txt | 5 | ||||
-rw-r--r-- | README.ans | 67 | ||||
-rw-r--r-- | src/main.c | 392 |
3 files changed, 464 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a42f488 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.30) + +project(shibari LANGUAGES C) + +add_executable(${PROJECT_NAME} src/main.c) diff --git a/README.ans b/README.ans new file mode 100644 index 0000000..932f2f4 --- /dev/null +++ b/README.ans @@ -0,0 +1,67 @@ +[0;31m + .. + .$$S. .,. + .S$$$$; .,,,,,,,. + ;$$$$. .,,;;,,,,. + ;S;,;;,,. + .;;,;, + ,s, s$; + .,.;$$S + .s$$$, + .;S$$$;. + s$s;. ,;. + .;S$$S .,;,. + .s$S;. .,;;. +.;sS$S;. ,$$s. .;;. .,S$;. +.;sS$$$$$$$s,. ,$$S. .;;. .;S$SSS$$;. + .;S$$$$$$$$$Ss;. ,$$$. .;, s$$$$$$$Ss;,. + ,sS$$$$$$$$$Ss;,.,s;. ,, ,$$$$$$$$$$S;. + .,;S$$$$$$$$$$$S, .sSSSsS$$$$$$$; + .,;s$$$$$$$, .S$$$SS$$, + .;;,sS, .,ss,.,,,. + .,,,,s$$s. ..,;..,,;;,,,,,. + .,,,,,,,,,;;,,,;$$$, .,;;. .,,,,,;;,,,,,. + .,,,,,,,;;,,,,,,,,,. .s$$. .,;;. .,,,,,;;,,,,. + .,;;,,,,,,,. .$$$s .;;,. .,,,,,. + .,,. ,$$$$. .;;. + .$$$$s .;;. + S$$$. .;;. + ;$S,,,,,. + .;;;, + .,;;.S$s + .;;,. $$$ + .;, .s$$; + ..,$$$; + ,s$$$;. + ,S$$$S.,;. + .S$$S,. .;;. + .;;,. .,. +[0m + +shibari is a program that reads inputs from various devices and +executes user-specified bindings. + +It uses the evdev interface exposed by the Linux kernel, which means +it can handle keyboards as well as mouse and gamepad buttons and that +it can function indiferrently of being run in X11, Wayland or in a +headless setup. + +shibari requires read access to files in /dev/input and +/sys/class/input. + +Bindings are specified in a bindings file searched for at, in order of +preference: + - the path given as argument to the "-c" option + - $XDG_CONFIG_HOME/shibari/bindings.conf + - $HOME/.config/shibari/bindings.conf + +The bindings file expects one binding per line of the form: + +"<code> <press|release> <cmd>" + +where <code> is the key code which can be seen by running shibari with +the "-t" option, <press|release> specifies wether the binding should +be run when the key is pressed or released and <cmd> is a command +string that will be run by /bin/sh when the binding is activated. + +Empty lines and anything after a "#" is ignored. diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..7ecfc15 --- /dev/null +++ b/src/main.c @@ -0,0 +1,392 @@ +#define _DEFAULT_SOURCE +#include <stdlib.h> +#include <errno.h> +#include <string.h> +#include <stdio.h> +#include <poll.h> +#include <unistd.h> +#include <fcntl.h> +#include <dirent.h> +#include <ctype.h> +#include <linux/input.h> +#include <linux/limits.h> +#include <sys/inotify.h> +#include <pwd.h> +#include <termios.h> + +#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); +} |