commit 5fecce7720f123d04aa5731a1a66fac7545b5793 Author: Raheman Vaiya Date: Thu Jul 22 16:06:59 2021 -0400 First commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f938f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.swp +*.swn +*.swo +*.out +tags +bin/ +*.gch diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b10de5c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT/X Consortium License + +© 2020 Raheman Vaiya + +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 new file mode 100644 index 0000000..c2ee0af --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +.PHONY: clean all utils + +all: clean utils + -mkdir bin + gcc -DGIT_COMMIT_HASH=\"$(shell git rev-parse HEAD)\" -DDEBUG -g -Wno-unused-parameter -Wall -Wextra -ludev src/*.c -o bin/keyd +man: + pandoc -s -t man man.md | gzip > keyd.1.gz +clean: + -rm -rf bin +install: + -mkdir /etc/keyd + -install -m755 keyd.service /etc/systemd/system/ + -install -m755 bin/keyd /usr/bin + -install -m644 keyd.1.gz /usr/share/man/man1/ + -systemctl enable keyd + -systemctl start keyd diff --git a/README.md b/README.md new file mode 100644 index 0000000..43701e0 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Impetus + +Linux lacks a good key remapping solution. In order to achieve satisfactory +results a medley of tools need to be employed (e.g xcape, xmodmap) with the end +result often being tethered to a specified environment (X11). keyd attempts to +solve this problem by providing a flexible system wide daemon which remaps keys +using kernel level input primitives (evdev, uinput). + +# Features + +keyd has several unique features many of which are traditionally only +found in custom keyboard firmware like [QMK](https://github.com/qmk/qmk_firmware). +Some of the more interesting ones include: + +- Layers. +- Key overloading (different behaviour on tap/hold). +- Per keyboard configuration. +- Instantaneous remapping (no flashing required). +- A simple and intuitive config format. +- Being display server agnostic (works on wayland and virtual console alike). + +# Dependencies + + - make + - gcc + - libudev + +# Installation + + git clone https://github.com/rvaiya/keyd + cd keyd + sudo apt-get install libudev-dev # Debian specific install the corresponding package on your distribution + make && sudo make install + +# Quickstart + +1. Install keyd + +2. Put the following in `/etc/keyd/default.cfg`: + +``` +# Turns capslock into an escape key when pressed and a control key when held. +capslock = mods_on_hold(C, esc) + +# Remaps the escape key to capslock +esc = capslock +``` + +3. Run `sudo systemctl restart keyd` + +4. See the [man page](man.md) for a comprehensive list of all config options. + +# Sample Config File + + # Maps escape to the escape layer when held and the escape key when pressed + + esc = layer_on_hold(escape_layer, esc) + + [escape_layer] + + 1 = layer_toggle(dvorak) + 2 = layer_toggle(default) + + # Creates a dvorak layer which inherits from the default layer. Without + # explicitly inheriting from another layer unmapped keys would be ignored. + + [dvorak:default] + + q = apostrophe + w = comma + e = dot + r = p + t = y + y = f + u = g + i = c + o = r + p = l + + a = a + s = o + d = e + f = u + g = i + h = d + j = h + k = t + l = n + semicolon = s + + z = semicolon + x = q + c = j + v = k + b = x + n = b + m = m + comma = w + dot = v + slash = z diff --git a/TODO b/TODO new file mode 100644 index 0000000..2dc80a4 --- /dev/null +++ b/TODO @@ -0,0 +1,4 @@ +Make layers inheritable. +Add configurable time based double tap detection. +Document and publish. +Simplify code and special key naming schemes (use a parser and don't manually implement each of HXTXDX). diff --git a/keyd.1.gz b/keyd.1.gz new file mode 100644 index 0000000..ca43393 Binary files /dev/null and b/keyd.1.gz differ diff --git a/keyd.service b/keyd.service new file mode 100644 index 0000000..eed065a --- /dev/null +++ b/keyd.service @@ -0,0 +1,11 @@ +[Install] +WantedBy=sysinit.target + +[Unit] +Description=key remapping daemon +DefaultDependencies=no +After=local-fs-pre.target + +[Service] +Type=simple +ExecStart=/usr/bin/keyd diff --git a/man.1.gz b/man.1.gz new file mode 100644 index 0000000..7346932 Binary files /dev/null and b/man.1.gz differ diff --git a/man.md b/man.md new file mode 100644 index 0000000..41fd8ae --- /dev/null +++ b/man.md @@ -0,0 +1,190 @@ +% WARP(1) +% Raheman Vaiya + +# OVERVIEW + +A system-wide key remapping daemon. + +# USAGE + +keyd [-m] [-l] + +# ARGS + + **-m**: Run in monitor mode. (ensure keyd is not running to see untranslated events). + + **-l**: List all valid key names. + +# OVERVIEW + +keyd is intended to be run as a system wide daemon via systemd. If run +directly it will run in the foreground and print diagnostic information to +stderr. + +Control can be exercised using the standard systemd mechanisms. E.G: + + - journalctl -fu keyd # Print diagnostic information. + - systemctl restart keyd # Restart the daemon to effect configuration reload. + +# CONFIGURATION + +All configuration files are stored in /etc/keyd. The name of each file should +correspond to the device name (see `-m`) to which it is to be applied followed +by .cfg (e.g /etc/keyd/Magic\ Keyboard.cfg). Configuration files are loaded +upon initialization, thus restarting the daemon is necessary for changes +to take effect. + +If no configuration file exists for a given keyboard and default.cfg is present, it is used. + +Each line in a configuration file consists of a mapping of the following form: + + = | + +or else represents the beginning of a new layer. E.G: + + [] + +Where `` has the form: `[-[-]]` + +and each modifier is one of: + +\ **C** - Control\ +\ **M** - Meta/Super\ +\ **A** - Alt\ +\ **S** - Shift + +In addition to simple key mappings keyd can remap keys to actions which +can conditionally send keystrokes or transform the state of the keymap. + +It is, for instance, possible to map a key to escape when tapped and control when held +by assigning it to mods_on_hold(C, esc). A complete list of available actions can be +found in ACTIONS. + +## LAYERS + +Each configuration file consists of one or more layers. Each layer is a keymap +unto itself and can be activated by a key mapped to the appropriate +action (see ACTIONS). + +By default layers do not have a parent, that is, unmapped keys will have no +effect. The default layer is called 'default' and is used for mappings which +are not explicitly assigned to a layer. + +For example the following configuration creates a new layer called 'symbols' which +is activated while the caps lock key is depressed. + + capslock = layer(symbols) + + [symbols] + + f = S-grave + d = slash + +Pressing capslock+f thus produces a tilde. + +By default unmapped keys inside of a layer do nothing, however +a layer may optionally have a parent from which mappings are +drawn for keys which are not explicitly mapped. A parent +is specified by appending `:` to the layer +name. This is particularly useful for custom letter layouts +like dvorak which remap a subset of keys but otherwise +leave the default mappings in tact. + +## ACTIONS + +**oneshot(mods)**: If tapped activate a modifier sequence for the next keypress, otherwise act as a normal modifier key when held. + +**mods_on_hold(mods, keyseq)**: Activates the given set of modifiers whilst held and emits keysequence when tapped. + +**layer_on_hold(layer, keyseq)**: Activates the given layer whilst held and emits keysequence when tapped. + +**layer_toggle(layer)**: Permanently activate a layer when tapped. *Note*: You will need to explicitly map a toggle in the destination layer if you wish to return. + +**layer(layer)**: Activate the given layer while the key is held down. + +**oneshot_layer(layer)**: If tapped activate a layer for the duration of the next keypress, otherwise act as a normal layer key when held. + +### Legend: + + - `` = A set of modifiers of the form: `[-...]` (e.g C-M = control + meta). + - `` = A key sequence consisting of zero or more control characters and a key (e.g C-a = control+a). + - `` = The name of a layer. + +## Examples + +Example 1 + + # Maps capslock to control when held and escape when tapped. + capslock = mods_on_hold(C, esc) + + # Makes the shift key sticky for one keystroke. + + leftshift = oneshot(S) + rightshift = oneshot(S) + +Example 2 + + # Maps escape to the escape layer when held and the escape key when pressed + + esc = layer_on_hold(escape_layer, esc) + + [escape_layer] + + 1 = layer_toggle(dvorak) + 2 = layer_toggle(default) + + # Creates a dvorak layer which inherits from the default layer. Without + # explicitly inheriting from another layer unmapped keys would be ignored. + + [dvorak:default] + + q = apostrophe + w = comma + e = dot + r = p + t = y + y = f + u = g + i = c + o = r + p = l + + a = a + s = o + d = e + f = u + g = i + h = d + j = h + k = t + l = n + semicolon = s + + z = semicolon + x = q + c = j + v = k + b = x + n = b + m = m + comma = w + dot = v + slash = z + + +# NOTES + +- Because of the way keyd works it is possible to render your machine unusable with a bad + config file. This can usually be resolved by plugging in a different keyboard, however + if *default.cfg* has been misconfigured you will have to find an alternate way to kill + the daemon (e.g SSH). + +# AUTHOR + + - Written by Raheman Vaiya (2017-). + +# BUGS + +Please file any bugs or feature requests at the following url: + +https://github.com/rvaiya/keyd/issues diff --git a/src/config.c b/src/config.c new file mode 100644 index 0000000..3d81895 --- /dev/null +++ b/src/config.c @@ -0,0 +1,601 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * 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 (including the next + * paragraph) 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "keys.h" + +#define MAX_ARGS 10 + +struct keyboard_config *configs = NULL; + +static char layers[MAX_LAYERS][MAX_LAYER_NAME_LEN]; //Layer names +static size_t nlayers; +static int parents[MAX_LAYERS]; + +static int lnum = 0; +static char path[PATH_MAX]; + +#define err(fmt, ...) fprintf(stderr, "%s: ERROR on line %d: "fmt"\n", path, lnum, ##__VA_ARGS__) + +static const char *modseq_to_string(uint8_t mods) { + static char s[32]; + int i = 0; + s[0] = '\0'; + + if(mods & MOD_CTRL) { + strcpy(s+i, "-C"); + i+=2; + } + if(mods & MOD_SHIFT) { + strcpy(s+i, "-S"); + i+=2; + } + + if(mods & MOD_SUPER) { + strcpy(s+i, "-M"); + i+=2; + } + + if(mods & MOD_ALT) { + strcpy(s+i, "-A"); + i+=2; + } + + if(i) + return s+1; + else + return s; +} + +static const char *keyseq_to_string(uint32_t keyseq) { + int i = 0; + static char s[256]; + + uint8_t mods = keyseq >> 16; + uint16_t code = keyseq & 0x00FF; + + const char *key = keycode_strings[code]; + + if(mods & MOD_CTRL) { + strcpy(s+i, "C-"); + i+=2; + } + if(mods & MOD_SHIFT) { + strcpy(s+i, "S-"); + i+=2; + } + + if(mods & MOD_SUPER) { + strcpy(s+i, "M-"); + i+=2; + } + + if(mods & MOD_ALT) { + strcpy(s+i, "A-"); + i+=2; + } + + if(key) + strcpy(s+i, keycode_strings[code]); + else + strcpy(s+i, "UNKNOWN"); + + return s; +} + +static int parse_mods(const char *s, uint8_t *mods) +{ + *mods = 0; + + while(*s) { + switch(*s) { + case 'C': + *mods |= MOD_CTRL; + break; + case 'M': + *mods |= MOD_SUPER; + break; + case 'A': + *mods |= MOD_ALT; + break; + case 'S': + *mods |= MOD_SHIFT; + break; + default: + return -1; + break; + } + + if(s[1] == 0) + return 0; + else if(s[1] != '-') { + err("%s is not a valid modifier set", s); + return -1; + } + + s+=2; + } + + return 0; +} + +static int parse_keyseq(const char *s, uint16_t *keycode, uint8_t *mods) { + const char *c = s; + size_t i; + + *mods = 0; + *keycode = 0; + + while(*c && c[1] == '-') { + switch(*c) { + case 'C': + *mods |= MOD_CTRL; + break; + case 'M': + *mods |= MOD_SUPER; + break; + case 'A': + *mods |= MOD_ALT; + break; + case 'S': + *mods |= MOD_SHIFT; + break; + default: + return -1; + break; + } + + c += 2; + } + + for(i = 0;i < sizeof keycode_strings / sizeof keycode_strings[0];i++) { + if(keycode_strings[i] && !strcmp(keycode_strings[i], c)) { + *keycode |= i; + return 0; + } + } + + return -1; +} + +static int parse_kvp(char *s, char **_k, char **_v) +{ + char *v = NULL, *k = NULL; + + while(*s && isspace(*s)) s++; + if(!*s) return -1; + k = s; + + while(*s && !isspace(*s) && *s != '=') s++; + if(!*s) return -1; + + while(*s && *s != '=') { + *s = 0; + s++; + } + if(!*s) return -1; + *s++ = 0; + + while(*s && isspace(*s)) *s++ = 0; + if(!*s) return -1; + v = s; + + *_k = k; + *_v = v; + return 0; +} + +static void parse_layer_names() +{ + size_t i; + char *line = NULL; + size_t n; + ssize_t len; + char strparents[MAX_LAYERS][MAX_LAYER_NAME_LEN] = {0}; + + FILE *fh = fopen(path, "r"); + if(!fh) { + fprintf(stderr, "ERROR: Failed to open %s\n", path); + perror("fopen"); + exit(-1); + } + + + nlayers = 0; + strcpy(layers[nlayers++], "default"); + while((len=getline(&line, &n, fh)) != -1) { + char *s = line; + + while(len && isspace(s[0])) { + s++; + len--; + } + + if(len > 2 && s[0] == '[' && s[len-2] == ']') { + const char *name = s+1; + size_t idx = nlayers; + char *c; + + s[len-2] = '\0'; + + if((c=strchr(name, ':'))) { + const char *parent = c+1; + *c = '\0'; + + strcpy(strparents[idx], parent); + } + + strcpy(layers[idx], name); + nlayers++; + } + + free(line); + line = NULL; + n = 0; + } + + free(line); + fclose(fh); + + for(i = 0;i < nlayers;i++) { + size_t j; + + parents[i] = -1; + + for(j = 0;j < nlayers;j++) { + if(!strcmp(layers[j], strparents[i])) { + parents[i] = j; + } + } + if(strparents[i][0] != 0 && parents[i] == -1) { + err("%s is not a valid parent layer", strparents[i]); + } + } +} + +int parse_fn(char *s, char **fn_name, char *args[MAX_ARGS], size_t *nargs) +{ + int openparen = 0; + + *fn_name = s; + *nargs = 0; + + while(*s && *s != ')') { + switch(*s) { + case '(': + openparen++; + *s = '\0'; + s++; + + while(*s && isspace(*s)) + s++; + + if(!*s) { + err("Missing closing parenthesis."); + return -1; + } + + if(*s == ')') { //No args + *s = '\0'; + return 0; + } + + args[(*nargs)++] = s; + + break; + case ',': + *s = '\0'; + s++; + while(*s && isspace(*s)) + s++; + + if(!*s) { + err("Missing closing parenthesis."); + return -1; + } + + args[(*nargs)++] = s; + break; + } + + s++; + } + + if(*s != ')') { + if(openparen) + err("Missing closing parenthesis."); + else + err("Invalid function or key sequence."); + + return -1; + } + + *s = '\0'; + + return 0; +} + +int parse_val(const char *_s, struct key_descriptor *desc) +{ + uint16_t code; + uint8_t mods; + + char *s = strdup(_s); + char *fn; + char *args[MAX_ARGS]; + size_t nargs; + + if(!parse_keyseq(s, &code, &mods)) { + desc->action = ACTION_KEYSEQ; + desc->arg.keyseq = ((uint32_t)mods << 16) | (uint32_t)code; + + goto cleanup; + } + + if(parse_fn(s, &fn, args, &nargs)) + goto fail; + + if(!strcmp(fn, "oneshot_layer") && nargs == 1) { + size_t i; + + for(i = 0;i < nlayers;i++) + if(!strcmp(args[0], layers[i])) { + desc->action = ACTION_LAYER_ONESHOT; + desc->arg.layer = i; + + goto cleanup; + } + + err("%s is not a valid layer name.", args[0]); + goto fail; + } else if(!strcmp(fn, "layer") && nargs == 1) { + size_t i; + + for(i = 0;i < nlayers;i++) + if(!strcmp(args[0], layers[i])) { + desc->action = ACTION_LAYER; + desc->arg.layer = i; + + goto cleanup; + } + + err("%s is not a valid layer name.", args[0]); + goto fail; + } else if(!strcmp(fn, "layer_toggle") && nargs == 1) { + size_t i; + + for(i = 0;i < nlayers;i++) + if(!strcmp(args[0], layers[i])) { + desc->action = ACTION_LAYER_TOGGLE; + desc->arg.layer = i; + + goto cleanup; + } + + err("%s is not a valid layer name.", args[0]); + goto fail; + } else if(!strcmp(fn, "oneshot") && nargs == 1) { + if(parse_mods(args[0], &mods)) + goto fail; + + desc->action = ACTION_ONESHOT; + desc->arg.mods = mods; + + } else if(!strcmp(fn, "layer_on_hold") && nargs == 2) { + size_t i; + + desc->action = ACTION_DOUBLE_LAYER; + + if(parse_keyseq(args[1], &code, &mods)) { + err("%s is not a vaid keysequence.", args[1]); + goto fail; + } + + desc->arg2.keyseq = ((uint32_t)mods << 16) | (uint32_t)code; + + for(i = 0;i < nlayers;i++) + if(!strcmp(args[0], layers[i])) { + desc->arg.layer = i; + goto cleanup; + } + + + err("%s is not a valid layer.", args[0]); + goto fail; + } else if(!strcmp(fn, "mods_on_hold") && nargs == 2) { + desc->action = ACTION_DOUBLE_MODIFIER; + + if(parse_mods(args[0], &mods)) + goto fail; + + desc->arg.mods = mods; + + if(parse_keyseq(args[1], &code, &mods)) { + err("%s is not a vaid keysequence.", args[1]); + goto fail; + } + + desc->arg2.keyseq = ((uint32_t)mods << 16) | (uint32_t)code; + + } else { + err("%s is not a valid action or key.", _s); + goto fail; + } + + +cleanup: + free(s); + return 0; + +fail: + free(s); + return -1; +} + +static void parse(struct keyboard_config *cfg) +{ + size_t i, j; + char *line = NULL; + size_t n = 0; + ssize_t len; + + lnum = 0; + parse_layer_names(); + + FILE *fh = fopen(path, "r"); + + if(!fh) { + perror("fopen"); + exit(-1); + } + + int current_layer = 0; + struct key_descriptor *layer = cfg->layers[0]; + + lnum = 0; + + while((len=getline(&line, &n, fh)) != -1) { + lnum++; + char *s = line; + + while(len && isspace(s[0])) { //Trim leading whitespace + s++; + len--; + } + + if(len && s[len-1] == '\n') //Strip tailing newline (not present on EOF) + s[--len] = '\0'; + + if(len == 0 || s[0] == '#') + continue; + + if(s[0] == '[' && s[len-1] == ']') { + layer = cfg->layers[++current_layer]; + } else if(layer) { + struct key_descriptor desc; + char *key, *val; + + uint16_t code; + uint8_t mods; + + if(parse_kvp(s, &key, &val)) { + err("Invalid key value pair."); + goto next; + } + + if(parse_keyseq(key, &code, &mods)) { + err("'%s' is not a valid key.", key); + goto next; + } + + if(mods != 0) { + err("key cannot contain modifiers."); + goto next; + } + + if(parse_val(val, &desc)) + goto next; + + layer[code] = desc; + } + +next: + free(line); + line = NULL; + n = 0; + } + + free(line); + fclose(fh); + + for(i = 0;i < nlayers;i++) { + int p = parents[i]; + if(p != -1) { + for(j = 0;j < KEY_CNT;j++) { + if(cfg->layers[i][j].action == ACTION_DEFAULT) + cfg->layers[i][j] = cfg->layers[p][j]; + } + } + } +} + +void config_free() +{ + struct keyboard_config *cfg = configs; + + while(cfg) { + struct keyboard_config *tmp = cfg; + cfg = cfg->next; + free(tmp); + }; +} + +void config_generate() +{ + struct dirent *ent; + DIR *dh = opendir(CONFIG_DIR); + + if(!dh) { + perror("opendir"); + exit(-1); + } + + while((ent=readdir(dh))) { + size_t i; + struct keyboard_config *cfg; + + int len = strlen(ent->d_name); + if(len <= 4 || strcmp(ent->d_name+len-4, ".cfg")) + continue; + + + sprintf(path, "%s/%s", CONFIG_DIR, ent->d_name); + + cfg = calloc(1, sizeof(struct keyboard_config)); + + for(i = 0;i < KEY_CNT;i++) { + struct key_descriptor *desc = &cfg->layers[0][i]; + desc->action = ACTION_KEYSEQ; + desc->arg.keyseq = i; + } + + strcpy(cfg->name, ent->d_name); + cfg->name[len-4] = '\0'; + + cfg->next = configs; + parse(cfg); + + configs = cfg; + } + + closedir(dh); +} diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..2717b6b --- /dev/null +++ b/src/config.h @@ -0,0 +1,68 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * 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 (including the next + * paragraph) 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. + */ + +#ifndef __H_CONFIG_ +#define __H_CONFIG_ + +#include +#include +#include +#include "keys.h" + +#define MAX_LAYERS 8 +#define CONFIG_DIR "/etc/keyd" +#define MAX_LAYER_NAME_LEN 256 + +enum action { + ACTION_DEFAULT, + ACTION_DOUBLE_MODIFIER, + ACTION_DOUBLE_LAYER, + ACTION_KEYSEQ, + ACTION_LAYER_TOGGLE, + ACTION_LAYER_ONESHOT, + ACTION_LAYER, + ACTION_ONESHOT +}; + +struct key_descriptor +{ + enum action action; + union { + uint32_t keyseq; + uint8_t mods; + uint8_t layer; + } arg, arg2; +}; + +struct keyboard_config { + char name[256]; + + struct key_descriptor layers[MAX_LAYERS][KEY_CNT]; + struct keyboard_config *next; +}; + +extern struct keyboard_config *configs; + +void config_generate(); +void config_free(); + +#endif diff --git a/src/keys.h b/src/keys.h new file mode 100644 index 0000000..59bdc59 --- /dev/null +++ b/src/keys.h @@ -0,0 +1,482 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * 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 (including the next + * paragraph) 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. + */ + +#ifndef _KEYS_H_ +#define _KEYS_H_ +#define _KEYS_H_ + +#include +#include + +#define ISMOD(keycode) \ + (((keycode) == KEY_LEFTCTRL) ||\ + ((keycode) == KEY_LEFTALT) ||\ + ((keycode) == KEY_LEFTSHIFT) ||\ + ((keycode) == KEY_LEFTMETA) ||\ + ((keycode) == KEY_RIGHTCTRL) ||\ + ((keycode) == KEY_RIGHTALT) ||\ + ((keycode) == KEY_RIGHTSHIFT) ||\ + ((keycode) == KEY_RIGHTMETA)) + +#define MOD_CTRL 0x8 +#define MOD_SHIFT 0x4 +#define MOD_SUPER 0x2 +#define MOD_ALT 0x1 + +static const char *keycode_strings[] = { + [KEY_ESC] = "esc", + [KEY_1] = "1", + [KEY_2] = "2", + [KEY_3] = "3", + [KEY_4] = "4", + [KEY_5] = "5", + [KEY_6] = "6", + [KEY_7] = "7", + [KEY_8] = "8", + [KEY_9] = "9", + [KEY_0] = "0", + [KEY_MINUS] = "minus", + [KEY_EQUAL] = "equal", + [KEY_BACKSPACE] = "backspace", + [KEY_TAB] = "tab", + [KEY_Q] = "q", + [KEY_W] = "w", + [KEY_E] = "e", + [KEY_R] = "r", + [KEY_T] = "t", + [KEY_Y] = "y", + [KEY_U] = "u", + [KEY_I] = "i", + [KEY_O] = "o", + [KEY_P] = "p", + [KEY_LEFTBRACE] = "leftbrace", + [KEY_RIGHTBRACE] = "rightbrace", + [KEY_ENTER] = "enter", + [KEY_LEFTCTRL] = "leftctrl", + [KEY_A] = "a", + [KEY_S] = "s", + [KEY_D] = "d", + [KEY_F] = "f", + [KEY_G] = "g", + [KEY_H] = "h", + [KEY_J] = "j", + [KEY_K] = "k", + [KEY_L] = "l", + [KEY_SEMICOLON] = "semicolon", + [KEY_APOSTROPHE] = "apostrophe", + [KEY_GRAVE] = "grave", + [KEY_LEFTSHIFT] = "leftshift", + [KEY_BACKSLASH] = "backslash", + [KEY_Z] = "z", + [KEY_X] = "x", + [KEY_C] = "c", + [KEY_V] = "v", + [KEY_B] = "b", + [KEY_N] = "n", + [KEY_M] = "m", + [KEY_COMMA] = "comma", + [KEY_DOT] = "dot", + [KEY_SLASH] = "slash", + [KEY_RIGHTSHIFT] = "rightshift", + [KEY_KPASTERISK] = "kpasterisk", + [KEY_LEFTALT] = "leftalt", + [KEY_SPACE] = "space", + [KEY_CAPSLOCK] = "capslock", + [KEY_F1] = "f1", + [KEY_F2] = "f2", + [KEY_F3] = "f3", + [KEY_F4] = "f4", + [KEY_F5] = "f5", + [KEY_F6] = "f6", + [KEY_F7] = "f7", + [KEY_F8] = "f8", + [KEY_F9] = "f9", + [KEY_F10] = "f10", + [KEY_NUMLOCK] = "numlock", + [KEY_SCROLLLOCK] = "scrolllock", + [KEY_KP7] = "kp7", + [KEY_KP8] = "kp8", + [KEY_KP9] = "kp9", + [KEY_KPMINUS] = "kpminus", + [KEY_KP4] = "kp4", + [KEY_KP5] = "kp5", + [KEY_KP6] = "kp6", + [KEY_KPPLUS] = "kpplus", + [KEY_KP1] = "kp1", + [KEY_KP2] = "kp2", + [KEY_KP3] = "kp3", + [KEY_KP0] = "kp0", + [KEY_KPDOT] = "kpdot", + [KEY_ZENKAKUHANKAKU] = "zenkakuhankaku", + [KEY_102ND] = "102nd", + [KEY_F11] = "f11", + [KEY_F12] = "f12", + [KEY_RO] = "ro", + [KEY_KATAKANA] = "katakana", + [KEY_HIRAGANA] = "hiragana", + [KEY_HENKAN] = "henkan", + [KEY_KATAKANAHIRAGANA] = "katakanahiragana", + [KEY_MUHENKAN] = "muhenkan", + [KEY_KPJPCOMMA] = "kpjpcomma", + [KEY_KPENTER] = "kpenter", + [KEY_RIGHTCTRL] = "rightctrl", + [KEY_KPSLASH] = "kpslash", + [KEY_SYSRQ] = "sysrq", + [KEY_RIGHTALT] = "rightalt", + [KEY_LINEFEED] = "linefeed", + [KEY_HOME] = "home", + [KEY_UP] = "up", + [KEY_PAGEUP] = "pageup", + [KEY_LEFT] = "left", + [KEY_RIGHT] = "right", + [KEY_END] = "end", + [KEY_DOWN] = "down", + [KEY_PAGEDOWN] = "pagedown", + [KEY_INSERT] = "insert", + [KEY_DELETE] = "delete", + [KEY_MACRO] = "macro", + [KEY_MUTE] = "mute", + [KEY_VOLUMEDOWN] = "volumedown", + [KEY_VOLUMEUP] = "volumeup", + [KEY_POWER] = "power", + [KEY_KPEQUAL] = "kpequal", + [KEY_KPPLUSMINUS] = "kpplusminus", + [KEY_PAUSE] = "pause", + [KEY_SCALE] = "scale", + [KEY_KPCOMMA] = "kpcomma", + [KEY_HANGEUL] = "hangeul", + [KEY_HANJA] = "hanja", + [KEY_YEN] = "yen", + [KEY_LEFTMETA] = "leftmeta", + [KEY_RIGHTMETA] = "rightmeta", + [KEY_COMPOSE] = "compose", + [KEY_STOP] = "stop", + [KEY_AGAIN] = "again", + [KEY_PROPS] = "props", + [KEY_UNDO] = "undo", + [KEY_FRONT] = "front", + [KEY_COPY] = "copy", + [KEY_OPEN] = "open", + [KEY_PASTE] = "paste", + [KEY_FIND] = "find", + [KEY_CUT] = "cut", + [KEY_HELP] = "help", + [KEY_MENU] = "menu", + [KEY_CALC] = "calc", + [KEY_SETUP] = "setup", + [KEY_SLEEP] = "sleep", + [KEY_WAKEUP] = "wakeup", + [KEY_FILE] = "file", + [KEY_SENDFILE] = "sendfile", + [KEY_DELETEFILE] = "deletefile", + [KEY_XFER] = "xfer", + [KEY_PROG1] = "prog1", + [KEY_PROG2] = "prog2", + [KEY_WWW] = "www", + [KEY_MSDOS] = "msdos", + [KEY_COFFEE] = "coffee", + [KEY_ROTATE_DISPLAY] = "display", + [KEY_CYCLEWINDOWS] = "cyclewindows", + [KEY_MAIL] = "mail", + [KEY_BOOKMARKS] = "bookmarks", + [KEY_COMPUTER] = "computer", + [KEY_BACK] = "back", + [KEY_FORWARD] = "forward", + [KEY_CLOSECD] = "closecd", + [KEY_EJECTCD] = "ejectcd", + [KEY_EJECTCLOSECD] = "ejectclosecd", + [KEY_NEXTSONG] = "nextsong", + [KEY_PLAYPAUSE] = "playpause", + [KEY_PREVIOUSSONG] = "previoussong", + [KEY_STOPCD] = "stopcd", + [KEY_RECORD] = "record", + [KEY_REWIND] = "rewind", + [KEY_PHONE] = "phone", + [KEY_ISO] = "iso", + [KEY_CONFIG] = "config", + [KEY_HOMEPAGE] = "homepage", + [KEY_REFRESH] = "refresh", + [KEY_EXIT] = "exit", + [KEY_MOVE] = "move", + [KEY_EDIT] = "edit", + [KEY_SCROLLUP] = "scrollup", + [KEY_SCROLLDOWN] = "scrolldown", + [KEY_KPLEFTPAREN] = "kpleftparen", + [KEY_KPRIGHTPAREN] = "kprightparen", + [KEY_NEW] = "new", + [KEY_REDO] = "redo", + [KEY_F13] = "f13", + [KEY_F14] = "f14", + [KEY_F15] = "f15", + [KEY_F16] = "f16", + [KEY_F17] = "f17", + [KEY_F18] = "f18", + [KEY_F19] = "f19", + [KEY_F20] = "f20", + [KEY_F21] = "f21", + [KEY_F22] = "f22", + [KEY_F23] = "f23", + [KEY_F24] = "f24", + [KEY_PLAYCD] = "playcd", + [KEY_PAUSECD] = "pausecd", + [KEY_PROG3] = "prog3", + [KEY_PROG4] = "prog4", + [KEY_DASHBOARD] = "dashboard", + [KEY_SUSPEND] = "suspend", + [KEY_CLOSE] = "close", + [KEY_PLAY] = "play", + [KEY_FASTFORWARD] = "fastforward", + [KEY_BASSBOOST] = "bassboost", + [KEY_PRINT] = "print", + [KEY_HP] = "hp", + [KEY_CAMERA] = "camera", + [KEY_SOUND] = "sound", + [KEY_QUESTION] = "question", + [KEY_EMAIL] = "email", + [KEY_CHAT] = "chat", + [KEY_SEARCH] = "search", + [KEY_CONNECT] = "connect", + [KEY_FINANCE] = "finance", + [KEY_SPORT] = "sport", + [KEY_SHOP] = "shop", + [KEY_ALTERASE] = "alterase", + [KEY_CANCEL] = "cancel", + [KEY_BRIGHTNESSDOWN] = "brightnessdown", + [KEY_BRIGHTNESSUP] = "brightnessup", + [KEY_MEDIA] = "media", + [KEY_SWITCHVIDEOMODE] = "switchvideomode", + [KEY_KBDILLUMTOGGLE] = "kbdillumtoggle", + [KEY_KBDILLUMDOWN] = "kbdillumdown", + [KEY_KBDILLUMUP] = "kbdillumup", + [KEY_SEND] = "send", + [KEY_REPLY] = "reply", + [KEY_FORWARDMAIL] = "forwardmail", + [KEY_SAVE] = "save", + [KEY_DOCUMENTS] = "documents", + [KEY_BATTERY] = "battery", + [KEY_BLUETOOTH] = "bluetooth", + [KEY_WLAN] = "wlan", + [KEY_UWB] = "uwb", + [KEY_UNKNOWN] = "unknown", + [KEY_VIDEO_NEXT] = "next", + [KEY_VIDEO_PREV] = "prev", + [KEY_BRIGHTNESS_CYCLE] = "cycle", + [KEY_BRIGHTNESS_AUTO] = "auto", + [KEY_DISPLAY_OFF] = "off", + [KEY_WWAN] = "wwan", + [KEY_RFKILL] = "rfkill", + [KEY_MICMUTE] = "micmute", + [KEY_OK] = "ok", + [KEY_SELECT] = "select", + [KEY_GOTO] = "goto", + [KEY_CLEAR] = "clear", + [KEY_POWER2] = "power2", + [KEY_OPTION] = "option", + [KEY_INFO] = "info", + [KEY_TIME] = "time", + [KEY_VENDOR] = "vendor", + [KEY_ARCHIVE] = "archive", + [KEY_PROGRAM] = "program", + [KEY_CHANNEL] = "channel", + [KEY_FAVORITES] = "favorites", + [KEY_EPG] = "epg", + [KEY_PVR] = "pvr", + [KEY_MHP] = "mhp", + [KEY_LANGUAGE] = "language", + [KEY_TITLE] = "title", + [KEY_SUBTITLE] = "subtitle", + [KEY_ANGLE] = "angle", + [KEY_ZOOM] = "zoom", + [KEY_MODE] = "mode", + [KEY_KEYBOARD] = "keyboard", + [KEY_SCREEN] = "screen", + [KEY_PC] = "pc", + [KEY_TV] = "tv", + [KEY_TV2] = "tv2", + [KEY_VCR] = "vcr", + [KEY_VCR2] = "vcr2", + [KEY_SAT] = "sat", + [KEY_SAT2] = "sat2", + [KEY_CD] = "cd", + [KEY_TAPE] = "tape", + [KEY_RADIO] = "radio", + [KEY_TUNER] = "tuner", + [KEY_PLAYER] = "player", + [KEY_TEXT] = "text", + [KEY_DVD] = "dvd", + [KEY_AUX] = "aux", + [KEY_MP3] = "mp3", + [KEY_AUDIO] = "audio", + [KEY_VIDEO] = "video", + [KEY_DIRECTORY] = "directory", + [KEY_LIST] = "list", + [KEY_MEMO] = "memo", + [KEY_CALENDAR] = "calendar", + [KEY_RED] = "red", + [KEY_GREEN] = "green", + [KEY_YELLOW] = "yellow", + [KEY_BLUE] = "blue", + [KEY_CHANNELUP] = "channelup", + [KEY_CHANNELDOWN] = "channeldown", + [KEY_FIRST] = "first", + [KEY_LAST] = "last", + [KEY_AB] = "ab", + [KEY_NEXT] = "next", + [KEY_RESTART] = "restart", + [KEY_SLOW] = "slow", + [KEY_SHUFFLE] = "shuffle", + [KEY_BREAK] = "break", + [KEY_PREVIOUS] = "previous", + [KEY_DIGITS] = "digits", + [KEY_TEEN] = "teen", + [KEY_TWEN] = "twen", + [KEY_VIDEOPHONE] = "videophone", + [KEY_GAMES] = "games", + [KEY_ZOOMIN] = "zoomin", + [KEY_ZOOMOUT] = "zoomout", + [KEY_ZOOMRESET] = "zoomreset", + [KEY_WORDPROCESSOR] = "wordprocessor", + [KEY_EDITOR] = "editor", + [KEY_SPREADSHEET] = "spreadsheet", + [KEY_GRAPHICSEDITOR] = "graphicseditor", + [KEY_PRESENTATION] = "presentation", + [KEY_DATABASE] = "database", + [KEY_NEWS] = "news", + [KEY_VOICEMAIL] = "voicemail", + [KEY_ADDRESSBOOK] = "addressbook", + [KEY_MESSENGER] = "messenger", + [KEY_DISPLAYTOGGLE] = "displaytoggle", + [KEY_SPELLCHECK] = "spellcheck", + [KEY_LOGOFF] = "logoff", + [KEY_DOLLAR] = "dollar", + [KEY_EURO] = "euro", + [KEY_FRAMEBACK] = "frameback", + [KEY_FRAMEFORWARD] = "frameforward", + [KEY_CONTEXT_MENU] = "menu", + [KEY_MEDIA_REPEAT] = "repeat", + [KEY_10CHANNELSUP] = "10channelsup", + [KEY_10CHANNELSDOWN] = "10channelsdown", + [KEY_IMAGES] = "images", + [KEY_DEL_EOL] = "eol", + [KEY_DEL_EOS] = "eos", + [KEY_INS_LINE] = "line", + [KEY_DEL_LINE] = "line", + [KEY_FN] = "fn", + [KEY_FN_ESC] = "esc", + [KEY_FN_F1] = "f1", + [KEY_FN_F2] = "f2", + [KEY_FN_F3] = "f3", + [KEY_FN_F4] = "f4", + [KEY_FN_F5] = "f5", + [KEY_FN_F6] = "f6", + [KEY_FN_F7] = "f7", + [KEY_FN_F8] = "f8", + [KEY_FN_F9] = "f9", + [KEY_FN_F10] = "f10", + [KEY_FN_F11] = "f11", + [KEY_FN_F12] = "f12", + [KEY_FN_1] = "1", + [KEY_FN_2] = "2", + [KEY_FN_D] = "d", + [KEY_FN_E] = "e", + [KEY_FN_F] = "f", + [KEY_FN_S] = "s", + [KEY_FN_B] = "b", + [KEY_BRL_DOT1] = "dot1", + [KEY_BRL_DOT2] = "dot2", + [KEY_BRL_DOT3] = "dot3", + [KEY_BRL_DOT4] = "dot4", + [KEY_BRL_DOT5] = "dot5", + [KEY_BRL_DOT6] = "dot6", + [KEY_BRL_DOT7] = "dot7", + [KEY_BRL_DOT8] = "dot8", + [KEY_BRL_DOT9] = "dot9", + [KEY_BRL_DOT10] = "dot10", + [KEY_NUMERIC_0] = "0", + [KEY_NUMERIC_1] = "1", + [KEY_NUMERIC_2] = "2", + [KEY_NUMERIC_3] = "3", + [KEY_NUMERIC_4] = "4", + [KEY_NUMERIC_5] = "5", + [KEY_NUMERIC_6] = "6", + [KEY_NUMERIC_7] = "7", + [KEY_NUMERIC_8] = "8", + [KEY_NUMERIC_9] = "9", + [KEY_NUMERIC_STAR] = "star", + [KEY_NUMERIC_POUND] = "pound", + [KEY_NUMERIC_A] = "a", + [KEY_NUMERIC_B] = "b", + [KEY_NUMERIC_C] = "c", + [KEY_NUMERIC_D] = "d", + [KEY_CAMERA_FOCUS] = "focus", + [KEY_WPS_BUTTON] = "button", + [KEY_TOUCHPAD_TOGGLE] = "toggle", + [KEY_TOUCHPAD_ON] = "on", + [KEY_TOUCHPAD_OFF] = "off", + [KEY_CAMERA_ZOOMIN] = "zoomin", + [KEY_CAMERA_ZOOMOUT] = "zoomout", + [KEY_CAMERA_UP] = "up", + [KEY_CAMERA_DOWN] = "down", + [KEY_CAMERA_LEFT] = "left", + [KEY_CAMERA_RIGHT] = "right", + [KEY_ATTENDANT_ON] = "on", + [KEY_ATTENDANT_OFF] = "off", + [KEY_ATTENDANT_TOGGLE] = "toggle", + [KEY_LIGHTS_TOGGLE] = "toggle", + [KEY_ALS_TOGGLE] = "toggle", + [KEY_BUTTONCONFIG] = "buttonconfig", + [KEY_TASKMANAGER] = "taskmanager", + [KEY_JOURNAL] = "journal", + [KEY_CONTROLPANEL] = "controlpanel", + [KEY_APPSELECT] = "appselect", + [KEY_SCREENSAVER] = "screensaver", + [KEY_VOICECOMMAND] = "voicecommand", + [KEY_BRIGHTNESS_MIN] = "min", + [KEY_BRIGHTNESS_MAX] = "max", + [KEY_KBDINPUTASSIST_PREV] = "prev", + [KEY_KBDINPUTASSIST_NEXT] = "next", + [KEY_KBDINPUTASSIST_PREVGROUP] = "prevgroup", + [KEY_KBDINPUTASSIST_NEXTGROUP] = "nextgroup", + [KEY_KBDINPUTASSIST_ACCEPT] = "accept", + [KEY_KBDINPUTASSIST_CANCEL] = "cancel", + [KEY_RIGHT_UP] = "up", + [KEY_RIGHT_DOWN] = "down", + [KEY_LEFT_UP] = "up", + [KEY_LEFT_DOWN] = "down", + [KEY_ROOT_MENU] = "menu", + [KEY_MEDIA_TOP_MENU] = "menu", + [KEY_NUMERIC_11] = "11", + [KEY_NUMERIC_12] = "12", + [KEY_AUDIO_DESC] = "desc", + [KEY_3D_MODE] = "mode", + [KEY_NEXT_FAVORITE] = "favorite", + [KEY_STOP_RECORD] = "record", + [KEY_PAUSE_RECORD] = "record", + [KEY_VOD] = "vod", + [KEY_UNMUTE] = "unmute", + [KEY_FASTREVERSE] = "fastreverse", + [KEY_SLOWREVERSE] = "slowreverse", + [KEY_DATA] = "data", + [KEY_MAX] = "max" +}; + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..576d30f --- /dev/null +++ b/src/main.c @@ -0,0 +1,666 @@ +/* Copyright © 2019 Raheman Vaiya. + * + * 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 (including the next + * paragraph) 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "keys.h" +#include "config.h" + +#define UINPUT_DEVICE_NAME "keyd virtual keyboard" +#define MAX_KEYBOARDS 256 +#define LOCK_FILE "/var/lock/keyd.lock" + +static int ufd = -1; + +static struct udev *udev; +static struct udev_monitor *udevmon; +static uint8_t keystate[KEY_CNT] = {0}; + +//Active keyboard state. +struct keyboard { + int fd; + char devnode[256]; + + struct keyboard_config *cfg; + + struct keyboard *next; +}; + +static struct keyboard *keyboards = NULL; +static int debug_flag = 0; + +static void warn(char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + + vfprintf(stderr, fmt, args); + va_end(args); + fprintf(stderr, "\n"); +} + +static void die(char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + + vfprintf(stderr, fmt, args); + va_end(args); + fprintf(stderr, "\n"); + exit(-1); +} + +static void dbg(char *fmt, ...) +{ + if(debug_flag) { + va_list args; + va_start(args, fmt); + + vfprintf(stderr, fmt, args); + va_end(args); + fprintf(stderr, "\n"); + } +} + +static int is_keyboard(struct udev_device *dev) +{ + int is_keyboard = 0; + + const char *path = udev_device_get_devnode(dev); + if(!path || !strstr(path, "event")) //Filter out non evdev devices. + return 0; + + struct udev_list_entry *prop; + udev_list_entry_foreach(prop, udev_device_get_properties_list_entry(dev)) { + //Some mice can also send keypresses, ignore these + if(!strcmp(udev_list_entry_get_name(prop), "ID_INPUT_MOUSE") && + !strcmp(udev_list_entry_get_value(prop), "1")) { + return 0; + } + + if(!strcmp(udev_list_entry_get_name(prop), "ID_INPUT_KEYBOARD") && + !strcmp(udev_list_entry_get_value(prop), "1")) { + is_keyboard = 1; + } + } + + return is_keyboard; +} + +static void get_keyboard_nodes(char *nodes[MAX_KEYBOARDS], int *sz) +{ + struct udev *udev; + struct udev_enumerate *enumerate; + struct udev_list_entry *devices, *ent; + + udev = udev_new(); + if (!udev) + die("Cannot create udev context."); + + enumerate = udev_enumerate_new(udev); + if (!enumerate) + die("Cannot create enumerate context."); + + udev_enumerate_add_match_subsystem(enumerate, "input"); + udev_enumerate_add_match_subsystem(enumerate, "input"); + udev_enumerate_scan_devices(enumerate); + + devices = udev_enumerate_get_list_entry(enumerate); + if (!devices) + die("Failed to get device list."); + + *sz = 0; + udev_list_entry_foreach(ent, devices) { + const char *name = udev_list_entry_get_name(ent);; + struct udev_device *dev = udev_device_new_from_syspath(udev, name); + const char *path = udev_device_get_devnode(dev); + + if(is_keyboard(dev)) { + nodes[*sz] = malloc(strlen(path)+1); + strcpy(nodes[*sz], path); + (*sz)++; + assert(*sz <= MAX_KEYBOARDS); + } + + udev_device_unref(dev); + } + + udev_enumerate_unref(enumerate); + udev_unref(udev); +} + +static int create_uinput_fd() +{ + size_t i; + struct uinput_setup usetup; + + int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); + if(fd < 0) { + perror("open"); + exit(-1); + } + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + ioctl(fd, UI_SET_EVBIT, EV_SYN); + + for(i = 1;i < KEY_CNT;i++) { + if((i >= 0x2c0 && i <= 0x2e7) || //Mouse button ranges. + (i > 248 && i <= 0x151)) + continue; + + ioctl(fd, UI_SET_KEYBIT, i); + } + + + memset(&usetup, 0, sizeof(usetup)); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0x046d; + usetup.id.product = 0xc52b; + strcpy(usetup.name, UINPUT_DEVICE_NAME); + + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); + + return fd; +} + +static void send_key(uint16_t code, int is_pressed) +{ + keystate[code] = is_pressed; + struct input_event ev; + + ev.type = EV_KEY; + ev.code = code; + ev.value = is_pressed; + ev.time.tv_sec = 0; + ev.time.tv_usec = 0; + + write(ufd, &ev, sizeof(ev)); + + ev.type = EV_SYN; + ev.code = 0; + ev.value = 0; + + write(ufd, &ev, sizeof(ev)); +} + +static void send_mods(uint16_t mods, int pressed) +{ + if(mods & MOD_CTRL) + send_key(KEY_LEFTCTRL, pressed); + if(mods & MOD_SHIFT) + send_key(KEY_LEFTSHIFT, pressed); + if(mods & MOD_SUPER) + send_key(KEY_LEFTMETA, pressed); + if(mods & MOD_ALT) + send_key(KEY_LEFTALT, pressed); +} + +static void send_keyseq(uint32_t keyseq, int pressed) +{ + uint8_t mods = keyseq >> 16; + uint16_t code = keyseq; + + send_mods(mods, pressed); + send_key(code, pressed); +} + +//Where the magic happens +static void process_event(struct keyboard *kbd, struct input_event *ev) +{ + static struct keyboard *current_kbd = NULL; + static uint16_t oneshotmods = 0; + static struct key_descriptor *last_pressed = NULL; + static uint8_t layer = 0; + static uint8_t main_layer = 0; + static struct key_descriptor *pressed_keys[KEY_CNT]; + static uint8_t oneshotlayer = 0; + + struct key_descriptor *desc; + int code = ev->code; + int pressed = ev->value; + uint32_t keypressed = 0; + + + if(current_kbd != kbd) { //Reset state when switching keyboards. + main_layer = 0; + layer = 0; + current_kbd = kbd; + } + + if(ev->type != EV_KEY || pressed == 2) + return; + + //Ensure that key descriptors are consistent accross key up/down event pairs. + //This is necessary to accomodate layer changes midkey. + if(pressed_keys[code]) + desc = pressed_keys[code]; + else + desc = &kbd->cfg->layers[layer][code]; + + if(pressed) + pressed_keys[code] = desc; + else + pressed_keys[code] = NULL; + + if(oneshotlayer) { + layer = main_layer; + oneshotlayer = 0; + } + + switch(desc->action) { + uint32_t keyseq; + + case ACTION_KEYSEQ: + keyseq = desc->arg.keyseq; + send_keyseq(keyseq, pressed); + keypressed = keyseq; + + break; + case ACTION_LAYER: + if(pressed) + layer = desc->arg.layer; + else + layer = main_layer; + break; + case ACTION_LAYER_ONESHOT: + if(pressed) { + layer = desc->arg.layer; + } else { + if(last_pressed == desc) //If tapped + oneshotlayer++; + else + layer = main_layer; + } + break; + case ACTION_LAYER_TOGGLE: + if(pressed) { + main_layer = desc->arg.layer; + layer = main_layer; + } + break; + case ACTION_ONESHOT: + if(pressed) + send_mods(desc->arg.mods, 1); + else if(last_pressed == desc) //If no key has been interposed make the modifiers transient + oneshotmods |= desc->arg.mods; + else //otherwise treat as a normal modifier. + send_mods(desc->arg.mods, 0); + + break; + case ACTION_DOUBLE_MODIFIER: + if(pressed) + send_mods(desc->arg.mods, 1); + else { + send_mods(desc->arg.mods, 0); + if(last_pressed == desc) { + send_keyseq(desc->arg2.keyseq, 1); + send_keyseq(desc->arg2.keyseq, 0); + keypressed = desc->arg2.keyseq; + } + } + break; + case ACTION_DOUBLE_LAYER: + if(pressed) { + layer = desc->arg.layer; + } else { + layer = main_layer; + if(last_pressed == desc) { + send_keyseq(desc->arg2.keyseq, 1); + send_keyseq(desc->arg2.keyseq, 0); + keypressed = desc->arg2.keyseq; + } + } + break; + default: + dbg("Ignoring action %d generated by %d", desc->action, code); + break; + } + + if(keypressed && !ISMOD(keypressed & 0xFFFF)) { + send_mods(oneshotmods, 0); + oneshotmods = 0; + } + + if(pressed) + last_pressed = desc; +} + +static const char *evdev_device_name(const char *devnode) +{ + static char name[256]; + + int fd = open(devnode, O_RDONLY); + if(fd < 0) { + perror("open"); + exit(-1); + } + + if(ioctl(fd, EVIOCGNAME(sizeof(name)), &name) == -1) { + perror("ioctl"); + exit(-1); + } + + close(fd); + return name; +} + +static int manage_keyboard(const char *devnode) +{ + int fd; + const char *name = evdev_device_name(devnode); + struct keyboard *kbd; + struct keyboard_config *cfg = NULL; + struct keyboard_config *default_cfg = NULL; + + if(!strcmp(name, UINPUT_DEVICE_NAME)) //Don't manage virtual keyboard + return 0; + + for(cfg = configs;cfg;cfg = cfg->next) { + if(!strcmp(cfg->name, "default")) + default_cfg = cfg; + + if(!strcmp(cfg->name, name)) + break; + } + + if(!cfg) { + if(default_cfg) { + warn("No config found for %s (%s), falling back to default.cfg", name, devnode); + cfg = default_cfg; + } else { + //Don't manage keyboards for which there is no configuration. + warn("No config found for %s (%s), ignoring", name, devnode); + return 0; + } + } + + if((fd = open(devnode, O_RDONLY | O_NONBLOCK)) < 0) { + perror("open"); + exit(1); + } + + kbd = malloc(sizeof(struct keyboard)); + kbd->fd = fd; + kbd->cfg = cfg; + + strcpy(kbd->devnode, devnode); + + //Grab the keyboard. + if(ioctl(fd, EVIOCGRAB, (void *)1) < 0) { + perror("EVIOCGRAB"); + exit(-1); + } + + kbd->next = keyboards; + keyboards = kbd; + + warn("Managing %s", evdev_device_name(devnode)); + return 1; +} + +static int destroy_keyboard(const char *devnode) +{ + struct keyboard **ent = &keyboards; + + while(*ent) { + if(!strcmp((*ent)->devnode, devnode)) { + dbg("Destroying %s", devnode); + struct keyboard *kbd = *ent; + *ent = kbd->next; + + //Attempt to ungrab the the keyboard (assuming it still exists) + if(ioctl(kbd->fd, EVIOCGRAB, (void *)1) < 0) { + perror("EVIOCGRAB"); + } + + close(kbd->fd); + free(kbd); + + return 1; + } + + ent = &(*ent)->next; + } + + return 0; +} + +static void evdev_monitor_loop(int *fds, int sz) +{ + struct input_event ev; + fd_set fdset; + int i; + char names[256][256]; + + for(i = 0;i < sz;i++) { + int fd = fds[i]; + if(ioctl(fd, EVIOCGNAME(sizeof(names[fd])), names[fd]) == -1) { + perror("ioctl"); + exit(-1); + } + } + + while(1) { + int i; + int maxfd = fds[0]; + + FD_ZERO(&fdset); + for(i = 0;i < sz;i++) { + if(maxfd < fds[i]) maxfd = fds[i]; + FD_SET(fds[i], &fdset); + } + + select(maxfd+1, &fdset, NULL, NULL, NULL); + + for(i = 0;i < sz;i++) { + int fd = fds[i]; + if(FD_ISSET(fd, &fdset)) { + while(read(fd, &ev, sizeof(ev)) > 0) { + if(ev.type == EV_KEY && ev.value != 2) { + fprintf(stderr, "%s: %s %s\n", + names[fd], + keycode_strings[ev.code], + ev.value == 0 ? "up" : "down"); + } + } + } + } + } +} + +static int monitor_loop() +{ + char *devnodes[256]; + int sz, i; + int fd = -1; + int fds[256]; + int nfds = 0; + + get_keyboard_nodes(devnodes, &sz); + + for(i = 0;i < sz;i++) { + fd = open(devnodes[i], O_RDONLY | O_NONBLOCK); + if(fd < 0) { + perror("open"); + exit(-1); + } + fds[nfds++] = fd; + } + + evdev_monitor_loop(fds, nfds); + + return 0; +} + +static void main_loop() +{ + struct keyboard *kbd; + int monfd; + + int i, sz; + char *devs[MAX_KEYBOARDS]; + + get_keyboard_nodes(devs, &sz); + + for(i = 0;i < sz;i++) { + manage_keyboard(devs[i]); + free(devs[i]); + } + + udev = udev_new(); + udevmon = udev_monitor_new_from_netlink(udev, "udev"); + + if (!udev) + die("Can't create udev."); + + udev_monitor_filter_add_match_subsystem_devtype(udevmon, "input", NULL); + udev_monitor_enable_receiving(udevmon); + + monfd = udev_monitor_get_fd(udevmon); + + int exit = 0; + while(!exit) { + int maxfd; + fd_set fds; + struct udev_device *dev; + + FD_ZERO(&fds); + FD_SET(monfd, &fds); + + maxfd = monfd; + + for(kbd = keyboards;kbd;kbd=kbd->next) { + int fd = kbd->fd; + + maxfd = maxfd > fd ? maxfd : fd; + FD_SET(fd, &fds); + } + + if(select(maxfd+1, &fds, NULL, NULL, NULL) > 0) { + if(FD_ISSET(monfd, &fds)) { + dev = udev_monitor_receive_device(udevmon); + + const char *devnode = udev_device_get_devnode(dev); + + if(devnode && is_keyboard(dev)) { + const char *action = udev_device_get_action(dev); + + if(!strcmp(action, "add")) + manage_keyboard(devnode); + else if(!strcmp(action, "remove")) + destroy_keyboard(devnode); + } + udev_device_unref(dev); + } + + + for(kbd = keyboards;kbd;kbd=kbd->next) { + int fd = kbd->fd; + + if(FD_ISSET(fd, &fds)) { + struct input_event ev; + + while(read(fd, &ev, sizeof(ev)) > 0) { + process_event(kbd, &ev); + } + } + } + } + } +} + + +static void cleanup() +{ + struct keyboard *kbd = keyboards; + config_free(); + + while(kbd) { + struct keyboard *tmp = kbd; + kbd = kbd->next; + free(tmp); + } + + udev_unref(udev); + udev_monitor_unref(udevmon); +} + +static void lock() +{ + int fd; + + if((fd=open(LOCK_FILE, O_CREAT | O_RDWR, 0600)) == -1) { + perror("open"); + exit(1); + } + + if(flock(fd, LOCK_EX | LOCK_NB) == -1) + die("Another instance of keyd is already running."); +} + + +static void exit_signal_handler(int sig) +{ + warn("%s received, cleaning up and termianting...", sig == SIGINT ? "SIGINT" : "SIGTERM"); + + cleanup(); + exit(0); +} + +int main(int argc, char *argv[]) +{ + if(argc > 1 && !strcmp(argv[1], "-m")) + return monitor_loop(); + if(argc > 1 && !strcmp(argv[1], "-l")) { + size_t i; + for(i = 0; i < sizeof(keycode_strings)/sizeof(keycode_strings[0]);i++) + if(keycode_strings[i]) + printf("%s\n", keycode_strings[i]); + return 0; + } + + lock(); + + signal(SIGINT, exit_signal_handler); + signal(SIGTERM, exit_signal_handler); + + config_generate(); + ufd = create_uinput_fd(); + + main_loop(); +}