summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt5
-rw-r--r--README.ans67
-rw-r--r--src/main.c392
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 @@
+
+ ..
+ .$$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,. .;;.
+ .;;,. .,.
+
+
+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);
+}