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

feat(behaviors): Leader key #1380

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

nickconway
Copy link
Contributor

Adds leader key functionality

@nickconway nickconway force-pushed the leader-key branch 5 times, most recently from c986393 to ddbc05d Compare July 9, 2022 17:30
@nickconway nickconway marked this pull request as ready for review July 9, 2022 19:01
@nickconway nickconway changed the title [WIP] feat(behaviors): Leader key feat(behaviors): Leader key Jul 9, 2022
@dxmh dxmh added enhancement New feature or request behaviors labels Jul 11, 2022
Copy link

@AlexCzar AlexCzar left a comment

Choose a reason for hiding this comment

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

Thank you so much, leader key is a very nice feature to have. Just a couple of questions/suggestions regarding the documentation.
Can't wait for this to be merged 👍🏽


#### `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.

Choose a reason for hiding this comment

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

I struggle to understand this part

  • Where and how timerless should be specified? Is it a possible value for timeout-ms or is it a separate option alongside it?
  • Why is the opition called "timerless"? The statement says that we should specify it if we want a timeout, it might be a typo and you meant to write "don't want a timeout"? In the latter case, the naming is still a bit non-intuitive, maybe it should be replaced by no-timeout = <true>, or even removed in favour of timeout-ms = <0>; or <timeout-ms = <-1>, or, if timerless is a possible value for timeout-ms, maybe a shorter = <false> or even = <no> could be a better option? (shooting up the sky here).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • Timerless would be a separate option, defined in the dts for the leader key along with timeout-ms and the other options.
  • Yeah, that was a typo, should be "don't want a timeout". Timeout-ms would need to be a positive number for the case of overlaps, unless we had something like overlap-timeout-ms and timeout-ms separate? Timerless is for when no sequence has been completed yet, and once an overlapped sequence is completed, timeout-ms is used.

