zmk icon indicating copy to clipboard operation
zmk copied to clipboard

feat(behaviors): Smart-layers (e.g., num-word) and caps-word enhancements

Open urob opened this issue 2 years ago • 20 comments

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

urob avatar Sep 10 '22 22:09 urob

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

instance-id avatar Feb 02 '23 02:02 instance-id

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; 
		};
	};
};

urob avatar Feb 02 '23 02:02 urob

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>;

instance-id avatar Feb 02 '23 02:02 instance-id

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?

yanshay avatar Mar 10 '23 14:03 yanshay

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.

urob avatar Mar 11 '23 20:03 urob

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?

minusfive avatar Jul 24 '23 00:07 minusfive

@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

snprajwal avatar Sep 25 '23 14:09 snprajwal

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.

infused-kim avatar Sep 25 '23 15:09 infused-kim

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

tilliwilli avatar Oct 18 '23 16:10 tilliwilli

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 avatar May 07 '24 18:05 urob

@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?

zoriya avatar May 07 '24 19:05 zoriya

@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 avatar May 07 '24 19:05 urob

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

infused-kim avatar May 07 '24 21:05 infused-kim

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

joelspadin avatar May 08 '24 00:05 joelspadin

@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 avatar May 08 '24 01:05 urob

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

infused-kim avatar May 08 '24 03:05 infused-kim

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.

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.

zxku avatar May 09 '24 15:05 zxku