zmk
zmk copied to clipboard
feat(behaviors): Smart-layers (e.g., num-word) and caps-word enhancements
EDIT: The PR is somewhat outdated. An updated version focusing on the auto-layer functions is available as ZMK module
This PR contains a number of enhancements for the caps-word behavior. Most importantly it adds a smart-layer option that activates a layer until a key not in continue-list
is pressed. The PR supersedes https://github.com/zmkfirmware/zmk/pull/1422
New/updated backend-properties:
The backend behavior zmk,behavior-caps-word
(not to be confused with the frontend &caps_word
) now supports the following config options:
-
[updated]
mods
: Specifies the mods that get activated when smart-word is active. New default:0
(none). -
[new] int
layers
: Specifies the layer that gets activated when smart-word is active. Default:-1
(none) -
[new] bool
ignore-alphas
: If true, alphas do not cancel the smart-word behavior -
[new] bool
ignore-numbers
: If true, numbers do not cancel the smart-word behavior -
[new] bool
ignore-modifiers
: If true, modifiers do not cancel the smart-word behavior
The new layers
property is used to configure the new smart-layers capabilities. The new ignore-*
flags can be used to configure whether smart-word continues on alphas, numbers or modifiers (the original caps-word would always continue on all of these regardless of the value of continue-list
)
Smart-layer example: Num-word
For instance, to set up a "numword", one can define a new behavior as follows:
\ {
behaviors {
num_word: behavior_num_word {
compatible = "zmk,behavior-caps-word";
label = "NUM_WORD";
#binding-cells = <0>;
layers = <NUM>; // insert location of numbers layer here
continue-list = <BACKSPACE DELETE DOT COMMA>; // adjust as desired
ignore-numbers; // numbers don't deactivate the layer
};
};
};
Pressing &num_word
will activate the number-layer and keep it active for as long as only numbers, backspace, space, dot or comma are pressed. After any other key is pressed, say, "space", the layer is automatically deactivated. This works well if the number-layer is mostly transparent, so that one can just continue typing normally after finishing with the numbers without having to actively release the number-layer.
For convenience, the PR also adds a pre-defined &num_word
behavior. To use it, one only needs to specify the correct layer specification in the user config:
&num_word {
layers = <NUM>; // replace NUM by the location of numbers layer
};
Caps-word
The pre-defined caps-word definition has been adjusted to yield the old behavior using the new configuration-flags:
/ {
behaviors {
/omit-if-no-ref/ caps_word: behavior_caps_word {
compatible = "zmk,behavior-caps-word";
label = "CAPS_WORD";
#binding-cells = <0>;
mods = <MOD_LSFT>;
continue-list = <UNDERSCORE BACKSPACE DELETE>;
ignore-alphas;
ignore-numbers;
ignore-modifiers;
};
};
};
On the user-side nothing changes when using &caps_word
.
Using ignore-flags to tweak caps-word
For backwards-compatibility, the frontend implementation of &caps_word
continues on all alphas, numbers and modifiers. This can be overwritten using /delete-property/
. For instance, to cancel caps-word when numbers and modifiers are pressed, one can add the following to the user config:
&caps_word {
/delete-property/ ignore-numbers;
/delete-property/ ignore-modifiers;
};
This is useful, for example, for people who activate caps-word with somewhat complex key combos or tap dances and who wish to be able to deactivate it by simply pressing LEFT_SHIFT (of course, it still deactivates automatically with any other key not specified in continue-list
). This fixes https://github.com/zmkfirmware/zmk/issues/1410.
If one wants more fine-grained control over which modifiers or numbers cancel caps-word, one can use continue-list
to do so. For example, if one wants to continue caps-word on shift but cancel it with all other modifiers, one can do so by adding the following to the user-config:
&caps_word {
/delete-property/ ignore-modifiers;
continue-list = <UNDERSCORE BACKSPACE DELETE LSHFT RSHFT>;
};
Discussion
Would it make sense to rename the backend-behavior from zmk,behavior-caps-word
to something like zmk,behavior-smart-word
? It might be a bit confusing that the zmk,behavior-caps-word
backend (which can be used to implement any smart-modifier or smart-layer) has almost exactly the same name as the &caps_word
frontend that sets mods
to MOD_LSFT
and layers
to -1
(none).
Unless I did something incorrectly, this seems to only work on keys N1-N6, it does not work properly for KP_N0-KP_N9, nor KP_DOT. I don't believe I did something incorrectly, otherwise it would not work for the N1-6 keys, I expect.
If I tap &num_word, KP_N0-9 will insert a single number and then the keypad layer will deactivate. If I hit plain N1-6 on the number row across the top, it will remain in the numeric layer. Do I have to do something specific/different for this to stay on the keypad layer when using KP_N numbers as opposed to using N numbers? I have the ignore-numbers bool enabled as per the example listed above.
Behavior implementation: https://github.com/instance-id/Adv360-Pro-ZMK/blob/9f241c1671b44a36fd6b92064022a3180f056b6e/config/adv360.keymap#L21
Key implementation: line 38/col 65 https://github.com/instance-id/Adv360-Pro-ZMK/blob/9f241c1671b44a36fd6b92064022a3180f056b6e/config/adv360.keymap#L38
Unless I did something incorrectly, this seems to only work on keys N1-N6, it does not work properly for KP_N0-KP_N9, nor KP_DOT. I don't believe I did something incorrectly, otherwise it would not work for the N1-6 keys, I expect.
If I tap &num_word, KP_N0-9 will insert a single number and then the keypad layer will deactivate. If I hit plain N1-6 on the number row across the top, it will remain in the numeric layer. Do I have to do something specific/different for this to stay on the keypad layer when using KP_N numbers as opposed to using N numbers? I have the ignore-numbers bool enabled as per the example listed above.
Behavior implementation: https://github.com/instance-id/Adv360-Pro-ZMK/blob/9f241c1671b44a36fd6b92064022a3180f056b6e/config/adv360.keymap#L21
Key implementation: line 38/col 65 https://github.com/instance-id/Adv360-Pro-ZMK/blob/9f241c1671b44a36fd6b92064022a3180f056b6e/config/adv360.keymap#L38
The ignore-numbers
boolean is only for the regular numbers. But you should be able to manually add the keypad numbers to continue-list
:
\ {
behaviors {
num_word: behavior_num_word {
compatible = "zmk,behavior-caps-word";
label = "NUM_WORD";
#binding-cells = <0>;
layers = <NUM>; // insert location of numbers layer here
continue-list = <BACKSPACE DELETE DOT COMMA KP_N0 KP_N1 KP_N2 etc etc>;
ignore-numbers;
};
};
};
Good call, that did the trick. I do appreciate it. :+1:
continue-list = <BACKSPACE DELETE DOT COMMA KP_N0 KP_N1 KP_N2 KP_N3 KP_N4 KP_N5 KP_N6 KP_N7 KP_N8 KP_N9 KP_DOT KP_EQUAL KP_DIVIDE KP_MULTIPLY KP_MINUS KP_ENTER LEFT RIGHT>;
I'm using num_word through urob/zmk and I noticed pressing !@#$% (EXLAMATION, AT, HASH, ...) through unshifted keymaps don't toggle off the layer, but -+~ etc. do. Is there a way to get all punctuations to toggle off the layer?
I'm using num_word through urob/zmk and I noticed pressing !@#$% (EXLAMATION, AT, HASH, ...) through unshifted keymaps don't toggle off the layer, but -+~ etc. do. Is there a way to get all punctuations to toggle off the layer?
Yes, the current implementation does not distinguish between shifted/unshifted keycodes. So all symbols that under the hood are implemented via shift + number won't trigger the exit condition.
For now, you could use tri-state as an alternative, allowing you to define exit conditions purely based on key positions.
Just wanted to drop by and thank you @urob! This made an incredible difference in comfort, cognitive ease and speed on my keymap. Feels so intuitive now! https://github.com/minusfive/zmk-config
Wondering whether there are any plans of merging this to main? Feels like a no-brainer for a native feature, and I know there are already a ton of users, right?
@maintainers could we please get this merged for the next release? It's an excellent feature that doesn't break anything (as far as I can see), and a lot of people would love to see it on mainline ZMK
I also love this feature and it’s working well.
It hasn’t received any feedback from the maintainers, but I imagine squeezing these features into the caps word implementation isn’t the way to go.
I think either defining a new behavior or adding it to the layer-toggle behavior would probably be more appropriate.
First off, I really enjoy all the work @urob has been doing and I'm using his ZMK repo for my stuff as well.
But regarding this functionality here I came upon one issue with my numbers layer that is definitely related to the layer addition (which capsword does not use).
That is that an &mo
layer does not overlay the numbers layer. If I activate my &numword
and then try to use my navigation &mo
layer by holding down a thumb key (which is transparent on the num layer), the numbers still take preference and I can't move the cursor around (I added RIGHT and LEFT to my continue list for numword).
This should actually be reproducable with urobs own keymap directly (e.g. try the swapper or any F## key with numword active).
I have been pondering about the future of this PR. The current proposal generalizes -- or perhaps more fittings: piggybacks on -- caps-word
. Implementation-wise this seemed to make sense to me at the time, given that 95% of the code is shared.
But design-wise I am wondering whether it would make sense to split off the backend behavior for "smart layers" from the one for "caps word" (especially in light of #1742 which adds some more customization options for the latter)?
Specifically, I am thinking of a more focused smart-layer
behavior that takes one argument (the layer index) and activates the layer until a key not in the ignore list is pressed. Relative to the current implementation it would drop support for locking mods and would move the layer spec from a property to an argument. The latter change would allow us to pre-define a fully functional num_word
which could be added to the keymap with &num_word LAYER
instead of needing to overwrite the behavior node.
@urob with your new implementation idea, how would you make a caps-word that ignore modifiers? Keep it in caps-word in a separate PR?
@urob with your new implementation idea, how would you make a caps-word that ignore modifiers? Keep it in caps-word in a separate PR?
At some point @joelspadin seemed open to add this to #1742. I am not sure what the current verdict is but IMHO this would be the best solution. Otherwise, I could also open a new PR for just that.
@urob how about a behavior that doesn’t define an allowed list at all? Instead it could just be “smart layer toggle”, which turns on a layer and if a none or trans behavior on it is hit, the layer is deactivated.
People have been using your PR to implement smart nav and symbol layers.
This would allow to create a generalized implementation that doesn’t need any customization for those.
I just want to get #1742 merged first, and then I can look into more enhancements like allowing it to cancel when modifiers are pressed (assuming I understand what you're wanting to do).
@urob how about a behavior that doesn’t define an allowed list at all? Instead it could just be “smart layer toggle”, which turns on a layer and if a none or trans behavior on it is hit, the layer is deactivated.
That makes a lot of sense. Instead of a continue-list
one could have a cancel-list
. It would default to &trans &none
but allows adding other conditions like &kp G
on a "vim-num" layer.
What I am not sure atm is how hard it would be to switch to a condition model that's behavior-based and is able to detect &trans
and &none
. But I definitely see the merit and think it's worth exploring.
@urob
What I am not sure atm is how hard it would be to switch to a condition model that's behavior-based and is able to detect
&trans
and&none
. But I definitely see the merit and think it's worth exploring.
I will soon start working on a feature that needs the same functionality and recently asked about it in the #development discord channel. Currently there is only a "pretty gross" way to accomplish it without adding new trans and none behaviors.
But if a major feature like your num layer implementation is using it and I also need it, then maybe it would make sense to turn the "dirty way" into a proper API or notification event.
That makes a lot of sense. Instead of a
continue-list
one could have acancel-list
. It would default to&trans &none
but allows adding other conditions like&kp G
on a "vim-num" layer.
I would agree that the ability to cancel using &trans and &none
would be incredibly useful. Though I can appreciate the complexity of implementing that functionality.
I was initially going to suggest having different "flavors" of the smart layer behavior, like the hold tap behavior has to specify a 'cancel-list' flavor, or a 'continue-list' flavor. But, I cannot think of an instance when you would need the continue-list if it is switching to a different layer and you cancel out with &trans and &none
. The 'continue-list' is simply the key mappings you include on that smart layer. And if I'm understanding it correctly, it would also allow more complex behaviors to be executed on that smart layer without cancelling it out.