From 5fecce7720f123d04aa5731a1a66fac7545b5793 Mon Sep 17 00:00:00 2001 From: Raheman Vaiya Date: Thu, 22 Jul 2021 16:06:59 -0400 Subject: [PATCH] First commit. --- .gitignore | 7 + LICENSE | 21 ++ Makefile | 16 ++ README.md | 100 ++++++++ TODO | 4 + keyd.1.gz | Bin 0 -> 2457 bytes keyd.service | 11 + man.1.gz | Bin 0 -> 2362 bytes man.md | 190 +++++++++++++++ src/config.c | 601 ++++++++++++++++++++++++++++++++++++++++++++++ src/config.h | 68 ++++++ src/keys.h | 482 +++++++++++++++++++++++++++++++++++++ src/main.c | 666 +++++++++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 2166 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 TODO create mode 100644 keyd.1.gz create mode 100644 keyd.service create mode 100644 man.1.gz create mode 100644 man.md create mode 100644 src/config.c create mode 100644 src/config.h create mode 100644 src/keys.h create mode 100644 src/main.c 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 0000000000000000000000000000000000000000..ca43393ce4b3b62647710fd02d4f199ecbb5cd9f GIT binary patch literal 2457 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 literal 0 HcmV?d00001 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(); +}