diff options
author | Kazuki Yamaguchi <k@rhe.jp> | 2020-11-18 13:52:40 +0900 |
---|---|---|
committer | Kazuki Yamaguchi <k@rhe.jp> | 2021-08-06 02:36:43 +0900 |
commit | b6ef8039c18eb384820376433d253fa0c5d70514 (patch) | |
tree | 9c21fe3a7dd3bd955d6d4fcee92004cfdf9ed94b | |
download | thotkeys-b6ef8039c18eb384820376433d253fa0c5d70514.tar.gz |
initial version of thotkeys
-rw-r--r-- | .gitignore | 22 | ||||
-rw-r--r-- | COPYING | 19 | ||||
-rw-r--r-- | Makefile.am | 2 | ||||
-rw-r--r-- | README | 74 | ||||
-rw-r--r-- | configure.ac | 19 | ||||
-rw-r--r-- | thotkeys.c | 358 |
6 files changed, 494 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5499219 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# autoreconf -i +/configure +/autom4te.cache +/aclocal.m4 +/compile +/config.h.in +/depcomp +/install-sh +/missing +Makefile.in + +# ./configure +Makefile +/.deps +/config.h +/config.log +/config.status +/stamp-h1 + +# make +*.o +thotkeys @@ -0,0 +1,19 @@ +Copyright (c) 2020 Kazuki Yamaguchi <k@rhe.jp> + +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.am b/Makefile.am new file mode 100644 index 0000000..c466f86 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,2 @@ +bin_PROGRAMS = thotkeys +thotkeys_LDADD = @X11_LIBS@ @XINPUT_LIBS@ @@ -0,0 +1,74 @@ +thotkeys +======== + +A "transparent" hotkey implementation for X11 - thotkeys will not consume any +keyboard events and they will be passed down to the focused window intact. + +Typically, window managers implement a hotkey feature with GrabKey. A big +problem with the approach is that activating a hotkey grabs the entire keyboard +until the hotkey is released. This makes it impossible to accomplish a common +request: "doing something while a hotkey is held down, while allowing +applications to process other key input events". + +thotkeys instead uses the X Input Device Extension to monitor key events +directly from an input device, but not grab it. + + +Installation +------------ + + $ autoreconf -i + $ ./configure + $ make + $ make install + + +Usage +----- + +Monitor events: + + # Use xorg-xinput to find out the device name/id + $ xinput list + [...] + ↳ Topre REALFORCE 87 US id=11 [slave keyboard (3)] + [...] + + $ ./thotkeys --device 'Topre REALFORCE 87 US' --monitor + # released Return + --key Control_L # pressed Control_L + --key Control_L --key c # pressed c + ^C + +Register hotkeys: + + $ ./thotkeys --device 'Topre REALFORCE 87 US' \ + --hotkey --key Control_L --key m \ + --on-press 'while :; do echo Ctrl+M is pressed; sleep 0.1; done' + + $ # Registering multiple hotkeys + $ ./thotkeys --device 'Topre REALFORCE 87 US' \ + --hotkey --key Control_L --key m --on-press \ + 'while :; do echo Ctrl+M is pressed; sleep 0.1; done' \ + --hotkey --key F11 --on-press \ + 'while :; do echo F11 is pressed; sleep 0.1; done' + +The program passed to --on-press is executed on a shell. The process will +receive SIGTERM once the hotkey is released. + + +Limitations +----------- + + - The hotkey is always global and it is currently not possible to enable or + disable hotkeys conditionally. + + - The current KeyCode <-> KeySym conversion is probably erroneous. How does it + behave with a different keyboard layout, or when multiple keyboards are + connected to the computer? + + +License +------- + +thotkeys is licensed under the MIT license. See also COPYING. diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..b01755f --- /dev/null +++ b/configure.ac @@ -0,0 +1,19 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ([2.69]) +AC_INIT([thotkeys], [1.0.0], [k@rhe.jp]) +AM_INIT_AUTOMAKE([foreign]) +AC_CONFIG_SRCDIR([thotkeys.c]) +AC_CONFIG_HEADERS([config.h]) + +# Checks for programs. +AC_PROG_CC +CFLAGS="$CFLAGS -Wall -Wextra -Wconversion -Wno-parentheses" + +# Checks for libraries. +PKG_CHECK_MODULES(X11, [x11]) +PKG_CHECK_MODULES(XINPUT, [xi]) + +AC_CONFIG_FILES([Makefile]) +AC_OUTPUT diff --git a/thotkeys.c b/thotkeys.c new file mode 100644 index 0000000..61f5975 --- /dev/null +++ b/thotkeys.c @@ -0,0 +1,358 @@ +#include "config.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <getopt.h> +#include <unistd.h> +#include <errno.h> +#include <stdbool.h> +#include <sys/wait.h> +#include <X11/XKBlib.h> +#include <X11/extensions/XInput.h> + +static int VERBOSE = 0; + +#define debug(...) do { \ + if (VERBOSE) \ + fprintf(stderr, "debug: " __VA_ARGS__); \ +} while (0) + +#define warn(...) do { \ + fprintf(stderr, "warning: " __VA_ARGS__); \ +} while (0) + +#define fatal(...) do { \ + fprintf(stderr, "fatal: " __VA_ARGS__); \ + exit(1); \ +} while (0) + +static inline void *xrealloc(void *o, size_t size) +{ + void *p = realloc(o, size); + if (!p) + fatal("realloc failed\n"); + return p; +} + +static Display *get_display(void) +{ + Display *display = XOpenDisplay(NULL); + if (!display) + fatal("XOpenDisplay() failed\n"); + return display; +} + +static XDeviceInfo *get_device_info(Display *display, const char *name) +{ + bool use_id = true; + long id; + + errno = 0; + char *endp; + id = strtol(name, &endp, 10); + if (errno == ERANGE || id == 0 && endp != name + strlen(name)) + use_id = false; + + int num_devices; + XDeviceInfo *devices = XListInputDevices(display, &num_devices); + + XDeviceInfo *found = NULL; + for (int i = 0; i < num_devices; i++) { + XDeviceInfo *device = &devices[i]; + + if (device->use < IsXExtensionDevice) + continue; + if (!strcmp(device->name, name) || use_id && (long)device->id == id) { + if (found) + fatal("more than one device found with the " \ + "name '%s'\n", name); + found = device; + } + } + return found; +} + +static XID key_press_type, key_release_type; +static bool is_key_press_event(const XDeviceKeyEvent *ev) +{ + return ev->type == (int)key_press_type; +} + +static void register_events(Display *display, XDevice *device) +{ + int screen = DefaultScreen(display); + Window root_win = RootWindow(display, screen); + + XEventClass event_list[2]; + DeviceKeyPress(device, key_press_type, event_list[0]); + DeviceKeyRelease(device, key_release_type, event_list[1]); + if (XSelectExtensionEvent(display, root_win, event_list, 2)) + fatal("XSelectExtensionEvent() failed\n"); +} + +static void prepare_monitor(Display *display, const char *device_name) +{ + XDeviceInfo *info = get_device_info(display, device_name); + if (!info) + fatal("unable to find device '%s'\n", device_name); + + XDevice *device = XOpenDevice(display, info->id); + if (!device) + fatal("unable to open device '%s'\n", device_name); + + register_events(display, device); +} + + +static XEvent process_event_ev; +static const XDeviceKeyEvent *process_event(Display *display) +{ +redo: + XNextEvent(display, &process_event_ev); + + if (process_event_ev.type == (int)key_press_type) + return (XDeviceKeyEvent *)&process_event_ev; + if (process_event_ev.type == (int)key_release_type) { + // Retriggered events + if (XEventsQueued(display, QueuedAfterReading)) { + XEvent nev; + XPeekEvent(display, &nev); + + if (nev.type == (int)key_press_type && + nev.xkey.time == process_event_ev.xkey.time && + nev.xkey.keycode == process_event_ev.xkey.keycode) { + // Consume the following KeyPress + XNextEvent(display, &nev); + goto redo; + } + } + return (XDeviceKeyEvent *)&process_event_ev; + } + + debug("ignoring event %d\n", process_event_ev.type); + goto redo; +} + +static void command_help(void) +{ + fprintf(stderr, "%s\n", PACKAGE_STRING); + fprintf(stderr, "\n"); + fprintf(stderr, "Commands:\n"); + fprintf(stderr, " thotkeys --help\n"); + fprintf(stderr, " Show this message\n"); + fprintf(stderr, " thotkeys --device <device> --monitor\n"); + fprintf(stderr, " Print key press and release events to stdout\n"); + fprintf(stderr, " thotkeys --device <device> --hotkey --key <key> " \ + "[--key <key>...] --on-press <on-press> [--hotkey ...]\n"); + fprintf(stderr, " Run <on-press> when <key> is pressed down. " \ + "SIGTERM will be sent to the process when the hotkey is\n" \ + " released. --hotkey and the following options may be repeated.\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " --verbose\n"); + fprintf(stderr, " Enable debugging output\n"); + exit(0); +} + +static void command_monitor(const char *device_name) +{ + Display *display = get_display(); + prepare_monitor(display, device_name); + + char keymap[256] = { 0 }; + while (1) { + const XDeviceKeyEvent *ev = process_event(display); + bool pressed = is_key_press_event(ev); + + if (ev->keycode > 255) + fatal("unexpected keycode %d\n", (int)ev->keycode); + keymap[ev->keycode] = pressed; + + for (int i = 0; i < 256; i++) { + if (keymap[i]) { + KeySym keysym = XkbKeycodeToKeysym(display, (KeyCode)i, 0, 0); + printf("--key %s ", XKeysymToString(keysym)); + } + } + KeySym basekeysym = XkbKeycodeToKeysym(display, (KeyCode)ev->keycode, 0, 0); + printf("# %s %s\n", + pressed ? "pressed" : "released", + XKeysymToString(basekeysym)); + } +} + +struct hotkey_config { + const char **keystrs; + size_t numkeystrs; + const char *on_press; + + char keymap[256]; + char checkmap[256]; + bool activated; + pid_t pid; +}; + +static void command_hotkeys(const char *device_name, struct hotkey_config *hotkeys, + size_t numhotkeys) +{ + Display *display = get_display(); + prepare_monitor(display, device_name); + + for (size_t i = 0; i < numhotkeys; i++) { + struct hotkey_config *c = hotkeys + i; + memset(c->keymap, 0, sizeof(c->keymap)); + memset(c->checkmap, 0, sizeof(c->checkmap)); + c->activated = false; + c->pid = -1; + + for (size_t j = 0; j < c->numkeystrs; j++) { + const char *str = c->keystrs[j]; + KeySym keysym = XStringToKeysym(str); + if (keysym == NoSymbol) + fatal("--key %s could not be recognized\n", str); + KeyCode keycode = XKeysymToKeycode(display, keysym); + if (keycode == 0) + fatal("--key %s could not be converted into keycode\n", str); + c->checkmap[keycode] = 1; + } + } + + while (1) { + // Reap child processes + pid_t pid; + int status; + while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { + debug("reaped child process %d\n", pid); + for (size_t i = 0; i < numhotkeys; i++) { + struct hotkey_config *c = hotkeys + i; + if (c->pid == pid) { + c->pid = -1; + break; + } + } + } + + const XDeviceKeyEvent *ev = process_event(display); + bool pressed = is_key_press_event(ev); + + if (ev->keycode > 255) + fatal("unexpected keycode %d\n", (int)ev->keycode); + + for (size_t i = 0; i < numhotkeys; i++) { + struct hotkey_config *c = hotkeys + i; + if (!c->checkmap[ev->keycode]) + continue; + + c->keymap[ev->keycode] = pressed; + bool matched = !memcmp(c->checkmap, c->keymap, sizeof(c->checkmap)); + + if (!c->activated && matched) { + if (c->pid != -1) + warn("program '%s' is still running with pid %d\n", + c->on_press, c->pid); + debug("spawning process %s\n", c->on_press); + if (!(c->pid = fork())) { + execl("/bin/sh", "sh", "-c", c->on_press, NULL); + exit(0); + } + } + else if (c->activated && !matched) { + if (c->pid != -1) { + debug("sending SIGTERM to process %d\n", c->pid); + kill(c->pid, SIGTERM); + } + } + c->activated = matched; + } + } +} + +int main(int argc, char **argv) +{ + const char *device_name = NULL; + bool do_help = false, do_monitor = false, do_hotkeys = false; + size_t numhotkeys = 0, numkeys = 0; + struct hotkey_config *hotkeys = NULL; + const char **keys = NULL, *on_press = NULL; + + while (1) { + static struct option long_options[] = { + { "verbose", no_argument, 0, 'V' }, + { "version", no_argument, 0, 'H' }, + { "help", no_argument, 0, 'H' }, + { "monitor", no_argument, 0, 'M' }, + { "hotkey", no_argument, 0, 'K' }, + + { "device", required_argument, 0, 'd' }, + { "key", required_argument, 0, 'k' }, + { "on-press", required_argument, 0, 'p' }, + { 0 } + }; + + int c = getopt_long(argc, argv, "", long_options, NULL); + if (c == -1) + break; + switch (c) { + case 'V': + VERBOSE = 1; + break; + case 'H': + do_help = true; + break; + case 'M': + do_monitor = true; + break; + case 'K': + if (do_hotkeys) { + if (!keys || !on_press) + fatal("--key and --on-press options are required\n"); + hotkeys = xrealloc(hotkeys, sizeof(*hotkeys) * (numhotkeys + 1)); + hotkeys[numhotkeys++] = (struct hotkey_config) { + .keystrs = keys, + .numkeystrs = numkeys, + .on_press = on_press, + }; + keys = NULL; + numkeys = 0; + on_press = NULL; + } + do_hotkeys = true; + break; + case 'd': + device_name = optarg; break; + case 'k': + keys = xrealloc(keys, sizeof(*keys) * (numkeys + 1)); + keys[numkeys++] = optarg; + break; + case 'p': + on_press = optarg; break; + case '?': + exit(1); + default: + fatal("[BUG] unknown option '%c'\n", c); + } + } + if (do_hotkeys) { + if (!keys || !on_press) + fatal("--key and --on-press options are required\n"); + hotkeys = xrealloc(hotkeys, sizeof(*hotkeys) * (numhotkeys + 1)); + hotkeys[numhotkeys++] = (struct hotkey_config) { + .keystrs = keys, + .numkeystrs = numkeys, + .on_press = on_press, + }; + } + if (do_monitor || do_hotkeys) { + if (!device_name) + fatal("--device option is required\n"); + } + if (optind != argc) + fatal("unknown argument %s\n", argv[optind]); + + if (do_help) + command_help(); + if (do_monitor) + command_monitor(device_name); + if (do_hotkeys) + command_hotkeys(device_name, hotkeys, numhotkeys); +} |