diff --git a/data/keyd-application-mapper.1.gz b/data/keyd-application-mapper.1.gz index 6b6d6bc..5afa794 100644 Binary files a/data/keyd-application-mapper.1.gz and b/data/keyd-application-mapper.1.gz differ diff --git a/data/keyd.1.gz b/data/keyd.1.gz index 03b8e2a..4451fdf 100644 Binary files a/data/keyd.1.gz and b/data/keyd.1.gz differ diff --git a/docs/keyd.scdoc b/docs/keyd.scdoc index 00a9ee1..f8ffb4b 100644 --- a/docs/keyd.scdoc +++ b/docs/keyd.scdoc @@ -350,6 +350,24 @@ b = c ``` +## Chording + +_Chords_ are groups of keys which are treated as a unit when simultaneously +depressed. A chord can be defined by using a group of + delimited key names as +a left hand value. The corresponding action will be activated if all keys are +struck within the chording interval (_chord_timeout_). + +E.g + +``` +j+k = esc +``` + +will cause _esc_ to be produced if both _j_ and _k_ are simultaneously depressed. + +Note: It may be desirable to change the default chording interval (50ms) to +account for the physical characteristics of your keyboard. + ## Unicode Support If keyd encounters a valid UTF8 sequence as a right hand value, it will try and @@ -516,6 +534,12 @@ any of the following options: microseconds*) between each emitted key in a macro sequence. This is useful to avoid overflowing the input buffer on some systems. + *chord_timeout:* The maximum time between successive keys + interpreted as part of a chord. (default: 50) + + *chord_hold_timeout:* The length of time a chord + must be held before being activated. (default: 0) + (default: 0) *Note:* Unicode characters and key sequences are treated as macros, and diff --git a/src/config.c b/src/config.c index 5c7d55e..a0367cb 100644 --- a/src/config.c +++ b/src/config.c @@ -190,34 +190,101 @@ static uint8_t lookup_keycode(const char *name) return 0; } +static struct descriptor *layer_lookup_chord(struct layer *layer, uint8_t *keys, size_t n) +{ + size_t i; + + for (i = 0; i < layer->nr_chords; i++) { + size_t j; + size_t nm = 0; + struct chord *chord = &layer->chords[i]; + + for (j = 0; j < n; j++) { + size_t k; + for (k = 0; k < chord->sz; k++) + if (keys[j] == chord->keys[k]) { + nm++; + break; + } + } + + if (nm == n) + return &chord->d; + } + + return NULL; +} + /* * Consumes a string of the form `[.] = ` and adds the * mapping to the corresponding layer in the config. */ -static int set_layer_entry(const struct config *config, struct layer *layer, - const char *key, const struct descriptor *d) +static int set_layer_entry(const struct config *config, + struct layer *layer, char *key, + const struct descriptor *d) { size_t i; int found = 0; - for (i = 0; i < 256; i++) { - if (!strcmp(config->aliases[i], key)) { - layer->keymap[i] = *d; - found = 1; + if (strchr(key, '+')) { + //TODO: Handle aliases + char *tok; + struct descriptor *ld; + uint8_t keys[ARRAY_SIZE(layer->chords[0].keys)]; + size_t n = 0; + + for (tok = strtok(key, "+"); tok; tok = strtok(NULL, "+")) { + uint8_t code = lookup_keycode(tok); + if (!code) { + err("%s is not a valid key", tok); + return -1; + } + + if (n >= ARRAY_SIZE(keys)) { + err("chords cannot contain more than %ld keys", n); + return -1; + } + + keys[n++] = code; } - } - if (!found) { - uint8_t code; - if (!(code = lookup_keycode(key))) { - err("%s is not a valid key or alias", key); - return -1; + if ((ld = layer_lookup_chord(layer, keys, n))) { + *ld = *d; + } else { + struct chord *chord; + if (layer->nr_chords >= ARRAY_SIZE(layer->chords)) { + err("max chords exceeded(%ld)", layer->nr_chords); + return -1; + } + + chord = &layer->chords[layer->nr_chords]; + memcpy(chord->keys, keys, sizeof keys); + chord->sz = n; + chord->d = *d; + + layer->nr_chords++; + } + } else { + for (i = 0; i < 256; i++) { + if (!strcmp(config->aliases[i], key)) { + layer->keymap[i] = *d; + found = 1; + } } - layer->keymap[code] = *d; + if (!found) { + uint8_t code; + + if (!(code = lookup_keycode(key))) { + err("%s is not a valid key or alias", key); + return -1; + } + layer->keymap[code] = *d; + + } } return 0; @@ -234,6 +301,8 @@ static int new_layer(char *s, const struct config *config, struct layer *layer) strcpy(layer->name, name); + layer->nr_chords = 0; + if (strchr(name, '+')) { char *layername; int n = 0; @@ -348,6 +417,9 @@ static void config_init(struct config *config) } /* In ms */ + config->chord_interkey_timeout = 50; + config->chord_hold_timeout = 0; + config->macro_timeout = 600; config->macro_repeat_timeout = 50; @@ -658,6 +730,10 @@ static void parse_global_section(struct config *config, struct ini_section *sect config->macro_timeout = atoi(ent->val); else if (!strcmp(ent->key, "macro_sequence_timeout")) config->macro_sequence_timeout = atoi(ent->val); + else if (!strcmp(ent->key, "chord_hold_timeout")) + config->chord_hold_timeout = atoi(ent->val); + else if (!strcmp(ent->key, "chord_timeout")) + config->chord_interkey_timeout = atoi(ent->val); else if (!strcmp(ent->key, "default_layout")) snprintf(config->default_layout, sizeof config->default_layout, "%s", ent->val); diff --git a/src/config.h b/src/config.h index 7e0ac75..519f03e 100644 --- a/src/config.h +++ b/src/config.h @@ -60,6 +60,13 @@ struct descriptor { union descriptor_arg args[MAX_DESCRIPTOR_ARGS]; }; +struct chord { + uint8_t keys[8]; + size_t sz; + + struct descriptor d; +}; + /* * A layer is a map from keycodes to descriptors. It may optionally * contain one or more modifiers which are applied to the base layout in @@ -80,6 +87,9 @@ struct layer { uint8_t mods; struct descriptor keymap[256]; + struct chord chords[32]; + size_t nr_chords; + /* Used for composite layers. */ size_t nr_constituents; int constituents[8]; @@ -114,6 +124,9 @@ struct config { long macro_sequence_timeout; long macro_repeat_timeout; + long chord_interkey_timeout; + long chord_hold_timeout; + uint8_t layer_indicator; char default_layout[MAX_LAYER_NAME_LEN]; }; diff --git a/src/evloop.c b/src/evloop.c index 131bc46..58e631a 100644 --- a/src/evloop.c +++ b/src/evloop.c @@ -80,6 +80,8 @@ int evloop(int (*event_handler) (struct event *ev)) if (timeout > 0 && elapsed >= timeout) { ev.type = EV_TIMEOUT; + ev.dev = NULL; + ev.devev = NULL; timeout = event_handler(&ev); } else { timeout -= elapsed; diff --git a/src/keyboard.c b/src/keyboard.c index 3ca8c35..7ec1a9d 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -258,6 +258,100 @@ static void activate_layer(struct keyboard *kbd, uint8_t code, int idx) kbd->layer_observer(kbd, kbd->config.layers[idx].name, 1); } +/* Returns: + * 0 on no match + * 1 on partial match + * 2 on exact match + */ +static int chord_event_match(struct chord *chord, struct key_event *events, size_t nevents) +{ + size_t i, j; + size_t n = 0; + size_t npressed = 0; + + if (!nevents) + return 0; + + for (i = 0; i < nevents; i++) + if (events[i].pressed) { + int found = 0; + + npressed++; + for (j = 0; j < chord->sz; j++) + if (chord->keys[j] == events[i].code) + found = 1; + + if (!found) + return 0; + else + n++; + } + + if (npressed == 0) + return 0; + else + return n == chord->sz ? 2 : 1; +} + +static void enqueue_chord_event(struct keyboard *kbd, uint8_t code, uint8_t pressed, long time) +{ + if (!code) + return; + + assert(kbd->chord.queue_sz < ARRAY_SIZE(kbd->chord.queue)); + + kbd->chord.queue[kbd->chord.queue_sz].code = code; + kbd->chord.queue[kbd->chord.queue_sz].pressed = pressed; + kbd->chord.queue[kbd->chord.queue_sz].timestamp = time; + + kbd->chord.queue_sz++; +} + +/* Returns: + * 0 in the case of no match + * 1 in the case of a partial match + * 2 in the case of an unambiguous match (populating d and dl) + * 3 in the case of an ambiguous match (populating d and dl) + */ +static int check_chord_match(struct keyboard *kbd, struct descriptor *d, int *dl) +{ + size_t idx; + int full_match = 0; + int partial_match = 0; + long maxts = -1; + + for (idx = 0; idx < kbd->config.nr_layers; idx++) { + size_t i; + struct layer *layer = &kbd->config.layers[idx]; + + if (!kbd->layer_state[idx].active) + continue; + + for (i = 0; i < layer->nr_chords; i++) { + int ret = chord_event_match(&layer->chords[i], + kbd->chord.queue, + kbd->chord.queue_sz); + + if (ret == 2 && + maxts <= kbd->layer_state[idx].activation_time) { + *d = layer->chords[i].d; + *dl = idx; + full_match = 1; + maxts = kbd->layer_state[idx].activation_time; + } else if (ret == 1) { + partial_match = 1; + } + } + } + + if (full_match) + return partial_match ? 3 : 2; + else if (partial_match) + return 1; + else + return 0; +} + static void execute_command(const char *cmd) { int fd; @@ -346,7 +440,7 @@ static void schedule_timeout(struct keyboard *kbd, long timeout) kbd->timeouts[kbd->nr_timeouts++] = timeout; } -static long calculate_main_loop_timeout(struct keyboard *kbd, long time) +static long calculate_main_loop_timeout(struct keyboard *kbd, long time) { size_t i; long timeout = 0; @@ -365,7 +459,7 @@ static long calculate_main_loop_timeout(struct keyboard *kbd, long time) } static long process_descriptor(struct keyboard *kbd, uint8_t code, - struct descriptor *d, int dl, + const struct descriptor *d, int dl, int pressed, long time) { int timeout = 0; @@ -421,7 +515,7 @@ static long process_descriptor(struct keyboard *kbd, uint8_t code, struct descriptor *action = &kbd->config.descriptors[d->args[1].idx]; kbd->pending_key.code = code; - kbd->pending_key.behaviour = + kbd->pending_key.behaviour = d->op == OP_OVERLOAD_TIMEOUT_TAP ? PK_UNINTERRUPTIBLE_TAP_ACTION2 : PK_UNINTERRUPTIBLE; @@ -647,12 +741,152 @@ struct keyboard *new_keyboard(struct config *config, kbd->config.default_layout); } + kbd->chord.queue_sz = 0; + kbd->chord.state = CHORD_INACTIVE; kbd->output = sink; kbd->layer_observer = layer_observer; return kbd; } +static int resolve_chord(struct keyboard *kbd) +{ + if (kbd->chord.match_sz != 0) { + process_descriptor(kbd, + kbd->chord.start_code, + &kbd->chord.match, + kbd->chord.match_layer, 1, kbd->chord.last_code_time); + cache_set(kbd, + kbd->chord.start_code, + &kbd->chord.match, kbd->chord.match_layer); + } + + kbd->chord.state = CHORD_RESOLVING; + kbd_process_events(kbd, + kbd->chord.queue + kbd->chord.match_sz, + kbd->chord.queue_sz - kbd->chord.match_sz); + kbd->chord.state = CHORD_INACTIVE; + return 1; +} + +static int abort_chord(struct keyboard *kbd) +{ + kbd->chord.match_sz = 0; + return resolve_chord(kbd); +} + +static int handle_chord(struct keyboard *kbd, + uint8_t code, int pressed, long time) +{ + const long interkey_timeout = kbd->config.chord_interkey_timeout; + const long hold_timeout = kbd->config.chord_hold_timeout; + + switch (kbd->chord.state) { + case CHORD_RESOLVING: + return 0; + case CHORD_INACTIVE: + kbd->chord.queue_sz = 0; + kbd->chord.match_sz = 0; + kbd->chord.start_code = code; + + enqueue_chord_event(kbd, code, pressed, time); + switch (check_chord_match(kbd, &kbd->chord.match, &kbd->chord.match_layer)) { + case 0: + return 0; + case 3: + kbd->chord.match_sz = kbd->chord.queue_sz; + case 1: + kbd->chord.state = CHORD_PENDING_DISAMBIGUATION; + kbd->chord.last_code_time = time; + schedule_timeout(kbd, time + interkey_timeout); + return 1; + default: + case 2: + kbd->chord.match_sz = kbd->chord.queue_sz; + + kbd->chord.last_code_time = time; + if (hold_timeout) { + kbd->chord.state = CHORD_PENDING_HOLD_TIMEOUT; + schedule_timeout(kbd, time + hold_timeout); + } else { + return resolve_chord(kbd); + } + return 1; + } + case CHORD_PENDING_DISAMBIGUATION: + if (!code) { + if ((time - kbd->chord.last_code_time) >= interkey_timeout) { + if (kbd->chord.match_sz) { + long timeleft = hold_timeout - interkey_timeout; + if (timeleft > 0) { + schedule_timeout(kbd, time + timeleft); + kbd->chord.state = CHORD_PENDING_HOLD_TIMEOUT; + } else { + return resolve_chord(kbd); + } + } else { + return abort_chord(kbd); + } + + return 1; + } + + return 0; + } + + enqueue_chord_event(kbd, code, pressed, time); + + if (!pressed) + return abort_chord(kbd); + + switch (check_chord_match(kbd, &kbd->chord.match, &kbd->chord.match_layer)) { + case 0: + return abort_chord(kbd); + case 3: + kbd->chord.match_sz = kbd->chord.queue_sz; + case 1: + kbd->chord.last_code_time = time; + + kbd->chord.state = CHORD_PENDING_DISAMBIGUATION; + schedule_timeout(kbd, time + interkey_timeout); + return 1; + default: + case 2: + kbd->chord.last_code_time = time; + + kbd->chord.match_sz = kbd->chord.queue_sz; + if (hold_timeout) { + kbd->chord.state = CHORD_PENDING_HOLD_TIMEOUT; + schedule_timeout(kbd, time + hold_timeout); + } else { + return resolve_chord(kbd); + } + return 1; + } + case CHORD_PENDING_HOLD_TIMEOUT: + if (!code) { + if ((time - kbd->chord.last_code_time) >= hold_timeout) + return resolve_chord(kbd); + + return 0; + } + + enqueue_chord_event(kbd, code, pressed, time); + + if (!pressed) { + size_t i; + + for (i = 0; i < kbd->chord.match_sz; i++) + if (kbd->chord.queue[i].code == code) + return abort_chord(kbd); + } + + return 1; + } + + return 0; +} + /* * `code` may be 0 in the event of a timeout. * @@ -724,6 +958,9 @@ static long process_event(struct keyboard *kbd, uint8_t code, int pressed, long goto exit; } + if (handle_chord(kbd, code, pressed, time)) + goto exit; + if (kbd->active_macro) { if (code) { kbd->active_macro = NULL; @@ -732,7 +969,7 @@ static long process_event(struct keyboard *kbd, uint8_t code, int pressed, long execute_macro(kbd, kbd->active_macro_layer, kbd->active_macro); schedule_timeout(kbd, time+kbd->macro_repeat_timeout); } - } + } if (code) { if (pressed) { @@ -774,14 +1011,14 @@ long kbd_process_events(struct keyboard *kbd, const struct key_event *events, si while (i != n) { const struct key_event *ev = &events[i]; - if (timeout > 0 && timeout_ts < ev->timestamp) { + if (timeout > 0 && timeout_ts <= ev->timestamp) { timeout = process_event(kbd, 0, 0, timeout_ts); + timeout_ts = timeout_ts + timeout; } else { timeout = process_event(kbd, ev->code, ev->pressed, ev->timestamp); + timeout_ts = ev->timestamp + timeout; i++; } - - timeout_ts = ev->timestamp + timeout; } return timeout; diff --git a/src/keyboard.h b/src/keyboard.h index 633b86b..d333ab2 100644 --- a/src/keyboard.h +++ b/src/keyboard.h @@ -50,9 +50,28 @@ struct keyboard { long macro_repeat_timeout; - long timeouts[32]; + long timeouts[64]; size_t nr_timeouts; + struct { + struct key_event queue[32]; + size_t queue_sz; + + struct descriptor match; + int match_layer; + size_t match_sz; + + uint8_t start_code; + long last_code_time; + + enum { + CHORD_RESOLVING, + CHORD_INACTIVE, + CHORD_PENDING_DISAMBIGUATION, + CHORD_PENDING_HOLD_TIMEOUT, + } state; + } chord; + struct { uint8_t code; uint8_t dl; diff --git a/t/chord-disambiguate.t b/t/chord-disambiguate.t new file mode 100644 index 0000000..9634f4b --- /dev/null +++ b/t/chord-disambiguate.t @@ -0,0 +1,30 @@ +a down +b down +200ms +a up +b up +a down +b down +d down +200ms +a up +b up +d up +a down +b down +d down +199ms +a up +b up +d up + +control down +control up +shift down +shift up +a down +b down +d down +a up +b up +d up diff --git a/t/chord-double.t b/t/chord-double.t new file mode 100644 index 0000000..b39b22c --- /dev/null +++ b/t/chord-double.t @@ -0,0 +1,18 @@ +a down +20ms +b down +199ms +a up +b up +j down +k down +201ms +j up +k up + +a down +b down +a up +b up +c down +c up diff --git a/t/chord-hold.t b/t/chord-hold.t new file mode 100644 index 0000000..3679020 --- /dev/null +++ b/t/chord-hold.t @@ -0,0 +1,26 @@ +a down +b down +199ms +x down +x up +a up +b up +1ms +a down +b down +200ms +x down +x up +a up +b up + +a down +b down +x down +x up +a up +b up +control down +x down +x up +control up diff --git a/t/chord.t b/t/chord.t new file mode 100644 index 0000000..f3f9b1f --- /dev/null +++ b/t/chord.t @@ -0,0 +1,25 @@ +a down +b down +150ms +a up +b up +x down +x up +a down +b down +200ms +a up +b up +x down +x up + +a down +b down +a up +b up +x down +x up +control down +control up +x down +x up diff --git a/t/chord2.t b/t/chord2.t new file mode 100644 index 0000000..68c3c04 --- /dev/null +++ b/t/chord2.t @@ -0,0 +1,19 @@ +j down +20ms +k down +200ms +j up +k up +j down +100ms +k down +200ms +j up +k up + +c down +c up +j down +k down +j up +k up diff --git a/t/test.conf b/t/test.conf index 44d1f77..16fa299 100644 --- a/t/test.conf +++ b/t/test.conf @@ -4,12 +4,20 @@ 2fac:2ade +[global] + +chord_interkey_timeout = 100 +chord_hold_timeout = 200 + [main] esc = clear() meta = layer(mymeta) leftalt = layer(myalt) capslock = layer(capslock) +a+b = layer(control) +j+k = c +a+b+d = layer(shift) 1 = layer(layer1) 2 = oneshot(customshift) w = oneshot(customshift)