Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Compose Onboard Feature #8359

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions common_features.mk
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,11 @@ ifeq ($(strip $(LEADER_ENABLE)), yes)
OPT_DEFS += -DLEADER_ENABLE
endif

ifeq ($(strip $(COMPOSE_ONBOARD_ENABLE)), yes)
SRC += $(QUANTUM_DIR)/process_keycode/process_compose_onboard.c
OPT_DEFS += -DCOMPOSE_ONBOARD_ENABLE
endif

ifeq ($(strip $(AUTO_SHIFT_ENABLE)), yes)
SRC += $(QUANTUM_DIR)/process_keycode/process_auto_shift.c
OPT_DEFS += -DAUTO_SHIFT_ENABLE
Expand Down
1 change: 1 addition & 0 deletions docs/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
* [Dynamic Macros](feature_dynamic_macros.md)
* [Grave Escape](feature_grave_esc.md)
* [Leader Key](feature_leader_key.md)
* [Compose Onboard](feature_compose_onboard.md)
* [Mod-Tap](mod_tap.md)
* [Macros](feature_macros.md)
* [Mouse Keys](feature_mouse_keys.md)
Expand Down
7 changes: 7 additions & 0 deletions docs/config_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ If you define these options you will enable the associated feature, which may in
* sets the timer for leader key chords to run on each key press rather than overall
* `#define LEADER_KEY_STRICT_KEY_PROCESSING`
* Disables keycode filtering for Mod-Tap and Layer-Tap keycodes. Eg, if you enable this, you would need to specify `MT(MOD_CTL, KC_A)` if you want to use `KC_A`.
* `#define COMPOSE_ONBOARD_ABORT KC_COMPOSE_ONBOARD`
* which key to use to abort the current composing sequence (defaults to `KC_COMPOSE_ONBOARD`)
* `#define COMPOSE_ONBOARD_LEN 5`
* how many keys should be recorded at maximum in the composing sequence (defaults to `5`)
* should be equal to the largest configured input sequence in the compose dictionary
* `#define ONESHOT_TIMEOUT 300`
* how long before oneshot times out
* `#define ONESHOT_TAP_TOGGLE 2`
Expand Down Expand Up @@ -361,6 +366,8 @@ Use these to enable or disable building certain features. The more you have enab
* Enable keyboard underlight functionality
* `LEADER_ENABLE`
* Enable leader key chording
* `COMPOSE_ONBOARD_ENABLE`
* Enable composing onboard directly on the keyboard
* `MIDI_ENABLE`
* MIDI controls
* `UNICODE_ENABLE`
Expand Down
1 change: 1 addition & 0 deletions docs/de/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
* [Key Lock](de/feature_key_lock.md)
* [Layouts](de/feature_layouts.md)
* [Leader Key](de/feature_leader_key.md)
* [Compose Onboard](de/feature_compose_onboard.md)
* [LED Matrix](de/feature_led_matrix.md)
* [Macros](de/feature_macros.md)
* [Mouse Keys](de/feature_mouse_keys.md)
Expand Down
1 change: 1 addition & 0 deletions docs/es/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
* [Key Lock](es/feature_key_lock.md)
* [Layouts](es/feature_layouts.md)
* [Tecla Leader](es/feature_leader_key.md)
* [Compose Onboard](es/feature_compose_onboard.md)
* [Matriz LED](es/feature_led_matrix.md)
* [Macros](es/feature_macros.md)
* [Teclas del ratón](es/feature_mouse_keys.md)
Expand Down
170 changes: 170 additions & 0 deletions docs/feature_compose_onboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Compose Onboard

You may already know the concept of composing from linux, sometimes also called multi key. Composing allows to map a sequence of keystrokes to press different keys, insert a character, or perform any preconfigured action. For example this is often used to write foreign or unicode characters. This features allows you to define such mappings directly on the keyboard. For example, you can create a mapping to write the character `→` by tapping `-` followed by `>` after pressing the compose key.

This is similar to how the [Leader Key](feature_leader_key.md) works. However, while the Leader Key is time-based, compose doesn't have any time constraints.

To use this feature, set one of your keys to `KC_COMPOSE_ONBOARD`, which you can then use to start a compose sequence. Also don't forget to enable the compose onboard feature as described in "Adding Compose Onboard Support in the `rules.mk`".

