feat: generic key definition (#1)

* ivimp-1: refactor for generic handling of keys

* Get CAPS and SPACE code as close to each other as possible.
  * Remove `caps_is_esc` and use `caps_tapped_should_emit` instead
  * Use `continue` keyword after successfully handling the event (work towards daisy chaining)

* wip: started moving to classes

* feat: Now additional mappings can be easily added

* Refactored to OOP C++ classes
* Each key handles its own processing.. these types available:
  * tap and hold for layer mappings
  * tap and hold for modifier key

* feat: extend to cover mouse buttons (not impl)

* fix: combos need to be emitted with structs

* fix: combos emitting doesn't work with typedef Cpp-like constructs
* fix: enable adding other layer keys except KEY_SPACE (bug on checking `input->code == KEY_SPACE` hardcoded)
* feat: helper function for building events
* chore: rename `event` to `Event`, cleanup cerr output

* fix: layer+modifier+input

* you can try it out in firefox in input box, try space+caps+h/l to move around words - if ctrl+h for history is emitted, it's a fail

* chore: Update README.md - configurability
This commit is contained in:
Nikola Marić
2021-09-23 16:47:58 +02:00
committed by GitHub
parent 0b47a7cb17
commit f4aa0fa923
3 changed files with 374 additions and 272 deletions

View File

@@ -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 TIMEOUT ?= 10
INSTALL_FILE := /opt/interception/interception-vimproved INSTALL_FILE := /opt/interception/interception-vimproved

View File

@@ -4,98 +4,38 @@ My hideous (but working and performant) C++ code to remap the keys on any input
## tl;dr; ## 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. * 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 CTRL when held. * 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 ## 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 ## 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. 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 ```yaml
- JOB: "intercept -g $DEVNODE | /opt/interception/interception-vimproved | uinput -d $DEVNODE" - JOB:
- "intercept -g $DEVNODE | /opt/interception/interception-vimproved | uinput -d $DEVNODE"
DEVICE: DEVICE:
EVENTS: NAME: ".*((k|K)(eyboard|EYBOARD)|TADA68).*"
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,
]
``` ```
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 ### 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. 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 ```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 sleep 1 && timeout 10 udevmon -c /etc/udevmon.yml
``` ```
## Why make this ## 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. 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. 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.

View File

@@ -11,77 +11,59 @@ using namespace std;
/** /**
* Global constants * 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 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); 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 // clang-format off
const struct input_event const Event
_syn = {.time = { .tv_sec = 0, .tv_usec = 0}, .type = EV_SYN, .code = SYN_REPORT, .value = KEY_STROKE_UP}; _syn = {.time = { .tv_sec = 0, .tv_usec = 0}, .type = EV_SYN, .code = SYN_REPORT, .value = KEY_STROKE_UP};
const struct input_event const Event
*syn = &_syn; *syn = &_syn;
// clang-format on // clang-format on
unsigned short map_space[KEY_MAX]; int readEvent(Event *e) {
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) {
return fread(e, input_event_struct_size, 1, stdin) == 1; 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) if (fwrite(e, input_event_struct_size, 1, stdout) != 1)
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
void write_events(vector<input_event> *events) { void writeEvents(const vector<input_event> *events) {
const unsigned long size = events->size(); const unsigned long size = events->size();
if (fwrite(events->data(), input_event_struct_size, size, stdout) != size) if (fwrite(events->data(), input_event_struct_size, size, stdout) != size)
exit(EXIT_FAILURE); 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}, input_event key_down = {.time = {.tv_sec = 0, .tv_usec = 0},
.type = EV_KEY, .type = EV_KEY,
.code = keycode, .code = keycode,
@@ -95,171 +77,351 @@ void write_combo(unsigned short keycode) {
combo->push_back(key_down); combo->push_back(key_down);
combo->push_back(*syn); combo->push_back(*syn);
combo->push_back(key_up); combo->push_back(key_up);
write_events(combo); writeEvents(combo);
delete combo; delete combo;
} }
struct input_event map(const struct input_event *input, int direction = -1) { /***************************************************************
struct input_event result(*input); *********************** Global classes ************************
result.code = map_space[input->code]; **************************************************************/
if (direction != -1) {
result.value = direction; /**
* 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<unsigned short> *_heldKeys = new set<unsigned short>();
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<Event> *held_keys_up = new vector<Event>();
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<Event> *modifier_and_input = new vector<Event>();
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<InterceptedKey *> *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<InterceptedKey *> *interceptedKeys = new vector<InterceptedKey *>();
interceptedKeys->push_back(caps);
interceptedKeys->push_back(enter);
interceptedKeys->push_back(space);
return interceptedKeys;
} }
int main() { int main() {
input_event *input = new input_event(); auto interceptedKeys = initInterceptedKeys();
set<int> 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;
map_space_init();
setbuf(stdin, NULL), setbuf(stdout, NULL); 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) if (input->type == EV_MSC && input->code == MSC_SCAN)
continue; continue;
if (input->type != EV_KEY) { if (input->type != EV_KEY) {
write_event(input); writeEvent(input);
continue; continue;
} }
switch (state_caps) { /* cerr << input->type << "," << input->code << " "; */
case START:
if (input->code == KEY_CAPSLOCK && input->value == KEY_STROKE_DOWN) { bool shouldEmitInput = true;
caps_is_esc = true; for (auto key : *interceptedKeys) {
ctrl_not_emitted = true; shouldEmitInput &= key->process(input);
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;
} }
switch (state_space) { if (shouldEmitInput) {
case START: writeEvent(input);
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<input_event> *held_keys_up = new vector<input_event>();
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;
} }
} }
free(input); /* free(input); */
delete input;
delete interceptedKeys;
} }