mirror of
https://github.com/RecentRunner/interception-vimproved-personal.git
synced 2026-06-04 16:19:18 -06:00
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:
2
Makefile
2
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
|
TIMEOUT ?= 10
|
||||||
|
|
||||||
INSTALL_FILE := /opt/interception/interception-vimproved
|
INSTALL_FILE := /opt/interception/interception-vimproved
|
||||||
|
|||||||
88
README.md
88
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main() {
|
bool processInterceptedHeld(Event *input) override {
|
||||||
input_event *input = new input_event();
|
if (this->matches(input->code) && input->value != KEY_STROKE_UP) {
|
||||||
set<int> held_keys;
|
return false; // don't emit anything
|
||||||
enum {
|
}
|
||||||
START,
|
|
||||||
MODIFIER_HELD,
|
if (this->matches(input->code)) { // && stroke up
|
||||||
KEY_HELD
|
// TODO: find a way to have a mouse click mark the key as intercepted
|
||||||
} state_space = START,
|
// or just make it time based
|
||||||
state_caps = START;
|
// this->_shouldEmitTapped &= mouse clicked || timeout;
|
||||||
bool space_not_emitted = true, caps_is_esc = true, ctrl_not_emitted = true;
|
|
||||||
|
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() {
|
||||||
|
auto interceptedKeys = initInterceptedKeys();
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user