Imagine the example mapping from above (`-` `>` → `→`). After pressing `KC_COMPOSE_ONBOARD` any key press will be captured (i.e. not written on the computer). We can now type `-` and `>`, which will result in the character `→` being sent to the computer instead. Composing stops by finishing a sequence (e.g. `-` `>`), typing a key for which no further mapping matches (`f`, or `-` `a`), or by pressing the abort key (which defaults to the compose key itself). In those cases no output will be generated.


```c
COMPOSE_ONBOARD_DICTIONARY(
// quantum keyboard
COMPOSE_ONBOARD_MAPPING(
COMPOSE_ONBOARD_INPUT(KC_Q, KC_M, KC_K),
COMPOSE_ONBOARD_ACTION(
KC_Q, KC_U, KC_A, KC_N, KC_T, KC_U, KC_M,
KC_SPC, KC_K, KC_E, KC_Y, KC_B, KC_O, KC_A, KC_R, KC_D
)
)
// →
COMPOSE_ONBOARD_MAPPING(
COMPOSE_ONBOARD_INPUT(KC_MINS, KC_GT),
{
unicode_input_start();
register_hex(0x2192);
unicode_input_finish();
}
)
// shrug
COMPOSE_ONBOARD_MAPPING(
COMPOSE_ONBOARD_INPUT(KC_S, KC_H, KC_R, KC_U, KC_G),
{
unicode_input_start();
register_hex(0xaf);
register_hex(0x5c);
register_hex(0x5f);
register_hex(0x28);
register_hex(0x30c4);
register_hex(0x29);
register_hex(0x5f);
register_hex(0x2f);
register_hex(0xaf);
unicode_input_finish();
}
)
)
```

* `COMPOSE_ONBOARD_DICTIONARY` starts the list of all mappings
* `COMPOSE_ONBOARD_MAPPING` defines a mapping from an input key sequence to an action
* the first argument is the input key sequence
* the second argument is the action
* `COMPOSE_ONBOARD_INPUT` is used to define the key sequence needed to be typed to trigger the action
* `COMPOSE_ONBOARD_ACTION` can be used to define a key sequence to be typed if the input sequence has been typed

The action can also be a code block containing any C code. This can be used like in the above example to write a unicode character, or to run any arbitrary code similar to a predefined macro.

## Adding Compose Onboard Support in the `rules.mk`

To add support for Compose Onboard you simply need to add a single line to your keymap's `rules.mk`:

```make
COMPOSE_ONBOARD_ENABLE = yes
```

## Configuring the Abort Key

