zmkfirmware / zmk

ZMK Firmware Repository
https://zmk.dev/
MIT License
2.82k stars 2.85k forks source link

Layer behaviors to only enable or disable a layer instead of toggling #2523

Open felix-hilden opened 1 month ago

felix-hilden commented 1 month ago

The ability to only turn on or off a layer with dedicated behaviors would make complex layer setups and combos easier. Let me give a specific example:

I regularly work and switch between two computers. One is connected via USB and uses Windows, and one is a Mac I connect to with Bluetooth. To make my layers consistent across the operating systems, I've defined some patches to my basic layers to activate on a flag layer. I always activate this layer when switching to prefer bluetooth connections, and disable it when going back to USB. But the way I need to do it now, is with a layer toggle. Sometimes I mistype, and end up with the wrong layer and output device combo.

Instead of using a layer toggle, I'd want to be sure I am explicitly activating or deactivating a layer. This would also make it usable in a macro, where I can put both actions behind a single key.

Any thoughts? Maybe I've just missed something obvious :sweat_smile: and if that's the case, please do inform me. Many thanks for considering this!

saponace commented 1 month ago

I would benefit from this too.

My usecase is a little different though: I use tap-dance to on my dedicated layers buttons:

The issue with this is: if I'm on layer_1 toggled, and double tap layer_2 button, both layer_1 and layer_2 will be enabled, and double tapping layer_2 button will return to layer_1.

With the behaviour: I could create a macro that, on double tap, disables all layers and then toggle (or enable, same at this point) the layer this button is dedicated to. This way I'd always have a single layer enabled and know I just have to double tap the currently enabled layer to return to base layer.

caksoylar commented 1 month ago

With the behaviour: I could create a macro that, on double tap, disables all layers and then toggle (or enable, same at this point) the layer this button is dedicated to.

IIUC the &to behavior should work for this: https://zmk.dev/docs/keymaps/behaviors#layer-navigation-behaviors

saponace commented 1 month ago

My bad, I simplified my situation for the sake of explaining, but turns out I oversimplified.

I have to regularly switch between OSX and Linux, and for this reason I actually have an intermediate layer (swaps LGUI and OSX) I toggle with another combo. So my layers are:

This causes that I cannot use &to since it would disable layer_OSX and I want to keep it active (if it was active) when double tapping another layer key.

The macro I want to do is, when double tapping layer_X: disable layer_1, disable layer_2, disable layer_3, then enable layer_X

minusfive commented 1 month ago

@saponace I'm probably missing something, but perhaps take a look at my &csl and &cmo macros here? https://github.com/minusfive/zmk-config/blob/main/knucklehead/macros.dtsi#L30

I [try to] explain it here: https://github.com/minusfive/zmk-config#upper-layer-swapping-vs-stacking

saponace commented 1 month ago

Thank you for taking the time to answer. However, I do not fully understand your macro csl.

It uses keycode K_CANCEL that, according to your documentation, clears all active smart layers. But I don't see anywhere in your repository a config that would define this behaviour, neither do I see anything relevant about this keycode in ZMK documentation. All I can find is https://zmk.dev/docs/keymaps/list-of-keycodes, where it doesn't say anything about this keycode being intercepted or having a specifig behaviour.

To be sure, I tried to add a button that sends &kp K_CANCEL, but it does nothing aside from sending this keycode to the computer.

I also know github issues is not the best place for asking for support and would gladly move somewhere else if you are willing to explain how your macro works.

minusfive commented 1 month ago

@saponace are you on the ZMK discord? Ping me there

minusfive commented 1 month ago

@saponace FYI K_CANCEL is a normal keycode typically ignored by most OSes, essentially used as a noop; in my case to break out of smart / sticky behaviors / layers, as it's just not in the continue list.

But in your case... I took a look at your layout, and if I understood correctly, couldn't you do something like this?:

/*
This macro:
  - disables all layers except `0` (default) - see `&to` documentation
  - and activates the desired `base layer` (param 1)
  - and subsequently toggles the desired `upper layer` (param 2)

Params:
  1. base layer to activate
  2. upper layer to activate

Example:
  `&btog OSX NUM`
*/
btog: btog {
  wait-ms = <0>;
  tap-ms = <0>;
  compatible = "zmk,behavior-macro-two-param";
  #binding-cells = <2>;
  bindings = <&macro_param_1to1>,
    <&macro_press &to MACRO_PLACEHOLDER>,
    <&macro_param_2to1>,
    <&macro_press &tog MACRO_PLACEHOLDER>;
};

