diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index a647e88363f..34e66391a1e 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -44,6 +44,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_caps_word.c) target_sources(app PRIVATE src/behaviors/behavior_key_repeat.c) target_sources(app PRIVATE src/behaviors/behavior_macro.c) + target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_LEADER_KEY app PRIVATE src/behaviors/behavior_leader_key.c) target_sources(app PRIVATE src/behaviors/behavior_momentary_layer.c) target_sources(app PRIVATE src/behaviors/behavior_mod_morph.c) target_sources(app PRIVATE src/behaviors/behavior_outputs.c) @@ -55,6 +56,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_VAR app PRIVATE src/behaviors/behavior_sensor_rotate_var.c) target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_SENSOR_ROTATE_COMMON app PRIVATE src/behaviors/behavior_sensor_rotate_common.c) target_sources(app PRIVATE src/combo.c) + target_sources_ifdef(CONFIG_ZMK_LEADER app PRIVATE src/leader.c) target_sources(app PRIVATE src/behaviors/behavior_tap_dance.c) target_sources(app PRIVATE src/behavior_queue.c) target_sources(app PRIVATE src/conditional_layer.c) diff --git a/app/Kconfig b/app/Kconfig index d1b6682f507..267dd4eb13f 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -349,6 +349,19 @@ config ZMK_COMBO_MAX_KEYS_PER_COMBO #Combo options endmenu +menu "Leader Options" + +config ZMK_LEADER_MAX_KEYS_PER_SEQUENCE + int "Maximum number of key presses in a leader sequence" + default 4 + +config ZMK_LEADER_MAX_SEQUENCES_PER_KEY + int "Maximum number of leader sequences that a key can belong to" + default 5 + +#Leader options +endmenu + menu "Behavior Options" config ZMK_BEHAVIORS_QUEUE_SIZE @@ -365,6 +378,18 @@ config ZMK_MACRO_DEFAULT_TAP_MS int "Default time to wait (in milliseconds) between the press and release events of a tapped behavior in macros" default 30 +DT_COMPAT_ZMK_LEADER := zmk,leader-sequences + +config ZMK_LEADER + bool + default $(dt_compat_enabled,$(DT_COMPAT_ZMK_LEADER)) + +DT_COMPAT_ZMK_BEHAVIOR_LEADER_KEY := zmk,behavior-leader-key + +config ZMK_BEHAVIOR_LEADER_KEY + bool + default $(dt_compat_enabled,$(DT_COMPAT_ZMK_BEHAVIOR_LEADER_KEY)) + endmenu menu "Advanced" diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index b3502cbbc13..c133959e313 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -18,4 +18,5 @@ #include #include #include -#include \ No newline at end of file +#include +#include diff --git a/app/dts/behaviors/leader_key.dtsi b/app/dts/behaviors/leader_key.dtsi new file mode 100644 index 00000000000..8cf420ca7d1 --- /dev/null +++ b/app/dts/behaviors/leader_key.dtsi @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + + / { + behaviors { + /omit-if-no-ref/ leader: leader_key { + compatible = "zmk,behavior-leader-key"; + label = "LEADER"; + #binding-cells = <0>; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-leader-key.yaml b/app/dts/bindings/behaviors/zmk,behavior-leader-key.yaml new file mode 100644 index 00000000000..22cc4e78a9a --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-leader-key.yaml @@ -0,0 +1,15 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Leader key behavior + +compatible: "zmk,behavior-leader-key" + +include: zero_param.yaml + +properties: + timerless: + type: boolean + timeout-ms: + type: int + default: 200 diff --git a/app/dts/bindings/zmk,leader-sequences.yaml b/app/dts/bindings/zmk,leader-sequences.yaml new file mode 100644 index 00000000000..f26a8b90b61 --- /dev/null +++ b/app/dts/bindings/zmk,leader-sequences.yaml @@ -0,0 +1,22 @@ +# Copyright (c) 2022, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Leader sequence container + +compatible: "zmk,leader-sequences" + +child-binding: + description: "A leader sequence" + + properties: + bindings: + type: phandle-array + required: true + key-positions: + type: array + required: true + layers: + type: array + default: [-1] + immediate-trigger: + type: boolean diff --git a/app/include/zmk/leader.h b/app/include/zmk/leader.h new file mode 100644 index 00000000000..6e28daa8b96 --- /dev/null +++ b/app/include/zmk/leader.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +void zmk_leader_activate(int32_t timeout, bool timeout_on_activation, uint32_t position); +void zmk_leader_deactivate(); +bool zmk_leader_get_status(); diff --git a/app/src/behaviors/behavior_leader_key.c b/app/src/behaviors/behavior_leader_key.c new file mode 100644 index 00000000000..b8624947d5e --- /dev/null +++ b/app/src/behaviors/behavior_leader_key.c @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_leader_key + +#include +#include +#include + +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct behavior_leader_key_config { + int32_t timeout_ms; + bool timerless; +}; + +static int behavior_leader_key_init(const struct device *dev) { return 0; } + +static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = device_get_binding(binding->behavior_dev); + const struct behavior_leader_key_config *cfg = dev->config; + + zmk_leader_activate(cfg->timeout_ms, cfg->timerless, event.position); + return ZMK_BEHAVIOR_OPAQUE; +} + +static int on_keymap_binding_released(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + return 0; +} + +static const struct behavior_driver_api behavior_leader_key_driver_api = { + .binding_pressed = on_keymap_binding_pressed, + .binding_released = on_keymap_binding_released, +}; + +#define LEAD_INST(n) \ + static struct behavior_leader_key_config behavior_leader_key_config_##n = { \ + .timerless = DT_INST_PROP(n, timerless), .timeout_ms = DT_INST_PROP(n, timeout_ms)}; \ + DEVICE_DT_INST_DEFINE(n, behavior_leader_key_init, NULL, NULL, \ + &behavior_leader_key_config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_leader_key_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(LEAD_INST) diff --git a/app/src/leader.c b/app/src/leader.c new file mode 100644 index 00000000000..9f90ebf170c --- /dev/null +++ b/app/src/leader.c @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_leader_sequences + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +bool leader_status; +int32_t press_count; +int32_t release_count; +int32_t timeout_ms; +int32_t active_leader_position; +int8_t layer; +bool first_release; +struct k_work_delayable release_timer; +int64_t release_at; +bool timer_started; +bool timer_cancelled; +bool timerless; + +struct leader_seq_cfg { + int32_t key_positions[CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE]; + int32_t key_position_len; + + bool immediate_trigger; + bool is_pressed; + // the virtual key position is a key position outside the range used by the keyboard. + // it is necessary so hold-taps can uniquely identify a behavior. + int32_t virtual_key_position; + struct zmk_behavior_binding behavior; + int32_t layers_len; + int8_t layers[]; +}; + +// leader_pressed_keys is filled with an event when a key is pressed. +// The keys are removed from this array when they are released. +// Once this array is empty, the behavior is released. +const struct zmk_position_state_changed + *leader_pressed_keys[CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE] = {NULL}; + +uint32_t current_sequence[CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE] = {-1}; +// the set of candidate leader based on the currently leader_pressed_keys +int num_candidates; +struct leader_seq_cfg *sequence_candidates[CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY]; +int num_comp_candidates; +struct leader_seq_cfg *completed_sequence_candidates[CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY]; +// a lookup dict that maps a key position to all sequences on that position +struct leader_seq_cfg *sequence_lookup[ZMK_KEYMAP_LEN][CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY] = { + NULL}; + +// Store the leader key pointer in the leader array, one pointer for each key position +// The leader are sorted shortest-first, then by virtual-key-position. +static int intitialiaze_leader_sequences(struct leader_seq_cfg *seq) { + for (int i = 0; i < seq->key_position_len; i++) { + int32_t position = seq->key_positions[i]; + if (position >= ZMK_KEYMAP_LEN) { + LOG_ERR("Unable to initialize leader, key position %d does not exist", position); + return -EINVAL; + } + + struct leader_seq_cfg *new_seq = seq; + bool set = false; + for (int j = 0; j < CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY; j++) { + struct leader_seq_cfg *sequence_at_j = sequence_lookup[position][j]; + if (sequence_at_j == NULL) { + sequence_lookup[position][j] = new_seq; + set = true; + break; + } + if (sequence_at_j->key_position_len < new_seq->key_position_len || + (sequence_at_j->key_position_len == new_seq->key_position_len && + sequence_at_j->virtual_key_position < new_seq->virtual_key_position)) { + continue; + } + // put new_seq in this spot, move all other leader up. + sequence_lookup[position][j] = new_seq; + new_seq = sequence_at_j; + } + if (!set) { + LOG_ERR( + "Too many leader for key position %d, CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY %d.", + position, CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY); + return -ENOMEM; + } + } + return 0; +} + +static bool sequence_active_on_layer(struct leader_seq_cfg *sequence) { + if (sequence->layers[0] == -1) { + // -1 in the first layer position is global layer scope + return true; + } + for (int j = 0; j < sequence->layers_len; j++) { + if (sequence->layers[j] == layer) { + return true; + } + } + return false; +} + +static bool has_current_sequence(struct leader_seq_cfg *sequence, int count) { + for (int i = 0; i < count; i++) { + if (sequence->key_positions[i] != current_sequence[i]) { + return false; + } + } + return true; +} + +static bool is_in_current_sequence(int32_t position) { + for (int i = 0; i < CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE; i++) { + if (position == current_sequence[i]) { + return true; + } + } + return false; +} + +static bool is_duplicate(struct leader_seq_cfg *seq) { + for (int i = 0; i < CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE; i++) { + if (sequence_candidates[i] == seq) { + return true; + } + } + return false; +} + +static bool release_key_in_sequence(int32_t position) { + for (int i = 0; i < release_count; i++) { + if (leader_pressed_keys[i] && position == leader_pressed_keys[i]->position) { + leader_pressed_keys[i] = NULL; + return true; + } + } + return false; +} + +static bool all_keys_released() { + for (int i = 0; i < press_count; i++) { + if (NULL != leader_pressed_keys[i]) { + return false; + } + } + return true; +} + +static void clear_candidates() { + for (int i = 0; i < CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY; i++) { + sequence_candidates[i] = NULL; + completed_sequence_candidates[i] = NULL; + } +} + +static void leader_find_candidates(int32_t position, int count) { + clear_candidates(); + num_candidates = 0; + num_comp_candidates = 0; + for (int i = 0; i < CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY; i++) { + struct leader_seq_cfg *sequence = sequence_lookup[position][i]; + if (sequence == NULL) { + continue; + } + if (sequence_active_on_layer(sequence) && sequence->key_positions[count] == position && + has_current_sequence(sequence, count) && !is_duplicate(sequence)) { + sequence_candidates[num_candidates] = sequence; + num_candidates++; + if (sequence->key_position_len == count + 1) { + completed_sequence_candidates[num_comp_candidates] = sequence; + num_comp_candidates++; + } + } + } +} + +const struct zmk_listener zmk_listener_leader; + +static inline int press_leader_behavior(struct leader_seq_cfg *sequence, int32_t timestamp) { + struct zmk_behavior_binding_event event = { + .position = sequence->virtual_key_position, + .timestamp = timestamp, + }; + + sequence->is_pressed = true; + return behavior_keymap_binding_pressed(&sequence->behavior, event); +} + +static inline int release_leader_behavior(struct leader_seq_cfg *sequence, int32_t timestamp) { + struct zmk_behavior_binding_event event = { + .position = sequence->virtual_key_position, + .timestamp = timestamp, + }; + + sequence->is_pressed = false; + return behavior_keymap_binding_released(&sequence->behavior, event); +} + +static int stop_timer() { + int timer_cancel_result = k_work_cancel_delayable(&release_timer); + if (timer_cancel_result == -EINPROGRESS) { + // too late to cancel, we'll let the timer handler clear up. + timer_cancelled = true; + } + return timer_cancel_result; +} + +static void reset_timer(int32_t timestamp) { + release_at = timestamp + timeout_ms; + int32_t ms_left = release_at - k_uptime_get(); + if (ms_left > 0) { + k_work_schedule(&release_timer, K_MSEC(ms_left)); + LOG_DBG("Successfully reset leader timer"); + } +} + +void zmk_leader_activate(int32_t timeout, bool _timerless, uint32_t position) { + LOG_DBG("leader key activated"); + leader_status = true; + press_count = 0; + release_count = 0; + timeout_ms = timeout; + active_leader_position = position; + layer = zmk_keymap_highest_layer_active(); + first_release = false; + timerless = _timerless; + if (!timerless) { + reset_timer(k_uptime_get()); + } + for (int i = 0; i < CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE; i++) { + leader_pressed_keys[i] = NULL; + } +}; + +void zmk_leader_deactivate() { + LOG_DBG("leader key deactivated"); + leader_status = false; + clear_candidates(); +}; + +void behavior_leader_key_timer_handler(struct k_work *item) { + if (!leader_status) { + return; + } + if (timer_cancelled) { + return; + } + LOG_DBG("Leader deactivated due to timeout"); + for (int i = 0; i < num_comp_candidates; i++) { + if (!completed_sequence_candidates[i]->is_pressed) { + press_leader_behavior(completed_sequence_candidates[i], k_uptime_get()); + release_leader_behavior(completed_sequence_candidates[i], k_uptime_get()); + } + } + zmk_leader_deactivate(); +} + +static int position_state_changed_listener(const zmk_event_t *ev) { + struct zmk_position_state_changed *data = as_zmk_position_state_changed(ev); + if (data == NULL) { + return 0; + } + + if (!leader_status && !data->state && !all_keys_released()) { + if (release_key_in_sequence(data->position)) { + return ZMK_EV_EVENT_HANDLED; + } + return 0; + } + + if (leader_status) { + if (data->state) { // keydown + leader_find_candidates(data->position, press_count); + LOG_DBG("leader cands: %d comp: %d", num_candidates, num_comp_candidates); + stop_timer(); + current_sequence[press_count] = data->position; + leader_pressed_keys[press_count] = data; + press_count++; + for (int i = 0; i < num_comp_candidates; i++) { + struct leader_seq_cfg *seq = completed_sequence_candidates[i]; + if (seq->immediate_trigger || (num_candidates == 1 && num_comp_candidates == 1)) { + press_leader_behavior(seq, data->timestamp); + } + } + } else { // keyup + if (data->position == active_leader_position && !first_release) { + first_release = true; + return 0; + } + if (!is_in_current_sequence(data->position)) { + return 0; + } + if (num_candidates == 0) { + zmk_leader_deactivate(); + return ZMK_EV_EVENT_HANDLED; + } + + release_count++; + release_key_in_sequence(data->position); + + for (int i = 0; i < num_comp_candidates; i++) { + struct leader_seq_cfg *seq = completed_sequence_candidates[i]; + if (seq->is_pressed && all_keys_released()) { + release_leader_behavior(seq, data->timestamp); + num_comp_candidates--; + } + if (num_candidates == 1 && num_comp_candidates == 0) { + zmk_leader_deactivate(); + } + } + if (!timerless) { + reset_timer(data->timestamp); + } + } + return ZMK_EV_EVENT_HANDLED; + } + + return 0; +} + +ZMK_LISTENER(leader, position_state_changed_listener); +ZMK_SUBSCRIPTION(leader, zmk_position_state_changed); + +#define LEADER_INST(n) \ + static struct leader_seq_cfg sequence_config_##n = { \ + .virtual_key_position = ZMK_KEYMAP_LEN + __COUNTER__, \ + .immediate_trigger = DT_PROP(n, immediate_trigger), \ + .is_pressed = false, \ + .key_positions = DT_PROP(n, key_positions), \ + .key_position_len = DT_PROP_LEN(n, key_positions), \ + .behavior = ZMK_KEYMAP_EXTRACT_BINDING(0, n), \ + .layers = DT_PROP(n, layers), \ + .layers_len = DT_PROP_LEN(n, layers), \ + }; + +DT_INST_FOREACH_CHILD(0, LEADER_INST) + +#define INTITIALIAZE_LEADER_SEQUENCES(n) intitialiaze_leader_sequences(&sequence_config_##n); + +static int leader_init() { + k_work_init_delayable(&release_timer, behavior_leader_key_timer_handler); + DT_INST_FOREACH_CHILD(0, INTITIALIAZE_LEADER_SEQUENCES); + return 0; +} + +SYS_INIT(leader_init, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT); diff --git a/app/tests/leader/basic/events.patterns b/app/tests/leader/basic/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/basic/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/basic/keycode_events.snapshot b/app/tests/leader/basic/keycode_events.snapshot new file mode 100644 index 00000000000..9ab32b1a876 --- /dev/null +++ b/app/tests/leader/basic/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/basic/native_posix_64.keymap b/app/tests/leader/basic/native_posix_64.keymap new file mode 100644 index 00000000000..f7db609d123 --- /dev/null +++ b/app/tests/leader/basic/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,2000) + >; +}; diff --git a/app/tests/leader/behavior_keymap.dtsi b/app/tests/leader/behavior_keymap.dtsi new file mode 100644 index 00000000000..dbcbf55aa01 --- /dev/null +++ b/app/tests/leader/behavior_keymap.dtsi @@ -0,0 +1,41 @@ +#include +#include +#include + +&leader { + timeout-ms = <200>; +}; + +/ { + leader-sequences { + compatible = "zmk,leader-sequences"; + + leader_seq_one { + key-positions = <0>; + bindings = <&kp A>; + }; + + leader_seq_two { + key-positions = <1>; + bindings = <&kp B>; + }; + + leader_seq_three { + key-positions = <3>; + bindings = <&kp N1>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp N1 &kp N2 + &kp N3 &leader + >; + }; + }; +}; diff --git a/app/tests/leader/behavior_keymap_layers.dtsi b/app/tests/leader/behavior_keymap_layers.dtsi new file mode 100644 index 00000000000..931c8303c19 --- /dev/null +++ b/app/tests/leader/behavior_keymap_layers.dtsi @@ -0,0 +1,33 @@ +#include +#include +#include + +/ { + leader-sequences { + compatible = "zmk,leader-sequences"; + + leader_seq_one { + key-positions = <0>; + bindings = <&kp A>; + layers = <2>; + }; + + leader_seq_two { + key-positions = <1>; + bindings = <&kp B>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp N1 &kp N2 + &kp N3 &leader + >; + }; + }; +}; diff --git a/app/tests/leader/behavior_keymap_overlap.dtsi b/app/tests/leader/behavior_keymap_overlap.dtsi new file mode 100644 index 00000000000..8f398085119 --- /dev/null +++ b/app/tests/leader/behavior_keymap_overlap.dtsi @@ -0,0 +1,41 @@ +#include +#include +#include + +&leader { + timeout-ms = <200>; +}; + +/ { + leader-sequences { + compatible = "zmk,leader-sequences"; + + leader_seq_one { + key-positions = <0>; + bindings = <&kp A>; + }; + + leader_seq_two { + key-positions = <1>; + bindings = <&kp B>; + }; + + leader_seq_three { + key-positions = <0 1>; + bindings = <&kp C>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp N1 &kp N2 + &kp N3 &leader + >; + }; + }; +}; diff --git a/app/tests/leader/behavior_keymap_overlap_immediate.dtsi b/app/tests/leader/behavior_keymap_overlap_immediate.dtsi new file mode 100644 index 00000000000..c05aeb993ee --- /dev/null +++ b/app/tests/leader/behavior_keymap_overlap_immediate.dtsi @@ -0,0 +1,42 @@ +#include +#include +#include + +&leader { + timeout-ms = <200>; +}; + +/ { + leader-sequences { + compatible = "zmk,leader-sequences"; + + leader_seq_one { + key-positions = <0>; + bindings = <&kp A>; + immediate-trigger; + }; + + leader_seq_two { + key-positions = <1>; + bindings = <&kp B>; + }; + + leader_seq_three { + key-positions = <0 1>; + bindings = <&kp C>; + }; + + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp N1 &kp N2 + &kp N3 &leader + >; + }; + }; +}; diff --git a/app/tests/leader/behavior_keymap_three.dtsi b/app/tests/leader/behavior_keymap_three.dtsi new file mode 100644 index 00000000000..f21ca2602a3 --- /dev/null +++ b/app/tests/leader/behavior_keymap_three.dtsi @@ -0,0 +1,36 @@ +#include +#include +#include + +&leader { + timeout-ms = <200>; + timerless; +}; + +/ { + leader-sequences { + compatible = "zmk,leader-sequences"; + + leader_seq_one { + key-positions = <0 1 2>; + bindings = <&kp A>; + }; + + leader_seq_two { + key-positions = <3 3>; + bindings = <&kp B>; + }; + }; + + keymap { + compatible = "zmk,keymap"; + label ="Default keymap"; + + default_layer { + bindings = < + &kp N1 &kp N2 + &kp N3 &leader + >; + }; + }; +}; diff --git a/app/tests/leader/double-tap/events.patterns b/app/tests/leader/double-tap/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/double-tap/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/double-tap/keycode_events.snapshot b/app/tests/leader/double-tap/keycode_events.snapshot new file mode 100644 index 00000000000..01529cacc29 --- /dev/null +++ b/app/tests/leader/double-tap/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x1E implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x1E implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/double-tap/native_posix_64.keymap b/app/tests/leader/double-tap/native_posix_64.keymap new file mode 100644 index 00000000000..d7081915b86 --- /dev/null +++ b/app/tests/leader/double-tap/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,2000) + >; +}; diff --git a/app/tests/leader/double-tap2/events.patterns b/app/tests/leader/double-tap2/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/double-tap2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/double-tap2/keycode_events.snapshot b/app/tests/leader/double-tap2/keycode_events.snapshot new file mode 100644 index 00000000000..486943c6d21 --- /dev/null +++ b/app/tests/leader/double-tap2/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/double-tap2/native_posix_64.keymap b/app/tests/leader/double-tap2/native_posix_64.keymap new file mode 100644 index 00000000000..cafaaf0713d --- /dev/null +++ b/app/tests/leader/double-tap2/native_posix_64.keymap @@ -0,0 +1,12 @@ +#include "../behavior_keymap_three.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,2000) + >; +}; diff --git a/app/tests/leader/none-layer/events.patterns b/app/tests/leader/none-layer/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/none-layer/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/none-layer/keycode_events.snapshot b/app/tests/leader/none-layer/keycode_events.snapshot new file mode 100644 index 00000000000..c476ce63ba4 --- /dev/null +++ b/app/tests/leader/none-layer/keycode_events.snapshot @@ -0,0 +1,2 @@ +leader: leader key activated +leader: leader key deactivated diff --git a/app/tests/leader/none-layer/native_posix_64.keymap b/app/tests/leader/none-layer/native_posix_64.keymap new file mode 100644 index 00000000000..1441719c853 --- /dev/null +++ b/app/tests/leader/none-layer/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap_layers.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,2000) + >; +}; diff --git a/app/tests/leader/none/events.patterns b/app/tests/leader/none/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/none/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/none/keycode_events.snapshot b/app/tests/leader/none/keycode_events.snapshot new file mode 100644 index 00000000000..c476ce63ba4 --- /dev/null +++ b/app/tests/leader/none/keycode_events.snapshot @@ -0,0 +1,2 @@ +leader: leader key activated +leader: leader key deactivated diff --git a/app/tests/leader/none/native_posix_64.keymap b/app/tests/leader/none/native_posix_64.keymap new file mode 100644 index 00000000000..d9a27375e52 --- /dev/null +++ b/app/tests/leader/none/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(1,0,10) + ZMK_MOCK_RELEASE(1,0,2000) + >; +}; diff --git a/app/tests/leader/overlapping-immediate/events.patterns b/app/tests/leader/overlapping-immediate/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/overlapping-immediate/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/overlapping-immediate/keycode_events.snapshot b/app/tests/leader/overlapping-immediate/keycode_events.snapshot new file mode 100644 index 00000000000..73f87e7c320 --- /dev/null +++ b/app/tests/leader/overlapping-immediate/keycode_events.snapshot @@ -0,0 +1,6 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/overlapping-immediate/native_posix_64.keymap b/app/tests/leader/overlapping-immediate/native_posix_64.keymap new file mode 100644 index 00000000000..1d657fce6b6 --- /dev/null +++ b/app/tests/leader/overlapping-immediate/native_posix_64.keymap @@ -0,0 +1,12 @@ +#include "../behavior_keymap_overlap_immediate.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,2000) + >; +}; diff --git a/app/tests/leader/overlapping-timeout/events.patterns b/app/tests/leader/overlapping-timeout/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/overlapping-timeout/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/overlapping-timeout/keycode_events.snapshot b/app/tests/leader/overlapping-timeout/keycode_events.snapshot new file mode 100644 index 00000000000..9ab32b1a876 --- /dev/null +++ b/app/tests/leader/overlapping-timeout/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/overlapping-timeout/native_posix_64.keymap b/app/tests/leader/overlapping-timeout/native_posix_64.keymap new file mode 100644 index 00000000000..ccb074a77f4 --- /dev/null +++ b/app/tests/leader/overlapping-timeout/native_posix_64.keymap @@ -0,0 +1,10 @@ +#include "../behavior_keymap_overlap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,2000) + >; +}; diff --git a/app/tests/leader/overlapping/events.patterns b/app/tests/leader/overlapping/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/overlapping/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/overlapping/keycode_events.snapshot b/app/tests/leader/overlapping/keycode_events.snapshot new file mode 100644 index 00000000000..f73a0180d59 --- /dev/null +++ b/app/tests/leader/overlapping/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/overlapping/native_posix_64.keymap b/app/tests/leader/overlapping/native_posix_64.keymap new file mode 100644 index 00000000000..080e563bdc0 --- /dev/null +++ b/app/tests/leader/overlapping/native_posix_64.keymap @@ -0,0 +1,12 @@ +#include "../behavior_keymap_overlap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,2000) + >; +}; diff --git a/app/tests/leader/roll-reverse/events.patterns b/app/tests/leader/roll-reverse/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/roll-reverse/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/roll-reverse/keycode_events.snapshot b/app/tests/leader/roll-reverse/keycode_events.snapshot new file mode 100644 index 00000000000..f73a0180d59 --- /dev/null +++ b/app/tests/leader/roll-reverse/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/roll-reverse/native_posix_64.keymap b/app/tests/leader/roll-reverse/native_posix_64.keymap new file mode 100644 index 00000000000..c926de482d0 --- /dev/null +++ b/app/tests/leader/roll-reverse/native_posix_64.keymap @@ -0,0 +1,12 @@ +#include "../behavior_keymap_overlap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,0,2000) + >; +}; diff --git a/app/tests/leader/roll-three-mix/events.patterns b/app/tests/leader/roll-three-mix/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/roll-three-mix/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/roll-three-mix/keycode_events.snapshot b/app/tests/leader/roll-three-mix/keycode_events.snapshot new file mode 100644 index 00000000000..9ab32b1a876 --- /dev/null +++ b/app/tests/leader/roll-three-mix/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/roll-three-mix/native_posix_64.keymap b/app/tests/leader/roll-three-mix/native_posix_64.keymap new file mode 100644 index 00000000000..19ae25d96bc --- /dev/null +++ b/app/tests/leader/roll-three-mix/native_posix_64.keymap @@ -0,0 +1,14 @@ +#include "../behavior_keymap_three.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,2,2000) + >; +}; diff --git a/app/tests/leader/roll-three-mix2/events.patterns b/app/tests/leader/roll-three-mix2/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/roll-three-mix2/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/roll-three-mix2/keycode_events.snapshot b/app/tests/leader/roll-three-mix2/keycode_events.snapshot new file mode 100644 index 00000000000..9ab32b1a876 --- /dev/null +++ b/app/tests/leader/roll-three-mix2/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/roll-three-mix2/native_posix_64.keymap b/app/tests/leader/roll-three-mix2/native_posix_64.keymap new file mode 100644 index 00000000000..f5a7153d8a6 --- /dev/null +++ b/app/tests/leader/roll-three-mix2/native_posix_64.keymap @@ -0,0 +1,14 @@ +#include "../behavior_keymap_three.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,2000) + >; +}; diff --git a/app/tests/leader/roll-three/events.patterns b/app/tests/leader/roll-three/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/roll-three/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/roll-three/keycode_events.snapshot b/app/tests/leader/roll-three/keycode_events.snapshot new file mode 100644 index 00000000000..9ab32b1a876 --- /dev/null +++ b/app/tests/leader/roll-three/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/roll-three/native_posix_64.keymap b/app/tests/leader/roll-three/native_posix_64.keymap new file mode 100644 index 00000000000..ab090a0b7cc --- /dev/null +++ b/app/tests/leader/roll-three/native_posix_64.keymap @@ -0,0 +1,14 @@ +#include "../behavior_keymap_three.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_PRESS(0,2,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,10) + ZMK_MOCK_RELEASE(0,2,2000) + >; +}; diff --git a/app/tests/leader/roll/events.patterns b/app/tests/leader/roll/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/roll/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/roll/keycode_events.snapshot b/app/tests/leader/roll/keycode_events.snapshot new file mode 100644 index 00000000000..f73a0180d59 --- /dev/null +++ b/app/tests/leader/roll/keycode_events.snapshot @@ -0,0 +1,4 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x06 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/roll/native_posix_64.keymap b/app/tests/leader/roll/native_posix_64.keymap new file mode 100644 index 00000000000..3a2e8032349 --- /dev/null +++ b/app/tests/leader/roll/native_posix_64.keymap @@ -0,0 +1,12 @@ +#include "../behavior_keymap_overlap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_RELEASE(0,1,2000) + >; +}; diff --git a/app/tests/leader/timeout/events.patterns b/app/tests/leader/timeout/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/timeout/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/timeout/keycode_events.snapshot b/app/tests/leader/timeout/keycode_events.snapshot new file mode 100644 index 00000000000..c476ce63ba4 --- /dev/null +++ b/app/tests/leader/timeout/keycode_events.snapshot @@ -0,0 +1,2 @@ +leader: leader key activated +leader: leader key deactivated diff --git a/app/tests/leader/timeout/native_posix_64.keymap b/app/tests/leader/timeout/native_posix_64.keymap new file mode 100644 index 00000000000..92926ec3838 --- /dev/null +++ b/app/tests/leader/timeout/native_posix_64.keymap @@ -0,0 +1,8 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,2000) + >; +}; diff --git a/app/tests/leader/two-seq/events.patterns b/app/tests/leader/two-seq/events.patterns new file mode 100644 index 00000000000..e73813454e0 --- /dev/null +++ b/app/tests/leader/two-seq/events.patterns @@ -0,0 +1,2 @@ +s/.*hid_listener_keycode_//p +s/.*zmk_leader.*:/leader:/p diff --git a/app/tests/leader/two-seq/keycode_events.snapshot b/app/tests/leader/two-seq/keycode_events.snapshot new file mode 100644 index 00000000000..4e5ad45caa9 --- /dev/null +++ b/app/tests/leader/two-seq/keycode_events.snapshot @@ -0,0 +1,8 @@ +leader: leader key activated +pressed: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x04 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated +leader: leader key activated +pressed: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +released: usage_page 0x07 keycode 0x05 implicit_mods 0x00 explicit_mods 0x00 +leader: leader key deactivated diff --git a/app/tests/leader/two-seq/native_posix_64.keymap b/app/tests/leader/two-seq/native_posix_64.keymap new file mode 100644 index 00000000000..d364d4b4c27 --- /dev/null +++ b/app/tests/leader/two-seq/native_posix_64.keymap @@ -0,0 +1,14 @@ +#include "../behavior_keymap.dtsi" + +&kscan { + events = < + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,0,10) + ZMK_MOCK_RELEASE(0,0,10) + ZMK_MOCK_PRESS(1,1,10) + ZMK_MOCK_RELEASE(1,1,10) + ZMK_MOCK_PRESS(0,1,10) + ZMK_MOCK_RELEASE(0,1,2000) + >; +}; diff --git a/docs/docs/behaviors/leader-key.md b/docs/docs/behaviors/leader-key.md new file mode 100644 index 00000000000..bcea191779e --- /dev/null +++ b/docs/docs/behaviors/leader-key.md @@ -0,0 +1,42 @@ +--- +title: Leader Key Behavior +sidebar_label: Leader Key +--- + +## Summary + +The leader key behavior when triggered will capture all following key presses and trigger a [Leader Sequence](../features/leader.md) if pressed. + +### Behavior Binding + +- Reference: `&leader` + +Example: + +``` +&leader +``` + +### Configuration + +#### `timeout-ms` + +Defines the amount of time to wait to trigger a completed leader sequence. Defaults to 200ms. + +To change the timeout term, you can update the existing behavior: + +``` +&leader { + timeout-ms = <500>; +}; + +/ { + keymap { + ... + }; +}; +``` + +#### `timerless` + +By default, the leader key will have a timeout, and will not wait for a sequence to be completed or another key to be pressed. Specify `timerless` if you want a timeout. diff --git a/docs/docs/config/leader.md b/docs/docs/config/leader.md new file mode 100644 index 00000000000..216a19357dd --- /dev/null +++ b/docs/docs/config/leader.md @@ -0,0 +1,40 @@ +--- +title: Leader Configuration +sidebar_label: Leader +--- + +See the [Leader feature page](../features/leader.md) for more details and examples. + +See [Configuration Overview](index.md) for instructions on how to change these settings. + +## Kconfig + +Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/Kconfig) + +| Config | Type | Description | Default | +| ----------------------------------------- | ---- | ----------------------------------------------------------------- | ------- | +| `CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE` | int | Maximum number of leader sequences that use the same key position | 4 | +| `CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY` | int | Maximum number of keys to press to activate a leader sequence | 5 | + +`CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE` sets the maximum length of a leader sequence. + +If `CONFIG_ZMK_LEADER_MAX_SEQUENCES_PER_KEY` is 5, you can have 5 separate leader sequences that start with position `0`, 5 leader sequences that start with position `1`, and so on. + +## Devicetree + +Applies to: `compatible = "zmk,leader-sequences"` + +Definition file: [zmk/app/dts/bindings/zmk,leader-sequences.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/dts/bindings/zmk%2Cleader-sequences.yaml) + +The `zmk,leader-sequences` node itself has no properties. It should have one child node per leader sequence. + +Each child node can have the following properties: + +| Property | Type | Description | Default | +| ------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `bindings` | phandle-array | A [behavior](../features/keymaps.md#behaviors) to run when the leader sequence is triggered | | +| `key-positions` | array | A list of key position indices for the keys which should trigger the leader sequence | | +| `immediate-trigger` | bool | Triggers the leader sequence when all keys are pressed instead of waiting for the timeout (only applicable when one leader sequence overlaps another) | false | +| `layers` | array | A list of layers on which the leader sequence may be triggered. `-1` allows all layers. | `<-1>` | + +The `key-positions` array must not be longer than the `CONFIG_ZMK_LEADER_MAX_KEYS_PER_SEQUENCE` setting, which defaults to 4. If you want a leader sequence that triggers when pressing 5 keys, then you must change the setting to 5. diff --git a/docs/docs/features/leader.md b/docs/docs/features/leader.md new file mode 100644 index 00000000000..05b7be60c0b --- /dev/null +++ b/docs/docs/features/leader.md @@ -0,0 +1,39 @@ +--- +title: Leader Sequences +--- + +## Summary + +Leader sequences are a way to have a sequence of key presses to output a different key. For example, you can hit the leader key followed by Q, then W to output escape. + +### Configuration + +Leader sequences configured in your `.keymap` file, but are separate from the `keymap` node found there, since they are processed before the normal keymap. They are specified like this: + +``` +/ { + leader_sequences { + compatible = "zmk,leader-sequences"; + seq_esc { + key-positions = <0 1>; + bindings = <&kp ESC>; + }; + }; +}; +``` + +- The `compatible` property should always be `"zmk,leader-sequences"` for leader sequences. +- `key-positions` is an array of key positions. See the info section below about how to figure out the positions on your board. +- `layers = <0 1...>` will allow limiting a sequence to specific layers. This is an _optional_ parameter, when omitted it defaults to global scope. +- `bindings` is the behavior that is activated when all the key positions are pressed. +- (advanced) you can specify `immediate-trigger` if you want the sequence to be triggered as soon as all key positions are pressed. The default is to wait for the leader key's timeout to trigger the sequence if it overlaps another. + +:::info + +Key positions are numbered like the keys in your keymap, starting at 0. So, if the first key in your keymap is `Q`, this key is in position `0`. The next key (possibly `W`) will have position 1, etcetera. + +::: + +### Advanced usage + +See [leader configuration](/docs/config/leader) for advanced configuration options. diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 142dcafc97e..66750778097 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -33,6 +33,7 @@ ZMK is currently missing some features found in other popular firmware. This tab | One Shot Keys | ✅ | ✅ | ✅ | | [Combo Keys](features/combos.md) | ✅ | | ✅ | | [Macros](behaviors/macros.md) | ✅ | ✅ | ✅ | +| [Leader Key](behaviors/leader-key.md) | ✅ | | ✅ | | Mouse Keys | 🚧 | ✅ | ✅ | | Low Active Power Usage | ✅ | | | | Low Power Sleep States | ✅ | ✅ | | diff --git a/docs/sidebars.js b/docs/sidebars.js index 43f17b4193f..2f53b57ce95 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -12,6 +12,7 @@ module.exports = { "features/keymaps", "features/bluetooth", "features/combos", + "features/leader", "features/conditional-layers", "features/debouncing", "features/displays", @@ -29,6 +30,7 @@ module.exports = { "behaviors/mod-tap", "behaviors/mod-morph", "behaviors/macros", + "behaviors/leader-key", "behaviors/key-toggle", "behaviors/sticky-key", "behaviors/sticky-layer",