The abort key can be used while in the middle of a sequence to abort composing. It defaults to the compose key itself. If you press the compose key to start a compose sequence, but want to abort halfway through, you can press the compose key (i.e. the configured abort key). (Or you could just continue with a key which doesn't have any mapping.)

```c
#define COMPOSE_ONBOARD_ABORT KC_COMPOSE_ONBOARD
```

## Configuring the Maximum Input Length

While performing a compose sequence, the pressed keys are recorded. The maximum number of keys recorded should match the longest input of any compose mapping (but can be larger). By default, the number of recorded keys is configured to be `5`. However, if you want to have an input sequence longer than that, you need to adjust that number.

```c
#define COMPOSE_ONBOARD_LEN 5
```

Notice that compilation will fail with "Number of keys in Compose Onboard input keystroke is too long. Consider increasing COMPOSE_ONBOARD_LEN" if you define an input which is longer than the configured `COMPOSE_ONBOARD_LEN`.

## Customization - Hooks

There are some functions which you can define, which will be called during composing.

* `void compose_onboard_start(void)` is called when the compose key is pressed and can for example be used to light up an LED to indicate that you are within a compose sequence.
* `void compose_onboard_end(void)` is called when composing is done, either by having been aborted or finished successfully. It can for example be used to turn off the composing LED again.

## Customization - Custom Dictionary

The macros used above to define the dictionary define a function, which is called for the mapping procedure. For fine-grained control, you can manually define that function instead of using the macros.

The mapping function has the signature `bool compose_onboard_mapping(uint16_t* sequence, uint8_t len)`. That function is called whenever a new key is pressed, after it has been added to the sequence array.

* `uint16_t* sequence`: a pointer to an array of recorded keys like `KC_MINS`
* `uint8_t len`: current length of that array


To get a feeling for that function, this is an example of the macros and their resulting function definition.

```c
COMPOSE_ONBOARD_DICTIONARY(
// quantum keyboard
COMPOSE_ONBOARD_MAPPING(
COMPOSE_ONBOARD_INPUT(KC_Q, KC_M, KC_K),
COMPOSE_ONBOARD_ACTION(
KC_Q, KC_U, KC_A, KC_N, KC_T, KC_U, KC_M,
KC_SPC, KC_K, KC_E, KC_Y, KC_B, KC_O, KC_A, KC_R, KC_D
)
)
// →
COMPOSE_ONBOARD_MAPPING(
COMPOSE_ONBOARD_INPUT(KC_MINS, KC_GT),
{
unicode_input_start();
register_hex(0x2192);
unicode_input_finish();
}
)
)
```

The above macros get converted to the following code.

```c
bool compose_onboard_mapping(uint16_t* sequence, uint8_t sequence_len) {
bool partial_match = false;
{
uint16_t input[] = {KC_Q, KC_M, KC_K};
_Static_assert((sizeof(input) / sizeof(input[0])) <= COMPOSE_ONBOARD_LEN, "Number of keys in Compose Onboard input keystroke is too long. Consider increasing COMPOSE_ONBOARD_LEN");
uint8_t input_len = (sizeof(input) / sizeof(input[0]));
int res = compose_onboard_compare_input(input, input_len, sequence, sequence_len);
if (res == -1) {
uint16_t actions[] = {
KC_Q, KC_U, KC_A, KC_N, KC_T, KC_U, KC_M,
KC_SPC, KC_K, KC_E, KC_Y, KC_B, KC_O, KC_A, KC_R, KC_D
};
for (int i = 0; i < (sizeof(actions) / sizeof(actions[0])); i++) {
register_code16(actions[i]);
unregister_code16(actions[i]);
}
return false;
}
partial_match |= res;
}
{
uint16_t input[] = {KC_MINS, KC_GT};
_Static_assert((sizeof(input) / sizeof(input[0])) <= 5, "Number of keys in Compose Onboard input keystroke is too long. Consider increasing COMPOSE_ONBOARD_LEN");
uint8_t input_len = (sizeof(input) / sizeof(input[0]));
int res = compose_onboard_compare_input(input, input_len, sequence, sequence_len);
if (res == -1) {
{
unicode_input_start();
register_hex(0x2192);
unicode_input_finish();
}
return false;
}
partial_match |= res;
}
return partial_match;
}
```

That code uses the helper function `int compose_onboard_compare_input(uint16_t* input, uint8_t input_len, uint16_t* seq, uint8_t seq_len)`. That function compares the compose mapping input to the sequence so far. Its return values are counter-intuitive for binary size reasons. It returns -1 on a full match (the input matched completely and the action can be performed), 0 on no match (it's never possible in the current sequence to get this input), and 1 on a partial match (the recorded sequence so far matches the beginning of the mapping input, but the mapping input still has some keys left).

1 change: 1 addition & 0 deletions docs/fr-fr/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
* [Touche à verrou / Lock-key](fr-fr/feature_key_lock.md)
* [Dispositions / layouts](fr-fr/feature_layouts.md)
* [Touche leader](fr-fr/feature_leader_key.md)
* [Compose Onboard](fr-fr/feature_compose_onboard.md)
* [Matrice LED](fr-fr/feature_led_matrix.md)
* [Macros](fr-fr/feature_macros.md)
* [Boutons de souris](fr-fr/feature_mouse_keys.md)
Expand Down
1 change: 1 addition & 0 deletions docs/he-il/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
* [Key Lock](he-il/feature_key_lock.md)
* [Layouts](he-il/feature_layouts.md)
* [Leader Key](he-il/feature_leader_key.md)
* [Compose Onboard](he-il/feature_compose_onboard.md)
* [LED Matrix](he-il/feature_led_matrix.md)
* [Macros](he-il/feature_macros.md)
* [Mouse Keys](he-il/feature_mouse_keys.md)
Expand Down
1 change: 1 addition & 0 deletions docs/ja/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
* [動的マクロ](ja/feature_dynamic_macros.md)
* [グレイブ エスケープ](ja/feature_grave_esc.md)
* [リーダーキー](ja/feature_leader_key.md)
* [Compose Onboard](ja/feature_compose_onboard.md)
* [モッドタップ](ja/mod_tap.md)
* [マクロ](ja/feature_macros.md)
* [マウスキー](ja/feature_mouse_keys.md)
Expand Down
8 changes: 8 additions & 0 deletions docs/keycodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,14 @@ See also: [Leader Key](feature_leader_key.md)
|---------|------------------------|
|`KC_LEAD`|Begins a leader sequence|

## Compose Onboard :id=compose-onboard

See also: [Compose Onboard](feature_compose_onboard.md)

|Key |Aliases |Description |
|--------------------|-----------|-----------------------------------------|
|`KC_COMPOSE_ONBOARD`|`KC_COMPOB`|Begins Composing directly on the keyboard|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
|`KC_COMPOSE_ONBOARD`|`KC_COMPOB`|Begins Composing directly on the keyboard|
|`KC_COMPOSE_ONBOARD`|`KC_CMOB`|Begins Composing directly on the keyboard|

We like these keycode aliases to be seven characters or less, when possible.

Also, this entire section should be moved up in the document to be between the Bluetooth and Dynamic Macros sections, as this page is organized alphabetically (with the exception of the Basic and Quantum sections).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO Quantum keycodes should not have the KC_ prefix either, as that is reserved for the basic keycode set.


## Mouse Keys :id=mouse-keys

See also: [Mouse Keys](feature_mouse_keys.md)
Expand Down
1 change: 1 addition & 0 deletions docs/pt-br/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
* [Key Lock](pt-br/feature_key_lock.md)
* [Layouts](pt-br/feature_layouts.md)
* [Leader Key](pt-br/feature_leader_key.md)
* [Compose Onboard](pt-br/feature_compose_onboard.md)
* [LED Matrix](pt-br/feature_led_matrix.md)
* [Macros](pt-br/feature_macros.md)
* [Mouse Keys](pt-br/feature_mouse_keys.md)
Expand Down
5 changes: 5 additions & 0 deletions docs/reference_glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ A feature that allows you to tap the leader key followed by a sequence of 1, 2,

* [Leader Key Documentation](feature_leader_key.md)

## Compose Onboard
Allows composing directly on the keyboard. After pressing the compose onboard key, the following keystrokes trigger a preconfigured action like pressing other keys, inserting a character, or any other quantum features.

* [Compose Onboard Documentation](feature_compose_onboard.md)

Comment on lines +81 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that this needs to be added to the glossary, but if its decided that it should be so, this section should move up between Compile and Dvorak, as this page is organized alphabetically.

## LED
Light Emitting Diode, the most common device used for indicators on a keyboard.

Expand Down
1 change: 1 addition & 0 deletions docs/ru-ru/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
* [Key Lock](ru-ru/feature_key_lock.md)
* [Layouts](ru-ru/feature_layouts.md)
* [Leader Key](ru-ru/feature_leader_key.md)
* [Compose Onboard](ru-ru/feature_compose_onboard.md)
* [LED Matrix](ru-ru/feature_led_matrix.md)
* [Macros](ru-ru/feature_macros.md)
* [Mouse Keys](ru-ru/feature_mouse_keys.md)
Expand Down
1 change: 1 addition & 0 deletions docs/understanding_qmk.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ The `process_record()` function itself is deceptively simple, but hidden within
* [`bool process_unicodemap(uint16_t keycode, keyrecord_t *record)`](https://github.com/qmk/qmk_firmware/blob/e1203a222bb12ab9733916164a000ef3ac48da93/quantum/process_keycode/process_unicodemap.c#L46)
* [`bool process_ucis(uint16_t keycode, keyrecord_t *record)`](https://github.com/qmk/qmk_firmware/blob/e1203a222bb12ab9733916164a000ef3ac48da93/quantum/process_keycode/process_ucis.c#L95)
* [`bool process_leader(uint16_t keycode, keyrecord_t *record)`](https://github.com/qmk/qmk_firmware/blob/e1203a222bb12ab9733916164a000ef3ac48da93/quantum/process_keycode/process_leader.c#L51)
* [`bool process_compose_onboard(uint16_t keycode, keyrecord_t *record)`](https://github.com/qmk/qmk_firmware/blob/a0f2e292946c9a5535c70d9f2dfebb3055f1652a/quantum/process_keycode/process_compose_onboard.c#L24)
* [`bool process_combo(uint16_t keycode, keyrecord_t *record)`](https://github.com/qmk/qmk_firmware/blob/e1203a222bb12ab9733916164a000ef3ac48da93/quantum/process_keycode/process_combo.c#L115)
* [`bool process_printer(uint16_t keycode, keyrecord_t *record)`](https://github.com/qmk/qmk_firmware/blob/e1203a222bb12ab9733916164a000ef3ac48da93/quantum/process_keycode/process_printer.c#L77)
* [`bool process_auto_shift(uint16_t keycode, keyrecord_t *record)`](https://github.com/qmk/qmk_firmware/blob/e1203a222bb12ab9733916164a000ef3ac48da93/quantum/process_keycode/process_auto_shift.c#L94)
Expand Down
1 change: 1 addition & 0 deletions docs/zh-cn/_summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
* [自锁键](zh-cn/feature_key_lock.md)
* [布局](zh-cn/feature_layouts.md)
* [前导键](zh-cn/feature_leader_key.md)
* [Compose Onboard](zh-cn/feature_compose_onboard.md)
* [LED阵列](zh-cn/feature_led_matrix.md)
* [宏指令](zh-cn/feature_macros.md)
* [鼠标键](zh-cn/feature_mouse_keys.md)
Expand Down
84 changes: 84 additions & 0 deletions quantum/process_keycode/process_compose_onboard.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#include "process_compose_onboard.h"

__attribute__((weak)) bool compose_onboard_mapping(uint16_t* sequence, uint8_t len) {
return false;
}
__attribute__((weak)) void compose_onboard_start(void) {}
__attribute__((weak)) void compose_onboard_end(void) {}

static bool composing_onboard = false;
static uint16_t compose_onboard_sequence[COMPOSE_ONBOARD_LEN] = {0, 0, 0, 0, 0};
static uint8_t compose_onboard_len = 0;

void qk_compose_onboard_start(void) {
composing_onboard = true;
memset(compose_onboard_sequence, 0, COMPOSE_ONBOARD_LEN * sizeof(uint16_t));
compose_onboard_len = 0;
compose_onboard_start();
}
void qk_compose_onboard_end(void) {
composing_onboard = false;
compose_onboard_end();
}

bool process_compose_onboard(uint16_t keycode, keyrecord_t* record) {
if (!composing_onboard && keycode == KC_COMPOSE_ONBOARD && record->event.pressed) {
qk_compose_onboard_start();
return false;
}
if (!composing_onboard || !record->event.pressed) {
return true;
}
// ignore MOD-keys and layer modifiers
if (
// ignore (L/R)-CTRL, ALT, GUI, Shift
IS_MOD(keycode)
// ignore any layer modifiers
|| (QK_LAYER_TAP <= keycode && keycode <= QK_ONE_SHOT_MOD_MAX)
|| (QK_LAYER_TAP_TOGGLE <= keycode && keycode <= QK_LAYER_MOD_MAX)
|| (QK_MOD_TAP <= keycode && keycode <= QK_MOD_TAP_MAX)
) {
return true;
}

// we are composing and a non-layer-modifying key was pressed

if (keycode == COMPOSE_ONBOARD_ABORT) {
qk_compose_onboard_end();
return false;
}

// this shouldn't happen, but check it for safety reasons
if (compose_onboard_len >= COMPOSE_ONBOARD_LEN) {
qk_compose_onboard_end();
}

compose_onboard_sequence[compose_onboard_len] = keycode;
compose_onboard_len += 1;
if (!compose_onboard_mapping(compose_onboard_sequence, compose_onboard_len)) {
qk_compose_onboard_end();
}

return false;
}

size_t compose_onboard_memcmp_index(uint16_t* seq, uint16_t* input, size_t len) {
size_t i = 0;
for (; i < len; i++) {
if (seq[i] != input[i]) {
return i;
}
}
return i;
}
int compose_onboard_compare_input(uint16_t* input, uint8_t input_len, uint16_t* seq, uint8_t seq_len) {
size_t test_len = min(input_len, seq_len);
size_t match_index = compose_onboard_memcmp_index(seq, input, test_len);
if (seq_len == input_len && match_index == test_len) {
return -1;
}
if (match_index == seq_len) {
return 1;
}
return 0;
}
Loading