diff --git a/Makefile b/Makefile index d6d79dc..a3e7e6b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -CXXFLAGS += -std=c++20 -D_POSIX_C_SOURCE=199309L -O3 -g -Wall -Wextra -Werror -Wno-type-limits +CXXFLAGS += -std=c++20 -D_POSIX_C_SOURCE=199309L -O3 -g -Wall -Wextra -Werror -Wno-unused-parameter -Wno-type-limits TIMEOUT ?= 10 INSTALL_FILE := /opt/interception/interception-vimproved diff --git a/README.md b/README.md index 08d058e..fcc8abe 100644 --- a/README.md +++ b/README.md @@ -4,98 +4,38 @@ My hideous (but working and performant) C++ code to remap the keys on any input ## tl;dr; -* space cadet: Maps space into modifier key when held for long that transforms hjkl (vim homerow) into arrows, number row into F-keys and a bit more. Space still emits SPACE key when tapped without holding. -* caps2esc: Makes CAPS key send ESC when tapped and CTRL when held. +* space cadet: Maps space key into modifier (layer) key when held for long that transforms hjkl (vim homerow) into arrows, number row into F-keys, and a bit more. Space still emits SPACE key when tapped without holding. +* caps2esc: Makes CAPS key send ESC when tapped and L_CTRL when held. +* return2ctrl: Makes RETURN key send RETURN when tapped and R_CTRL when held. ## Requirements -Basically any OS that works with `libevdev` (linux with kernel newer than 2.6.36), no matter what desktop environment, or even if any DE is used (yes, it works the same in X server instead of `xmodmap`, but also in plain terminal, without graphical environment). +Basically any OS that works with `libevdev` (linux with kernel newer than 2.6.36), no matter what desktop environment, or even if any DE is used (yes, it works the same in X server instead of `xmodmap`, but also in plain terminal without graphical environment). ## Running Use it with a job specification for `udevmon` (from [Interception Tools](https://gitlab.com/interception/linux/tools)). I install the binary to `/opt/interception/interception-vimproved` and use it like the following on Arch linux on Thinkpad x1c gen7. ```yaml -- JOB: "intercept -g $DEVNODE | /opt/interception/interception-vimproved | uinput -d $DEVNODE" +- JOB: + - "intercept -g $DEVNODE | /opt/interception/interception-vimproved | uinput -d $DEVNODE" DEVICE: - EVENTS: - EV_KEY: - [ - KEY_SPACE, - - KEY_1, - KEY_2, - KEY_3, - KEY_4, - KEY_5, - KEY_6, - KEY_7, - KEY_8, - KEY_9, - KEY_0, - KEY_MINUS, - KEY_EQUAL, - KEY_F1, - KEY_F2, - KEY_F3, - KEY_F4, - KEY_F5, - KEY_F6, - KEY_F7, - KEY_F8, - KEY_F9, - KEY_F10, - KEY_F11, - KEY_F12, - - KEY_B, - KEY_BACKSPACE, - - KEY_E, - KEY_ESC, - - KEY_D, - KEY_DELETE, - - KEY_Y, - KEY_U, - KEY_I, - KEY_O, - KEY_HOME, - KEY_PAGEDOWN, - KEY_PAGEUP, - KEY_END, - - KEY_H, - KEY_J, - KEY_K, - KEY_L, - KEY_LEFT, - KEY_DOWN, - KEY_UP, - KEY_RIGHT, - - KEY_M, - KEY_COMMA, - KEY_DOT, - KEY_MUTE, - KEY_VOLUMEDOWN, - KEY_VOLUMEUP, - - KEY_CAPSLOCK, - KEY_LEFTCTRL, - ] + NAME: ".*((k|K)(eyboard|EYBOARD)|TADA68).*" ``` -Alternatively, you can run it with `udevmon` straight, just make sure to be negatively nice (`nice -n -20 udevmon -c /etc/udevmon.yml`) so your input is always available. +That matches any udev devices containing keyboard in the name (or my external TADA68 keyboard). + +Alternatively, you can run it with `udevmon` binary straight, just make sure to be negatively nice (`nice -n -20 udevmon -c /etc/udevmon.yml`) so your input is always available. + +### Configuration +Currently, there's no config file, but if you want to experiment with adding/removing/changing the mappings, take a look at the bottom of the ./interception-vimproved.cpp - `initInterceptedKeys` function has comments to guide you. Remember to `make` the project and replace the binary (or point to the new one from your udevmon config). ### Testing In case you want to edit the source code, kill the `udevmon` daemon, and manually try the following to avoid getting stuck with broken input. Trust me, you can get yourself in a dead end situation easily. ```bash -# sleep buys you some time to focus away from terminal to your playground +# sleep buys you some time to focus away from terminal to your playground, also you'll probably need to add a sudo sleep 1 && timeout 10 udevmon -c /etc/udevmon.yml ``` - ## Why make this 1. I have problems switching back and forth between my external keyboard and laptop keyboard. I customized my external keyboard with QMK to reduce my pinky strain and improve usability, but when I switch back to laptop keyboard, it's all lost, plus I have to fight my muscle memory. 2. I used to use X.Org server with xinput, where I had an [xkbcomp based solution with xcape and xmodmap](https://github.com/maricn/dotfiles/blob/master/.xinitrc-keyboard-remap). However, since moving to wayland, that solution doesn't work anymore, and I needed to move to `libevdev` based solution. diff --git a/interception-vimproved.cpp b/interception-vimproved.cpp index dc54967..bdf1ebc 100644 --- a/interception-vimproved.cpp +++ b/interception-vimproved.cpp @@ -11,77 +11,59 @@ using namespace std; /** * Global constants **/ -#define MS_TO_NS 1000000L // 1 millisecond = 1,000,000 Nanoseconds - -typedef struct input_event event; - const int KEY_STROKE_UP = 0, KEY_STROKE_DOWN = 1, KEY_STROKE_REPEAT = 2; -const int INPUT_BUFFER_SIZE = 16; -const long SLEEP_INTERVAL_NS = 20 * MS_TO_NS; const int input_event_struct_size = sizeof(struct input_event); +/** + * Only very rare keys are above 0x151, not found on most of keyboards, probably + * you don't need to mimic them. We cover above 0x100 which includes mouse BTNs. + * Check what else is there at: + * https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h + **/ +const unsigned short MAX_KEY = 0x151; + +typedef struct input_event Event; + // clang-format off -const struct input_event -_syn = {.time = { .tv_sec = 0, .tv_usec = 0}, .type = EV_SYN, .code = SYN_REPORT, .value = KEY_STROKE_UP}; -const struct input_event -*syn = &_syn; +const Event +_syn = {.time = { .tv_sec = 0, .tv_usec = 0}, .type = EV_SYN, .code = SYN_REPORT, .value = KEY_STROKE_UP}; +const Event +*syn = &_syn; // clang-format on -unsigned short map_space[KEY_MAX]; -void map_space_init() { - // special chars - map_space[KEY_E] = KEY_ESC; - map_space[KEY_D] = KEY_DELETE; - map_space[KEY_B] = KEY_BACKSPACE; - - // vim home row - map_space[KEY_H] = KEY_LEFT; - map_space[KEY_J] = KEY_DOWN; - map_space[KEY_K] = KEY_UP; - map_space[KEY_L] = KEY_RIGHT; - - // vim above home row - map_space[KEY_Y] = KEY_HOME; - map_space[KEY_U] = KEY_PAGEDOWN; - map_space[KEY_I] = KEY_PAGEUP; - map_space[KEY_O] = KEY_END; - - // number row to F keys - map_space[KEY_1] = KEY_F1; - map_space[KEY_2] = KEY_F2; - map_space[KEY_3] = KEY_F3; - map_space[KEY_4] = KEY_F4; - map_space[KEY_5] = KEY_F5; - map_space[KEY_6] = KEY_F6; - map_space[KEY_7] = KEY_F7; - map_space[KEY_8] = KEY_F8; - map_space[KEY_9] = KEY_F9; - map_space[KEY_0] = KEY_F10; - map_space[KEY_MINUS] = KEY_F11; - map_space[KEY_EQUAL] = KEY_F12; - - // xf86 audio - map_space[KEY_M] = KEY_MUTE; - map_space[KEY_COMMA] = KEY_VOLUMEDOWN; - map_space[KEY_DOT] = KEY_VOLUMEUP; -} - -int read_event(event *e) { +int readEvent(Event *e) { return fread(e, input_event_struct_size, 1, stdin) == 1; } -void write_event(event *e) { +void writeEvent(const Event *e) { if (fwrite(e, input_event_struct_size, 1, stdout) != 1) exit(EXIT_FAILURE); } -void write_events(vector *events) { +void writeEvents(const vector *events) { const unsigned long size = events->size(); if (fwrite(events->data(), input_event_struct_size, size, stdout) != size) exit(EXIT_FAILURE); } -void write_combo(unsigned short keycode) { +// @TODO: convert to a map cache to reduce amount of memory allocations +Event buildEvent(int direction, unsigned short keycode) { + input_event ev = {.time = {.tv_sec = 0, .tv_usec = 0}, + .type = EV_KEY, + .code = keycode, + .value = direction}; + return ev; +} + +Event buildEventDown(unsigned short keycode) { + return buildEvent(KEY_STROKE_DOWN, keycode); +} + +Event buildEventUp(unsigned short keycode) { + return buildEvent(KEY_STROKE_UP, keycode); +} + +void writeCombo(unsigned short keycode) { input_event key_down = {.time = {.tv_sec = 0, .tv_usec = 0}, .type = EV_KEY, .code = keycode, @@ -95,171 +77,351 @@ void write_combo(unsigned short keycode) { combo->push_back(key_down); combo->push_back(*syn); combo->push_back(key_up); - write_events(combo); + writeEvents(combo); delete combo; } -struct input_event map(const struct input_event *input, int direction = -1) { - struct input_event result(*input); - result.code = map_space[input->code]; - if (direction != -1) { - result.value = direction; +/*************************************************************** + *********************** Global classes ************************ + **************************************************************/ + +/** + * Intercepted key specification + **/ +class InterceptedKey { +public: + enum State { START = 0, INTERCEPTED_KEY_HELD = 1, OTHER_KEY_HELD = 2 }; + + static int isModifier(int key) { + switch (key) { + default: + return 0; + // clang-format off + case KEY_LEFTSHIFT: case KEY_RIGHTSHIFT: + case KEY_LEFTCTRL: case KEY_RIGHTCTRL: + case KEY_LEFTALT: case KEY_RIGHTALT: + case KEY_LEFTMETA: case KEY_RIGHTMETA: + // clang-format on + // @TODO: handle capslock as not modifier? + case KEY_CAPSLOCK: + return 1; + } } - return result; + + InterceptedKey(unsigned short intercepted, unsigned short tapped) { + this->_intercepted = intercepted; + this->_tapped = tapped; + } + + bool matches(unsigned short code) { return this->_intercepted == code; } + + State getState() { return this->_state; } + + bool process(Event *input) { + switch (this->_state) { + case START: + return this->processStart(input); + case INTERCEPTED_KEY_HELD: + return this->processInterceptedHeld(input); + case OTHER_KEY_HELD: + return this->processOtherKeyHeld(input); + } + + throw std::exception(); + } + +protected: + bool processStart(Event *input) { + if (this->matches(input->code) && input->value == KEY_STROKE_DOWN) { + this->_shouldEmitTapped = true; + this->_state = INTERCEPTED_KEY_HELD; + return false; + } + + return true; + }; + + virtual bool processInterceptedHeld(Event *input) = 0; + virtual bool processOtherKeyHeld(Event *input) = 0; + + unsigned short _intercepted; + unsigned short _tapped; + State _state; + bool _shouldEmitTapped = true; +}; + +class InterceptedKeyLayer : public InterceptedKey { +private: + unsigned short *_map; + set *_heldKeys = new set(); + +protected: + Event map(Event *input) { + Event result(*input); + result.code = this->_map[input->code]; + return result; + } + + bool processInterceptedHeld(Event *input) override { + if (this->matches(input->code) && input->value != KEY_STROKE_UP) { + return false; // don't emit anything + } + + if (this->matches(input->code)) { // && stroke up + // TODO: find a way to have a mouse click mark the key as intercepted + // or just make it time based + // this->_shouldEmitTapped &= mouse clicked || timeout; + + if (this->_shouldEmitTapped) { + writeCombo(this->_tapped); + this->_shouldEmitTapped = false; + } + + this->_state = START; + return false; + } + + if (input->value == KEY_STROKE_DOWN) { // any other key + + // @NOTE: if we don't blindly set _shouldEmitTapped to false on any + // keypress, we can type faster because only in case of mapped key down, + // the intercepted key will not be emitted - useful for scenario: + // L_DOWN, SPACE_DOWN, A_DOWN, L_UP, A_UP, SPACE_UP + this->_shouldEmitTapped &= !this->hasMapped(input->code) && + !InterceptedKey::isModifier(input->code); + + if (this->hasMapped(input->code)) { + this->_heldKeys->insert(input->code); + Event mapped = this->map(input); + writeEvent(&mapped); + this->_state = InterceptedKey::OTHER_KEY_HELD; + return false; + } + } + + return true; + } + + bool processOtherKeyHeld(Event *input) override { + if (input->code == this->_intercepted && input->value != KEY_STROKE_UP) + return false; + if (input->value == KEY_STROKE_DOWN && + this->_heldKeys->find(input->code) != this->_heldKeys->end()) { + return false; + } + + bool shouldEmitInput = true; + if (input->value == KEY_STROKE_UP) { + + if (this->_heldKeys->find(input->code) != + this->_heldKeys->end()) { // one of mapped held keys goes up + Event mapped = this->map(input); + writeEvent(&mapped); + this->_heldKeys->erase(input->code); + if (this->_heldKeys->empty()) { + this->_state = InterceptedKey::INTERCEPTED_KEY_HELD; + } + shouldEmitInput = false; + } else { // key that was not mapped & held goes up + if (this->matches(input->code)) { + vector *held_keys_up = new vector(); + for (auto held_key_code : *this->_heldKeys) { + Event held_key_up = buildEventUp(this->_map[held_key_code]); + held_keys_up->push_back(held_key_up); + held_keys_up->push_back(*syn); + } + + writeEvents(held_keys_up); + delete held_keys_up; + this->_heldKeys->clear(); + this->_state = InterceptedKey::START; + shouldEmitInput = false; + } + } + } else { // KEY_STROKE_DOWN or KEY_STROKE_REPEAT + if (this->hasMapped(input->code)) { + auto mapped = this->map(input); + writeEvent(&mapped); + if (input->value == KEY_STROKE_DOWN) { + this->_heldKeys->insert(input->code); + } + shouldEmitInput = false; + } + } + + return shouldEmitInput; + } + +public: + InterceptedKeyLayer(unsigned short intercepted, unsigned short tapped) + : InterceptedKey(intercepted, tapped) { + this->_map = new unsigned short[MAX_KEY]{0}; + } + + ~InterceptedKeyLayer() { delete this->_map; } + + InterceptedKey *addMapping(unsigned short from, unsigned short to) { + this->_map[from] = to; + return this; + } + + bool hasMapped(unsigned short from) { return this->_map[from] != 0; } +}; + +class InterceptedKeyModifier : public InterceptedKey { +protected: + unsigned short _modifier; + + bool processInterceptedHeld(Event *input) override { + if (this->matches(input->code) && input->value != KEY_STROKE_UP) { + return false; + } + + bool shouldEmitInput = true; + if (this->matches(input->code)) { // && stroke up + if (this->_shouldEmitTapped) { + writeCombo(this->_tapped); + } else { // intercepted is mapped to modifier and key stroke up + Event *modifier_up = new Event(*input); + modifier_up->code = this->_modifier; + writeEvent(modifier_up); + delete modifier_up; + } + + this->_state = START; + return false; + } + + if (input->value == KEY_STROKE_DOWN) { // any other than intercepted + if (this->_shouldEmitTapped) { // on first non-matching input after a + // matching input + Event *modifier_down = new Event(*input); + modifier_down->code = this->_modifier; + + // for some reason, need to push "syn" after modifier here + vector *modifier_and_input = new vector(); + modifier_and_input->push_back(*modifier_down); + modifier_and_input->push_back(*syn); + writeEvents(modifier_and_input); + + this->_shouldEmitTapped = false; + delete modifier_and_input; + return true; // gotta emit input event independently so we can process layer+modifier+input together + } + } + + return shouldEmitInput; + } + + bool processOtherKeyHeld(Event *input) override { return true; } + +public: + InterceptedKeyModifier(unsigned short intercepted, unsigned short tapped, + unsigned short modifier) + : InterceptedKey(intercepted, tapped) { + if (!InterceptedKey::isModifier(modifier)) + throw invalid_argument("Specified wrong modifier key"); + this->_modifier = modifier; + } +}; + +vector *initInterceptedKeys() { + // tap space for space, hold for layer mapping + InterceptedKeyLayer *space = new InterceptedKeyLayer(KEY_SPACE, KEY_SPACE); + + // special chars + space->addMapping(KEY_E, KEY_ESC); + space->addMapping(KEY_D, KEY_DELETE); + space->addMapping(KEY_B, KEY_BACKSPACE); + + // vim home row + space->addMapping(KEY_H, KEY_LEFT); + space->addMapping(KEY_J, KEY_DOWN); + space->addMapping(KEY_K, KEY_UP); + space->addMapping(KEY_L, KEY_RIGHT); + + // vim above home row + space->addMapping(KEY_Y, KEY_HOME); + space->addMapping(KEY_U, KEY_PAGEDOWN); + space->addMapping(KEY_I, KEY_PAGEUP); + space->addMapping(KEY_O, KEY_END); + + // number row to F keys + space->addMapping(KEY_1, KEY_F1); + space->addMapping(KEY_2, KEY_F2); + space->addMapping(KEY_3, KEY_F3); + space->addMapping(KEY_4, KEY_F4); + space->addMapping(KEY_5, KEY_F5); + space->addMapping(KEY_6, KEY_F6); + space->addMapping(KEY_7, KEY_F7); + space->addMapping(KEY_8, KEY_F8); + space->addMapping(KEY_9, KEY_F9); + space->addMapping(KEY_0, KEY_F10); + space->addMapping(KEY_MINUS, KEY_F11); + space->addMapping(KEY_EQUAL, KEY_F12); + + // xf86 audio + space->addMapping(KEY_M, KEY_MUTE); + space->addMapping(KEY_COMMA, KEY_VOLUMEDOWN); + space->addMapping(KEY_DOT, KEY_VOLUMEUP); + + // mouse navigation + space->addMapping(BTN_LEFT, BTN_BACK); + space->addMapping(BTN_RIGHT, BTN_FORWARD); + + // @FIXME: this is not working, even though `wev` says keycode 99 is Print + // PrtSc -> Context Menu + space->addMapping(KEY_SYSRQ, KEY_CONTEXT_MENU); + + // tap caps for esc, hold for ctrl + InterceptedKeyModifier *caps = + new InterceptedKeyModifier(KEY_CAPSLOCK, KEY_ESC, KEY_LEFTCTRL); + + // tap enter for enter, hold for ctrl + InterceptedKeyModifier *enter = + new InterceptedKeyModifier(KEY_ENTER, KEY_ENTER, KEY_RIGHTCTRL); + + // @NOTE: modifier keys must go first because layerKey.processInterceptedHeld + // emits mapped key as soon as the for loop calls layerKey.process.. + // if that process is run before modifierKey.process, the modifier key will + // not be emitted + vector *interceptedKeys = new vector(); + interceptedKeys->push_back(caps); + interceptedKeys->push_back(enter); + interceptedKeys->push_back(space); + return interceptedKeys; } int main() { - input_event *input = new input_event(); - set held_keys; - enum { - START, - MODIFIER_HELD, - KEY_HELD - } state_space = START, - state_caps = START; - bool space_not_emitted = true, caps_is_esc = true, ctrl_not_emitted = true; + auto interceptedKeys = initInterceptedKeys(); - map_space_init(); setbuf(stdin, NULL), setbuf(stdout, NULL); - while (read_event(input)) { + /* event *input = (event*) malloc(input_event_struct_size); */ + Event *input = new Event(); + while (readEvent(input)) { if (input->type == EV_MSC && input->code == MSC_SCAN) continue; if (input->type != EV_KEY) { - write_event(input); + writeEvent(input); continue; } - switch (state_caps) { - case START: - if (input->code == KEY_CAPSLOCK && input->value == KEY_STROKE_DOWN) { - caps_is_esc = true; - ctrl_not_emitted = true; - state_caps = MODIFIER_HELD; - continue; - } - break; - case MODIFIER_HELD: - if (input->code == KEY_CAPSLOCK && input->value != KEY_STROKE_UP) - break; - if (input->code == KEY_CAPSLOCK) { - if (caps_is_esc) { // and key stroke up - write_combo(KEY_ESC); - } else { // caps is ctrl and key stroke up - event ctrl_up(*input); - ctrl_up.code = KEY_LEFTCTRL; - write_event(&ctrl_up); - } - state_caps = START; - continue; - } else if (input->value != - KEY_STROKE_UP) { // any key != capslock goes down or repeat - // TODO: find a way to have a mouse click mark caps as ctrl - // or just make it time based - caps_is_esc = false; - if (ctrl_not_emitted) { - event ctrl_down = {.time = {.tv_sec = 0, .tv_usec = 0}, - .type = EV_KEY, - .code = KEY_LEFTCTRL, - .value = KEY_STROKE_DOWN}; - write_event(&ctrl_down); - ctrl_not_emitted = false; - } - } - break; - default: - break; + /* cerr << input->type << "," << input->code << " "; */ + + bool shouldEmitInput = true; + for (auto key : *interceptedKeys) { + shouldEmitInput &= key->process(input); } - switch (state_space) { - case START: - if (input->code == KEY_SPACE && input->value != KEY_STROKE_UP) { - space_not_emitted = true; - state_space = MODIFIER_HELD; - } else { - write_event(input); - } - break; - case MODIFIER_HELD: - if (input->code == KEY_SPACE && input->value != KEY_STROKE_UP) - break; - if (input->value == KEY_STROKE_DOWN) { - if (map_space[input->code] != 0) { // mapped key down - held_keys.insert(input->code); - space_not_emitted = false; - event mapped = map(input); - write_event(&mapped); - state_space = KEY_HELD; - } else { // key stroke down any unmapped key - if (input->code == KEY_CAPSLOCK) { - space_not_emitted = false; - } - write_event(input); - } - } else { // KEY_STROKE_REPEAT or KEY_STROKE_UP - if (input->code == KEY_SPACE && space_not_emitted) { // && stroke up - write_combo(KEY_SPACE); - space_not_emitted = false; - } else { - write_event(input); - } - if (input->code == KEY_SPACE && input->value == KEY_STROKE_UP) { - state_space = START; - } - } - break; - case KEY_HELD: - if (input->code == KEY_SPACE && input->value != KEY_STROKE_UP) - break; - if (input->value == KEY_STROKE_DOWN && - held_keys.find(input->code) != held_keys.end()) { - break; - } - - if (input->value == KEY_STROKE_UP) { - if (held_keys.find(input->code) != - held_keys.end()) { // one of mapped held keys goes up - struct input_event mapped = map(input); - write_event(&mapped); - held_keys.erase(input->code); - if (held_keys.empty()) { - state_space = MODIFIER_HELD; - } - } else { // key that was not mapped & held goes up - if (input->code == KEY_SPACE) { - vector *held_keys_up = new vector(); - for (auto held_key_code : held_keys) { - event held_key_up = {.time = {.tv_sec = 0, .tv_usec = 0}, - .type = EV_KEY, - .code = map_space[held_key_code], - .value = KEY_STROKE_UP}; - held_keys_up->push_back(held_key_up); - } - - write_events(held_keys_up); - delete held_keys_up; - held_keys.clear(); - state_space = START; - } else { // unmapped key goes up - write_event(input); - } - } - } else { // KEY_STROKE_DOWN or KEY_STROKE_REPEAT - if (map_space[input->code] != 0) { - auto mapped = map(input); - write_event(&mapped); - if (input->value == KEY_STROKE_DOWN) { - held_keys.insert(input->code); - } - } else { - write_event(input); - } - } - break; + if (shouldEmitInput) { + writeEvent(input); } } - free(input); + /* free(input); */ + delete input; + delete interceptedKeys; }