Then on your ALPHA layer you can use normal &to or &tog behaviors (or this macro if you want to be explicit), and on your OSX layer you override those keys/combos with this macro.

Does that make sense?

felix-hilden commented 1 month ago

That's actually a pretty good workaround for me too! Mainly I would indeed want to activate an extra layer on top of the base (and also include the output device preference in the macro), and even activating different base layers would be possible by using different macro params based on the current layer - maybe with conditional layers.

Still, being able to disable and enable layers individually would make it much easier and succinct to define this kind of behavior 🙏

minusfive commented 1 month ago

@felix-hilden nice, glad to be of help. That layer + bt profile case sounds cool, hadn't considered it, but yeah, should work w/macros.

And yeah, I can still see how explicit layer on/off would be nice in these cases.

saponace commented 1 month ago

Oh wow, that's a smart workaround. Thank you for taking the time to look into my config @minusfive ! I added the macro you suggest.

This has me double the number of tap-dance behaviors I have for each layer: &num to be called from the base layer (btog ALPHA NUM) and &num_on_osx to be called from OSX layer (btog OSX NUM). That's not very elegant, but it works.

The downside of this workaround with my keymap is that it doesn't work for layer SYSTEM, that is triggered via a combo. From within the combo I don't know what behavior to trigger: num or num_on_osx because I don't the context (OSX layer enabled or disabled).

The best I've found is to create another combo lsystem_on_osx that filters on layer OSX and triggers the behavior &system_on_osx, and leave combo lsystem unchanged (trigger behavior &suystem). To avoid conflicts with the combo lsystem, I need to specify layers for lsystem (all layers except OSX).

This causes that whenever I leave layer SYSTEM by tapping the combo once from NUM or SYMBOLS it triggers behavior &system and returns to layer ALPHA instead of OSX. Surprisingly, and for a reason I don't understand, triggering the combo from layer SYSTEM with layer OSX enabled returns to OSX instead of ALPHA. Which is great because it is the behavior I want, but it does not make sense to me why it works that way.

For reference, this is my config as defined above: https://github.com/saponace/zmk-config/blob/525983a54677827eb93b2d97ee3e488abad8e77f/config/corne.keymap

Another surprising behaviour I don't understand is: I tried to enable combo lsystem only on layers ALPHA and SYSTEM. It would behave weirdly in that config: I could tap once the combo from any layer to return to the expected base layer (OSX or ALPHA) from anywhere, but couldn't hold the combo for the first tap of the tap-dance behavior (&sl). It is a cool behavior I could see myself using, but I don't want to leave in my config something that doesn't make sense and it reduces maintainability of the config.

Anyways, long story short. Thanks for your help, and a new behavior &lon and &loff to turn on/off a layer would make some configs so much more simple

minusfive commented 1 month ago

@felix-hilden pleasure!

You can make macros work on hold, just need to use &macro_pause_for_release. You can read more about it here https://zmk.dev/docs/keymaps/behaviors/macros#processing-continuation-on-release, and see my example here:

https://github.com/minusfive/zmk-config/blob/main/knucklehead/macros.dtsi#L3-L28

Generally I recommend you read thoroughly those docs, and experiment, experiment, experiment. I've found ZMK's power lies primarily in its composability and modularity, as you don't have to depend on new features being added to the core that often, as the primitives can give you most of the behaviors you want. BUT there's a LOT there, and takes patience and experimentation to leverage it. I feel I'm only scratching the surface myself.

The solution for your combo issue, that I'm not sure about. I'd have to think about it more. Top level it'd be best if you contained the contextual behaviors to the base layer, and used regular &trans keys on all upper layers. Perhaps moving "something else" to the combo? But I know that's not going to work in all cases. That's probably just my head trying to find the simplest path, though :P.

saponace commented 1 month ago

Holding the key/combo down works as expected and the tap-dance picks it up correctly as the first hold (and fires a &mo).

Regarding the combo behaving the way it does, I'll look further into it and will post here when (if) I find what I did wrong and how to do it properly.

Thank you again for your precious help. ZMK is indeed a tough piece to approach and I want to get better at understanding how it works and be more comfortable with it