├── Makefile ├── LICENSE ├── README.md ├── config.h └── hkd.c /Makefile: -------------------------------------------------------------------------------- 1 | # Author: aaronamk 2 | 3 | CC = gcc 4 | INCS = -I/usr/include/libevdev-1.0 5 | LIBS = -L/usr/lib/ -levdev -pthread 6 | CFLAGS = -std=c11 -Wall -D_POSIX_C_SOURCE=200809L -O3 7 | PREFIX = /usr/local 8 | 9 | hkd: 10 | $(CC) $(CFLAGS) -o hkd hkd.c ${INCS} ${LIBS} 11 | 12 | install: hkd 13 | mkdir -p ${DESTDIR}${PREFIX}/bin 14 | cp -f hkd ${DESTDIR}${PREFIX}/bin 15 | chmod 755 ${DESTDIR}${PREFIX}/bin/hkd 16 | 17 | uninstall: 18 | $(RM) ${DESTDIR}${PREFIX}/bin/hkd 19 | 20 | clean: 21 | $(RM) hkd 22 | 23 | .PHONY: all install uninstall clean 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Aaron Klein 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DESCRIPTION 2 | A key mapper program. 3 | 4 | Features: 5 | * Map a key plus any number of modifiers to a command 6 | * Map a modifier release to a command 7 | * Configure which keys to use as modifiers 8 | * Works in Xorg, Wayland, and the TTY (using [libevdev](https://www.freedesktop.org/software/libevdev/doc/latest/index.html)) 9 | 10 | ## INSTALLATION 11 | ### From source 12 | 1. Run: 13 | ``` 14 | git clone https://github.com/aaronamk/hkd.git 15 | cd hkd 16 | sudo make install 17 | ``` 18 | 2. Add yourself to the `input` group: `sudo usermod -a -G input `. This allows hkd to access/modify input devices in the filesystem without root access. 19 | 3. Reboot for the change to take affect 20 | 21 | ## CONFIGURATION 22 | The file `config.h` is where you set your key bindings. This is similar to [suckless](https://suckless.org/philosophy)'s software practices. `config.h` is part of the source, so you must recompile and restart the program for any changes to take effect. Some default bindings are provided; you can add or remove them as needed. 23 | 24 | See [linux/input-event-codes.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h) for a list of available key codes or use the [evtest](https://gitlab.freedesktop.org/libevdev/evtest) command to see device events printed to the terminal. 25 | 26 | ## USAGE 27 | ``` 28 | hkd /dev/input/by-id/ /dev/input/by-id/ ... 29 | ``` 30 | 31 | ## RELATED/SIMILAR SOFTWARE 32 | * [swhkd](https://github.com/waycrate/swhkd) 33 | * [Interception Tools](https://gitlab.com/interception/linux/tools) 34 | --- 35 | 36 | *NOTICE*: Please check the issues board if you find a bug or would like a feature added. Open a new issue if it is not listed. 37 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Hotkey Daemon sample config.h 3 | * 4 | * Author: aaronamk 5 | */ 6 | 7 | #ifndef CONFIG_H 8 | #define CONFIG_H 9 | 10 | #include 11 | #include 12 | 13 | typedef unsigned short key_code; 14 | 15 | struct binding { 16 | const unsigned int mod_mask; 17 | const key_code key; 18 | const void *cmd; 19 | }; 20 | 21 | /* Maximum number of keyboards to allow via the command line */ 22 | #define MAX_DEVICES 32 23 | 24 | /* any key can be used as a modifier (e.g. caps lock), these are just the most common ones: */ 25 | static const key_code mods[][2] = { { KEY_LEFTSHIFT, KEY_RIGHTSHIFT }, 26 | { KEY_LEFTALT, KEY_RIGHTALT }, 27 | { KEY_LEFTMETA, KEY_RIGHTMETA }, 28 | { KEY_LEFTCTRL, KEY_RIGHTCTRL } }; 29 | 30 | /* masks */ 31 | #define M_NONE 0 32 | #define M_SHIFT 0b1000 33 | #define M_ALT 0b0100 34 | #define M_META 0b0010 35 | #define M_CTRL 0b0001 36 | 37 | 38 | /* commands */ 39 | /* requires pulseaudio */ 40 | static const char *vol_up[] = { "pactl", "set-sink-volume", "@DEFAULT_SINK@", "+2%", NULL }; 41 | static const char *vol_down[] = { "pactl", "set-sink-volume", "@DEFAULT_SINK@", "-2%", NULL }; 42 | static const char *vol_toggle_mute[] = { "pactl", "set-sink-mute", "@DEFAULT_SINK@", "toggle", NULL }; 43 | 44 | /* requires playerctl */ 45 | static const char *media_next[] = { "playerctl", "--player=playerctld", "next", NULL }; 46 | static const char *media_prev[] = { "playerctl", "--player=playerctld", "previous", NULL }; 47 | static const char *media_forward[] = { "playerctl", "--player=playerctld", "position", "5+", NULL }; 48 | static const char *media_backward[] = { "playerctl", "--player=playerctld", "position", "5-", NULL }; 49 | static const char *media_toggle_pause[] = { "playerctl", "--player=playerctld", "play-pause", NULL }; 50 | 51 | /* requires a $TERMINAL environment variable set to your preferred terminal */ 52 | static const char *term[] = { "sh", "-c", "$TERMINAL", NULL }; 53 | 54 | /* requires dmenu */ 55 | static const char *launcher[] = { "dmenu_run", NULL }; 56 | 57 | static const char *shutdown[] = { "shutdown", "now", NULL }; 58 | 59 | /* bindings */ 60 | /* https://www.kernel.org/doc/html/latest/input/event-codes.html */ 61 | static const struct binding bindings[] = { 62 | /* modifier mask key code command */ 63 | { M_NONE, KEY_VOLUMEUP, vol_up }, 64 | { M_NONE, KEY_VOLUMEDOWN, vol_down }, 65 | { M_NONE, KEY_MUTE, vol_toggle_mute }, 66 | 67 | { M_NONE, KEY_NEXTSONG, media_next }, 68 | { M_NONE, KEY_PREVIOUSSONG, media_prev }, 69 | { M_SHIFT, KEY_NEXTSONG, media_forward }, 70 | { M_SHIFT, KEY_PREVIOUSSONG, media_backward }, 71 | { M_NONE, KEY_PLAYPAUSE, media_toggle_pause }, 72 | 73 | { M_META, KEY_ENTER, term }, 74 | { M_NONE, KEY_LEFTMETA, launcher }, 75 | { M_NONE, KEY_RIGHTMETA, launcher }, 76 | { M_CTRL|M_ALT, KEY_DELETE, shutdown } 77 | }; 78 | 79 | #endif 80 | -------------------------------------------------------------------------------- /hkd.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Hotkey Daemon 3 | * 4 | * Author: aaronamk 5 | */ 6 | 7 | #include "config.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | #define VERSION "v1.0.1" 21 | #define INPUT_VAL_PRESS 1 22 | #define INPUT_VAL_RELEASE 0 23 | #define INPUT_VAL_REPEAT 2 24 | #define LENGTH(X) sizeof X / sizeof X[0] 25 | 26 | _Atomic(unsigned int) mod_state = 0; 27 | _Atomic(key_code) last_press = 0; 28 | _Atomic(int) running = 1; 29 | pthread_t threads[MAX_DEVICES]; 30 | int dev_count; 31 | 32 | 33 | void print_usage(const char *program) { 34 | printf("Signal hkd based on info from libevdev device key events\n" 35 | "\n" 36 | "Usage: %s [options] \n" 37 | "\n" 38 | "Options:\n" 39 | " -h show this message and exit\n" 40 | " -V print version number and exit\n" 41 | "\n" 42 | "devices: space-separated list of device paths" 43 | " (/dev/input/by-...) to be monitored\n", 44 | program); 45 | } 46 | 47 | 48 | void handle_terminate(int signum) { 49 | running = 0; 50 | for (int i = 0; i < dev_count; i++) pthread_kill(threads[i], SIGUSR1); 51 | } 52 | 53 | 54 | void spawn(char *cmd[]) { 55 | if (fork() == 0) { 56 | if (fork() == 0) { 57 | setsid(); 58 | execvp(cmd[0], cmd); 59 | } 60 | exit(EXIT_SUCCESS); 61 | } 62 | wait(NULL); 63 | } 64 | 65 | 66 | unsigned int get_mod_mask(key_code key) { 67 | int i = 0; 68 | for (unsigned int bits = 1 << (LENGTH(mods) - 1); bits; bits >>= 1) { 69 | for (int j = 0; j < LENGTH(mods[i]); j++) { 70 | if (mods[i][j] == key) return bits; 71 | } 72 | i++; 73 | } 74 | return 0; 75 | } 76 | 77 | 78 | int try_hotkey(key_code key) { 79 | for (int i = 0; i < LENGTH(bindings); i++) { 80 | if (bindings[i].key == key 81 | && bindings[i].mod_mask == mod_state) { 82 | spawn((char**)bindings[i].cmd); 83 | return 1; 84 | } 85 | } 86 | return 0; 87 | } 88 | 89 | 90 | int handle_event(struct input_event input) { 91 | if (input.type == EV_MSC && input.code == MSC_SCAN) return 0; 92 | 93 | /* forward anything that is not a key event */ 94 | if (input.type != EV_KEY) return 0; 95 | 96 | /* handle key events */ 97 | unsigned int mod_mask = get_mod_mask(input.code); 98 | switch (input.value) { 99 | case 1: /* key press event */ 100 | case 2: /* key repeat event */ 101 | last_press = input.code; 102 | mod_state |= mod_mask; 103 | return !mod_mask && try_hotkey(input.code); 104 | case 0: /* key release event */ 105 | if (mod_mask) { 106 | mod_state ^= mod_mask; 107 | if (last_press == input.code) try_hotkey(input.code); 108 | } 109 | return 0; 110 | default: 111 | fprintf(stderr, "unexpected .value=%d .code=%d, doing nothing", 112 | input.value, input.code); 113 | return 0; 114 | } 115 | } 116 | 117 | 118 | void nop(int signum) {} 119 | void *handle_device(void *path) { 120 | struct sigaction ignore; 121 | ignore.sa_flags = 0; 122 | ignore.sa_handler = nop; 123 | sigaction(SIGUSR1, &ignore, NULL); 124 | /* open device file */ 125 | int fd = open((char*)path, O_RDONLY); 126 | if (fd < 0) { 127 | fprintf(stderr, "Error: failed to open device: %s\n", (char*)path); 128 | return NULL; 129 | } 130 | 131 | /* open device */ 132 | struct libevdev *dev; 133 | int rc = libevdev_new_from_fd(fd, &dev); 134 | if (rc < 0) { 135 | close(fd); 136 | fprintf(stderr, "Error: failed to init libevdev (%s)\n", 137 | strerror(-rc)); 138 | return NULL; 139 | } 140 | 141 | /* grab device */ 142 | if (libevdev_grab(dev, LIBEVDEV_GRAB) < 0) { 143 | libevdev_free(dev); 144 | close(fd); 145 | fprintf(stderr, "Error: failed to grab device\n"); 146 | return NULL; 147 | } 148 | 149 | /* create virtual uinput device */ 150 | struct libevdev_uinput *virtual_dev; 151 | if (libevdev_uinput_create_from_device(dev, LIBEVDEV_UINPUT_OPEN_MANAGED, 152 | &virtual_dev) < 0) { 153 | libevdev_grab(dev, LIBEVDEV_UNGRAB); 154 | libevdev_free(dev); 155 | close(fd); 156 | fprintf(stderr, "Error: failed to create virtual uinput device\n"); 157 | return NULL; 158 | } 159 | 160 | /* handle key events */ 161 | struct input_event input = {}; 162 | do { 163 | rc = libevdev_next_event(dev, LIBEVDEV_READ_FLAG_NORMAL 164 | | LIBEVDEV_READ_FLAG_BLOCKING, &input); 165 | if (rc != 0) continue; 166 | 167 | /* continue if event was handled */ 168 | if (handle_event(input)) continue; 169 | 170 | /* forward events not associated with a binding */ 171 | if (libevdev_uinput_write_event(virtual_dev, input.type, input.code, 172 | input.value) < 0) { 173 | fprintf(stderr, "Error: failed to send event\n"); 174 | } 175 | } while (running && (rc == 1 || rc == 0 || rc == -EAGAIN)); 176 | 177 | /* cleanup */ 178 | libevdev_grab(dev, LIBEVDEV_UNGRAB); 179 | libevdev_free(dev); 180 | libevdev_uinput_destroy(virtual_dev); // closes fd 181 | 182 | return EXIT_SUCCESS; 183 | } 184 | 185 | 186 | int main(int argc, char *argv[]) { 187 | /* parse options */ 188 | int opt; 189 | while ((opt = getopt(argc, argv, ":hV")) != -1) { 190 | switch (opt) { 191 | case 'h': 192 | print_usage(argv[0]); 193 | exit(EXIT_SUCCESS); 194 | case 'V': 195 | printf("%s %s\n", argv[0], VERSION); 196 | exit(EXIT_SUCCESS); 197 | case '?': 198 | fprintf(stderr, "Error: invalid option: %c\n", optopt); 199 | print_usage(argv[0]); 200 | exit(EXIT_FAILURE); 201 | } 202 | } 203 | 204 | /* validate number of devices */ 205 | dev_count = argc - optind; 206 | if (!dev_count) { 207 | fprintf(stderr, "Error: device path not specified\n"); 208 | print_usage(argv[0]); 209 | exit(EXIT_FAILURE); 210 | } 211 | if (dev_count > MAX_DEVICES) { 212 | fprintf(stderr, "Error: Exceeded MAX_DEVICES (increase the value in " 213 | "config.h)\n"); 214 | exit(EXIT_FAILURE); 215 | } 216 | 217 | /* catch termination signals to exit gracefully */ 218 | struct sigaction terminate; 219 | terminate.sa_flags = 0; 220 | terminate.sa_handler = handle_terminate; 221 | sigaction(SIGINT, &terminate, NULL); 222 | sigaction(SIGTERM, &terminate, NULL); 223 | sigaction(SIGHUP, &terminate, NULL); 224 | 225 | /* create thread for each device */ 226 | for (int i = 0; i < dev_count; i++) { 227 | if (pthread_create(&threads[i], NULL, handle_device, 228 | (void*)argv[optind + i])) { 229 | fprintf(stderr, "Error: failed to create thread."); 230 | exit(EXIT_FAILURE); 231 | } 232 | } 233 | 234 | pthread_exit(EXIT_SUCCESS); 235 | } 236 | --------------------------------------------------------------------------------