linput

Listen to input events
git clone git://git.akobets.xyz/linput
Log | Files | Refs | README | LICENSE

commit d9b723d028b6ab5b0f02932a6e9edf6abcc8f2b7
Author: Artem Kobets <artem@akobets.xyz>
Date:   Thu, 16 Apr 2020 23:06:36 +0300

initial commit

Diffstat:
ALICENSE | 21+++++++++++++++++++++
AMakefile | 23+++++++++++++++++++++++
AREADME | 17+++++++++++++++++
Aarg.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.mk | 9+++++++++
Alinput.1 | 40++++++++++++++++++++++++++++++++++++++++
Alinput.c | 507+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 748 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Artem Kobets <artem@akobets.xyz> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,23 @@ +include config.mk + +all: linput + +linput: linput.c config.h + $(CC) $(CFLAGS) -o linput linput.c + +config.h: + cp config.def.h config.h + +clean: + rm -f linput + +install: linput + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f linput $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/linput + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + cp -f linput.1 $(DESTDIR)$(MANPREFIX)/man1 + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/linput + rm -f $(DESTDIR)$(MANPREFIX)/man1/linput.1 diff --git a/README b/README @@ -0,0 +1,17 @@ +linput - listen to input events. + +linput is an input event listener that runs a given script when a +certain sequence of events has occurred. It reads event data +from /dev/input/event* files. + +Two types of rules can be specified: events and hotkeys + +Events are simple rules that listen to a specific event. + +Hotkeys are rules specifically for keyboard events, which match +familiar hotkey behavior. You can specify a +modifier mask (Ctrl, Alt, etc.), a list of regular keys +that need to be pressed at once, and an event mask +(trigger on press, release or hold). + +linput is customized through editing a config.h file and recompiling the source code. diff --git a/arg.h b/arg.h @@ -0,0 +1,49 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][1]\ + && argv[0][0] == '-';\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/config.def.h b/config.def.h @@ -0,0 +1,82 @@ +/* default script name */ +static char *script = "/etc/linput/handler"; + +static const struct EventRule { + const char *name; + /* see <linux/input-event-codes.h> + * for type/code/value available values */ + int type; + int code; + int value; +} events[] = { + { + "sleep", + EV_SW, SW_LID, 1 + } +}; + +enum { + MOD_ANY = 0x01, + MOD_SHIFT = 0x02, + MOD_CTRL = 0x04, + MOD_SUPER = 0x08, + MOD_ALT = 0x10 +} HotkeyMod; + +enum { + ON_RELEASE = 0x01, + ON_PRESS = 0x02, + ON_HOLD = 0x04 +} HotkeyEvent; + +#define HOTKEY_MAX_KEYS 20 + +static const struct HotkeyRule { + const char *name; + int mod_mask; + /* see <linux/input-event-codes.h> for complete list + * (KEY_* constants) */ + int keys[HOTKEY_MAX_KEYS + 1]; + int event_mask; +} hotkeys[] = { + { + "open-terminal", + MOD_SUPER, + { KEY_C, 0 }, + ON_PRESS, + }, + { + "open-browser", + MOD_SUPER, + { KEY_E, KEY_B, 0 }, + ON_PRESS + }, + { + "volume-up", + 0, + { KEY_VOLUMEUP, 0 }, + ON_PRESS | ON_HOLD + }, + { + "volume-down", + 0, + { KEY_VOLUMEDOWN, 0 } , + ON_PRESS | ON_HOLD + }, +}; + +/* keys, pressing which shouldn't block hotkeys */ +static int ignored_keys[] = { + /* touchpad events */ + BTN_TOOL_FINGER, + BTN_TOUCH, + BTN_TOOL_DOUBLETAP, + BTN_TOOL_TRIPLETAP, + BTN_TOOL_QUADTAP, + BTN_TOOL_QUINTTAP, + /* mouse events */ + BTN_MOUSE, + BTN_LEFT, + BTN_RIGHT, + BTN_MIDDLE +}; diff --git a/config.mk b/config.mk @@ -0,0 +1,9 @@ +VERSION = 0.1.0 + +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +CC = cc +CPPFLAGS = -D_DEFAULT_SOURCE \ + -DVERSION=\"$(VERSION)\" +CFLAGS = -std=c99 -pedantic -Wall -Os $(CPPFLAGS) diff --git a/linput.1 b/linput.1 @@ -0,0 +1,40 @@ +.TH LINPUT 1 2020-04-15 +.SH NAME +linput \- listen to input events +.SH SYNOPSIS +.B linput +[-v] [-s script] +.SH DESCRIPTION +.B linput +is an input event listener that runs a given script when a +certain sequence of events has occurred. It reads event data +from /dev/input/event* files. + +Two types of rules can be specified: +.B events +and +.B hotkeys + +.B Events +are simple rules that listen to a specific event. + +.B Hotkeys +are rules specifically for keyboard events, which match +familiar hotkey behavior. You can specify a +modifier mask (Ctrl, Alt, etc.), a list of regular keys +that need to be pressed at once, and an event mask +(trigger on press, release or hold). + +linput is customized through editing config.h file and recompiling source code. + +.SH OPTIONS +.TP +.B \-v +display version and exit. +.TP +.B \-s script +set script name to execute. Can be a full pathname, or a script +in your shell PATH. Overrides default in config.h. + +.SH AUTHOR +Artem Kobets <artem@akobets.xyz> diff --git a/linput.c b/linput.c @@ -0,0 +1,507 @@ +#include <fcntl.h> +#include <glob.h> +#include <errno.h> +#include <limits.h> +#include <linux/input.h> +#include <poll.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/inotify.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "arg.h" +#include "config.h" + +#define LENGTH(x) (sizeof(x) / sizeof(x[0])) + +typedef struct node { + int fd; + struct node *next; +} fd_node; + +static int is_mod(int key); +static int get_mod_mask(); +static int has_key(const struct HotkeyRule *hotkey, int key); +static int is_match_hotkey_event(const struct HotkeyRule *hotkey, int key); +static int is_match_hotkey_mod(const struct HotkeyRule *hotkey); +static int is_hotkey_active(const struct HotkeyRule *hotkey, int last_key); +static void run(const char *name); + +static void add_fd(int fd); +static void remove_fd(int fd); +static void generate_poll_array(); +static int open_input_file(const char *path); +static void close_input_file(int fd); + +static void init_input_files(); +static void init_inotify(); +static void handle_input(int fd); +static void handle_inotify(int fd); + +static void sigchld(int unused); +static void die(const char *fmt, ...); +static void usage(); + +char *argv0; +static int key_state[KEY_CNT] = { 0 }; +static fd_node *fd_head = NULL; +static int inotify_fd = -1; +static size_t nfds = 0; +static struct pollfd *pfds = NULL; +static int poll_change = 0; + +int +is_mod(int key) +{ + return key == KEY_LEFTSHIFT || + key == KEY_RIGHTSHIFT || + key == KEY_LEFTCTRL || + key == KEY_RIGHTCTRL || + key == KEY_LEFTMETA || + key == KEY_RIGHTMETA || + key == KEY_LEFTALT || + key == KEY_RIGHTALT; +} + +int +get_mod_mask() +{ + int mod_mask = 0; + + if ( + key_state[KEY_LEFTSHIFT] != 0 || + key_state[KEY_RIGHTSHIFT] != 0 + ) { + mod_mask |= MOD_SHIFT; + } + if ( + key_state[KEY_LEFTCTRL] != 0 || + key_state[KEY_RIGHTCTRL] != 0 + ) { + mod_mask |= MOD_CTRL; + } + if ( + key_state[KEY_LEFTMETA] != 0 || + key_state[KEY_RIGHTMETA] != 0 + ) { + mod_mask |= MOD_SUPER; + } + if ( + key_state[KEY_LEFTALT] != 0 || + key_state[KEY_RIGHTALT] != 0 + ) { + mod_mask |= MOD_ALT; + } + + return mod_mask; +} + +int +has_key(const struct HotkeyRule *hotkey, int key) +{ + const int *p; + + for (p = hotkey->keys; *p != 0; p++) + if (key == *p) + return 1; + + return 0; +} + +int +is_key_ignored(int key) +{ + size_t i; + + for (i = 0; i < LENGTH(ignored_keys); i++) + if (ignored_keys[i] == key) + return 1; + + return 0; +} + +int +is_match_hotkey_event(const struct HotkeyRule *hotkey, int key) +{ + if ((hotkey->event_mask & ON_RELEASE) && + key_state[key] == 0) + return 1; + if ((hotkey->event_mask & ON_PRESS) && + key_state[key] == 1) + return 1; + if ((hotkey->event_mask & ON_HOLD) && + key_state[key] == 2) + return 1; + + return 0; +} + +int +is_match_hotkey_mod(const struct HotkeyRule *hotkey) +{ + if (hotkey->mod_mask & MOD_ANY) + return 1; + else + return get_mod_mask() == hotkey->mod_mask; +} + +int +is_hotkey_active(const struct HotkeyRule *hotkey, int last_key) +{ + int k; + int match = 1; + + for (k = 1; match && k < LENGTH(key_state); k++) { + if (k == last_key) { + if (!is_match_hotkey_event(hotkey, k)) + match = 0; + } else if (has_key(hotkey, k)) { + /* all other keys in hotkey should be pressed */ + if (key_state[k] == 0) + match = 0; + } else if (is_mod(k)) { + /* skip, modifier keys are handled separately */ + } else { + /* all keys not in hotkey should not be pressed */ + if (key_state[k] != 0 && !is_key_ignored(k)) + match = 0; + } + } + + return match && is_match_hotkey_mod(hotkey); +} + +void +run(const char *name) +{ + switch (fork()) { + case -1: + die("fork: %s\n", strerror(errno)); + break; + case 0: + execlp(script, script, name, (char *) NULL); + fprintf(stderr, "execlp %s: %s\n", script, strerror(errno)); + _exit(EXIT_FAILURE); + break; + } +} + +void +add_fd(int fd) +{ + fd_node *new; + + new = (fd_node *) malloc(sizeof(fd_node)); + if (new == NULL) + die("malloc: %s\n", strerror(errno)); + new->fd = fd; + new->next = NULL; + + if (fd_head == NULL) { + fd_head = new; + } else { + fd_node *last; + + last = fd_head; + while (last->next != NULL) + last = last->next; + last->next = new; + } +} + +void +remove_fd(int fd) +{ + fd_node *current, *prev, *node; + + if (fd_head == NULL) + return; + + current = fd_head; + prev = NULL; + node = NULL; + while (current != NULL) { + if (current->fd == fd) { + node = current; + break; + } + prev = current; + current = current->next; + } + + if (node != NULL) { + if (node == fd_head) + fd_head = node->next; + else if (prev != NULL) + prev->next = node->next; + free(node); + } +} + +void +generate_poll_array() +{ + fd_node *current; + size_t n, i; + + /* total fds - 1 inotify fd + all input file fds */ + n = 1; + + if (fd_head != NULL) { + current = fd_head; + while (current != NULL) { + n++; + current = current->next; + } + } + + nfds = n; + pfds = realloc(pfds, sizeof(struct pollfd) * n); + if (pfds == NULL) + die("malloc: %s\n", strerror(errno)); + + pfds[0].fd = inotify_fd; + pfds[0].events = POLLIN; + for (current = fd_head, i = 1; current != NULL; current = current->next, i++) { + pfds[i].fd = current->fd; + pfds[i].events = POLLIN; + } + + poll_change = 0; +} + +int +open_input_file(const char *path) { + int fd; + int savedErrno; + + fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC); + savedErrno = errno; + + if (fd != -1) { + add_fd(fd); + poll_change = 1; + } + + errno = savedErrno; + return fd; +} + +void +close_input_file(int fd) { + close(fd); + remove_fd(fd); + poll_change = 1; +} + +void +init_input_files() +{ + int res; + glob_t pglob; + size_t i; + int success; + + success = 0; + + res = glob("/dev/input/event*", 0, NULL, &pglob); + if (res != 0) + die("glob: can't find files in /dev/input/event*", strerror(errno)); + for (i = 0; i < pglob.gl_pathc; i++) + if (open_input_file(pglob.gl_pathv[i]) != -1) + success = 1; + + if (!success) + die("can't open any files in /dev/input/event*\n"); + + globfree(&pglob); +} + +void +init_inotify() +{ + inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + poll_change = 1; + if (inotify_fd == -1) { + fprintf(stderr, "inotify init failed (inotify_init): %s\n", strerror(errno)); + return; + } + + if (inotify_add_watch(inotify_fd, "/dev/input", IN_CREATE) == -1) { + fprintf(stderr, "inotify init failed (inotify_add_watch): %s\n", strerror(errno)); + return; + } +} + +void +handle_input(int fd) +{ + ssize_t n; + struct input_event buf; + + while ( + ( + (n = read(fd, &buf, sizeof(struct input_event))) == + sizeof(struct input_event) + ) || + (n == -1 && errno == EINTR) + ) { + size_t i; + + for (i = 0; i < LENGTH(events); i++) { + struct EventRule event = events[i]; + + if (event.name == NULL) continue; + + if ( + event.type == buf.type && + event.code == buf.code && + event.value == buf.value + ) + run(event.name); + } + + if (buf.type == EV_KEY) { + key_state[buf.code] = buf.value; + + for (i = 0; i < LENGTH(hotkeys); i++) { + struct HotkeyRule hotkey = hotkeys[i]; + + if (hotkey.name == NULL) continue; + + if (is_hotkey_active(&hotkey, buf.code)) + run(hotkey.name); + } + } + } +} + +void +handle_inotify(int fd) { + char buf[sizeof(struct inotify_event) + NAME_MAX + 1]; + char filename[NAME_MAX + 1]; + ssize_t n, processed_bytes; + + n = read(fd, &buf, sizeof(buf)); + if (n <= 0) + return; + + processed_bytes = 0; + while (processed_bytes < n) { + struct inotify_event* event = (struct inotify_event *) &buf[processed_bytes]; + + if ( + event->mask & IN_CREATE && + strncmp(event->name, "event", 5) == 0 + ) { + int fd, retries, max_retries; + + snprintf(filename, sizeof(filename), "/dev/input/%s", event->name); + + retries = 0; + max_retries = 3; + while (retries <= max_retries) { + fd = open_input_file(filename); + /* If linput was started as normal user, + * file may not be opened due to missing file permissions. + * If that's the case, try several times before giving up. + */ + if (fd == -1 && errno == EACCES) { + sleep(1); + retries++; + } + else + break; + } + } + + processed_bytes += sizeof(struct inotify_event) + event->len; + } +} + +void +sigchld(int unused) +{ + while (waitpid(-1, NULL, WNOHANG) > 0); +} + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + exit(EXIT_FAILURE); +} + +void +usage() +{ + die("usage: %s [-v] [-s script]\n", argv0); +} + + +int +main(int argc, char **argv) +{ + struct sigaction act; + sigset_t sigmask; + int res; + + ARGBEGIN { + case 's': + script = EARGF(usage()); + break; + case 'v': + puts("linput "VERSION); + exit(EXIT_FAILURE); + break; + default: + usage(); + break; + } ARGEND + + act.sa_handler = sigchld; + sigemptyset(&sigmask); + act.sa_mask = sigmask; + act.sa_flags = 0; + sigaction(SIGCHLD, &act, NULL); + + init_input_files(); + init_inotify(); + generate_poll_array(); + + while ( + (res = poll(pfds, nfds, -1)) > 0 || + (res == -1 && errno == EINTR) + ) { + size_t i; + + /* handle input event fds */ + for (i = 1; i < nfds; i++) { + if (pfds[i].revents & POLLIN) + handle_input(pfds[i].fd); + if (pfds[i].revents & (POLLERR | POLLHUP)) + close_input_file(pfds[i].fd); + } + + /* handle inotify fd */ + if (pfds[0].revents & POLLIN) + handle_inotify(pfds[0].fd); + if (pfds[0].revents & (POLLERR | POLLHUP)) { + close(pfds[0].fd); + init_inotify(); + } + + /* rebuild poll array if necessary */ + if (poll_change) + generate_poll_array(); + } +}