- `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.

Choose a reason for hiding this comment

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

Where and how?

Copy link
Contributor

@petejohanson petejohanson left a comment

Choose a reason for hiding this comment

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

One important high level question on the design.

Thanks!


/ {
behaviors {
/omit-if-no-ref/ leader: leader_key {
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems to me we shouldn't limit this to just one global leader key, as this limits folks to just one leader key, and it's associated sequences.

Is there really no use case for different leader keys, with different possible sequences?

Seems like you could have child props in the yaml binding, and something like:

leader_foo: leader_foo {
  compatible = "zmk,behavior-leader";
  ...
  seq1 {
  };
  seq2 {
  };
};

That would give us a bit more flexibility.

If we really want to make it easy, we could still define one global leader you could enhance with children, e.g.:

&leader {
  seq1 {
  };
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now I use multiple leader keys with layers (see here). I use macros to switch layers before pressing the leader key which has the same effect although is a bit more work for the user.

properties:
timerless:
type: boolean
timeout-ms:
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs documenting as well. Also see the other reviewers question.

@petejohanson petejohanson self-assigned this Oct 8, 2022
@rmwphd rmwphd mentioned this pull request Dec 15, 2022
@grezp
Copy link

grezp commented Feb 22, 2023

Any update on this branch merging to main?

@vladsolokha
Copy link

I don't understand the difference of this "leader key" and a sticky layer. Can someone differentiate?

@cougarten
Copy link

I don't understand the difference of this "leader key" and a sticky layer. Can someone differentiate?

you can do more than 2 key presses and it can chain arbitrary long sequences as the triggers.

e.g. "super > s > e" for "leader key" > "shortcuts" > "email" to trigger spelling your email address (or anything you want).

@ujl123
Copy link

ujl123 commented Apr 12, 2023

I don't understand the difference of this "leader key" and a sticky layer. Can someone differentiate?

Other uses I use leader keys in QMK is (all are for windows)

Leader Key + B O O T = qmk reset
Leader Key + R G B = toggle rgb
Leader Key + Q = Alt F4
Leader key + Z Z Z = system shutdown
Leader key + T O P = CTRL + SHIFT + ESC (task manager in windows)
Leader Key + MKDIR = CTRL + SHIFT + N (new folder)
Leader Key + RM = shift delete (hard delete in windows)
Leader KEy + PIN = windows pin (not recommended storing passwords on your keyboard but I'm lazy).

I'm hoping that this gets reviewed and can be merged into the main branch though as this is a major component that makes my 32 key keyboard work in QMK (along with tap mods, tap dances autoshifts, custom autoshift keys)

Copy link
Contributor

@petejohanson petejohanson left a comment

Choose a reason for hiding this comment

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

A few more items from my first pass at looking at this again. Thanks again for working on this!

app/src/leader.c Outdated
Comment on lines 340 to 352
#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)
Copy link
Contributor

Choose a reason for hiding this comment

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

If we really are only supporting one instance of this node, then the FOREACH mechanics are superfluous and misleading. Just create one static struct instance directly.


static int leader_init() {
k_work_init_delayable(&release_timer, behavior_leader_key_timer_handler);
DT_INST_FOREACH_CHILD(0, INTITIALIAZE_LEADER_SEQUENCES);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto, this adds unnecessary complexity since only a single instance of the node is supported.

app/src/leader.c Outdated
Comment on lines 25 to 36
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;
Copy link
Contributor

Choose a reason for hiding this comment

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

These all need to be static.


void zmk_leader_activate(int32_t timeout, bool timeout_on_activation, uint32_t position);
void zmk_leader_deactivate();
bool zmk_leader_get_status();
Copy link
Contributor

Choose a reason for hiding this comment

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

status I don't love... is active? is enabled? Something a bit more aligned with the bool return value here.

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);
Copy link
Contributor

Choose a reason for hiding this comment

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

If you press this again while it's active, should the behavior be to deactivate to interrupt it?

app/src/leader.c Outdated

#define LEADER_INST(n) \
static struct leader_seq_cfg sequence_config_##n = { \
.virtual_key_position = ZMK_KEYMAP_LEN + __COUNTER__, \
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be refactored to use the virtual key position work @joelspadin added for combos, etc.

app/src/leader.c Outdated
Comment on lines 55 to 70
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};
Copy link
Contributor

Choose a reason for hiding this comment

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

All should also be static

@arabshapt
Copy link

Please try to finish this feature... I will soon try to merge it on my branch, but it would be great to have it on master. I do not understand how people are not showing more interest for it. If it is easy to add key sequences(similar to vimrc) then it would be even better, but I think that can be done with a custom function... The main thing is having it on master and bugfree:) Thank you all but especially @nickconway and @petejohanson for working on it🙂

@arabshapt
Copy link

Is there any limitation regarding number of sequences that can be created? For combos there are (I think) 5 combos that can use the same key position. Could one create e.g. 900 sequences: leader + letter_1 + letter_2 = shortcut/macro?
So three key taps, around 900 unique sequences. Would it be possible with this implementation?

@tvdab
Copy link

tvdab commented Feb 25, 2024

I was a happy user of the leader key feature in my previous zmk build. Recently I tried updating my firmware to the latest release of zmk (c9c620d) and also update from zephyr sdk 0.15 to 0.16. Everything seems to work, except for the leader key. The leader key acts like a transparent key. Also running the test tests/leader/basic fails:

❯ west test tests/leader/basic
Running tests/leader/basic:
--- tests/leader/basic/keycode_events.snapshot	2024-02-25 22:11:10.516535082 +0100
+++ build/tests/leader/basic/keycode_events.log	2024-02-25 22:35:07.778445825 +0100
@@ -1,4 +1,2 @@
-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
+pressed: usage_page 0x07 keycode 0x1E implicit_mods 0x00 explicit_mods 0x00
+released: usage_page 0x07 keycode 0x1E implicit_mods 0x00 explicit_mods 0x00
FAILED: tests/leader/basic

Could someone verify if the leader key works with zephyr sdk 0.16 and a recent version of zmk? Maybe I'm doing something wrong...

@caksoylar
Copy link
Contributor

I am guessing this needs to be rebased for #2028.

@tvdab
Copy link

tvdab commented Mar 30, 2024

I found why it wasn't working in Zephyr 3.5. This is what needs to be changed in order to make it work again:

--- a/app/src/behaviors/behavior_leader_key.c
+++ b/app/src/behaviors/behavior_leader_key.c
@@ -47,8 +47,8 @@ static const struct behavior_driver_api behavior_leader_key_driver_api = {
 #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,                            \
+    BEHAVIOR_DT_INST_DEFINE(n, behavior_leader_key_init, NULL, NULL,                                 \
+                          &behavior_leader_key_config_##n, POST_KERNEL,                            \
                           CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_leader_key_driver_api);

 DT_INST_FOREACH_STATUS_OKAY(LEAD_INST) 

There are probably more changes to be made (#2028) to update everything to zephyr 3.5.

@nickconway nickconway requested a review from a team as a code owner April 14, 2024 15:32
@magoz
Copy link

magoz commented May 11, 2024

Looking forward to this, great work!

j-w-e added a commit to j-w-e/zmk that referenced this pull request Jun 5, 2024
@nickconway nickconway requested a review from a team as a code owner August 18, 2024 01:23
@urob
Copy link
Contributor

urob commented Sep 11, 2024

I have been using this for a while now and really loving it! Just a few small comments to nitpick here:

Keycode vs position-based?

I see advantages for both sides. But overall I tend to think that this one might be more useful being keycode based.

The main reason is that in my experience leader sequences are almost always mnemonic (or are otherwise remembered in terms of their alphanumeric sequence). With this presumption, making them keycode based simplifies configuration and increases portability across layouts without much of a downside.

Purpose of layer option

I don't quite see the use case for this one. With the current position-based model, the sequence tracks every key press. So the active layer is a deterministic function of the layer on which &leader is placed and the sequence of keys, which can be matched against &leader_sequence. While it doesn't hurt to have the option, it might be unnecessary clutter?

(Similar, with a keycode-based model I can't really think of a use case either.)

Small bug?

Suppose I have two overlapping sequences:

  • <0> mapping to &kp A
  • <0 1> mapping to &kp B

Then the typing the sequence <0 2> within the leader term will eat up the sequence and not produce anything. I would expect it to match <0> and generate &kp A, and then terminate the processing of the sequence so that <2> can be processed by whatever behavior it is bound to.

@urob
Copy link
Contributor

urob commented Nov 25, 2024

If anyone is interested using this as a module that works without having to patch the ZMK firmware, I just pushed one to https://github.com/urob/zmk-leader-key.

For now it's a one-for-one adaption of Nick's PR but I am planning some changes going forward.

EDIT: I ended up reimplementing the behavior, making some changes to fit my personal needs (keycode-based, allow multiple instances, ...). For anyone who prefers the original PR, there's a legacy branch that makes that available as module as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
behaviors enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.