pqrs-org / Karabiner-Elements

Karabiner-Elements is a powerful utility for keyboard customization on macOS Sierra (10.12) or later.
https://pqrs.org/osx/karabiner/
The Unlicense
18.86k stars 838 forks source link

Bug: unexpected order-dependence when setting variables in complex manipulators #3068

Open andmis opened 2 years ago

andmis commented 2 years ago

I would like to be able to build leader keys in Karabiner.

For example, suppose that I would like the key sequence [f1, a] to produce the key sequence [f, o, o]. To be clear, I am not trying to use f1 as a modifier key, I am trying to use f1 as a leader. I would like to be able to press and release f1, and then (within some timeout) press a, and have this generate [f, o, o].

We can use Karabiner variables to make f1 act as a modifier while it is held down. But I don't see a way to make f1 act as a leader. For example, the following code almost works:

{
    "description": "Sample leader",
    "manipulators": [
        {
            "from": {
                "key_code": "f1"
            },
            "to": [
                {
                    "set_variable": {
                        "name": "my_leader",
                        "value": 1
                    }
                }
            ],
            "to_delayed_action": {
                "to_if_invoked": [
                    {
                        "set_variable": {
                            "name": "my_leader",
                            "value": 0
                        }
                    }
                ]
            },
            "type": "basic"
        },
        {
            "conditions": [
                {
                    "name": "my_leader",
                    "type": "variable_if",
                    "value": 1
                }
            ],
            "from": {
                "key_code": "a"
            },
            "to": [
                {
                    "key_code": "f"
                },
                {
                    "key_code": "o"
                },
                {
                    "key_code": "o"
                },
                {
                    "set_variable": {
                        "name": "my_leader",
                        "value": 0
                    }
                }
            ],
            "type": "basic"
        }
    ]
},

However, I would like my_leader to be deactivated after a timeout or if another key is pressed. For example, if I type [f1, h, e, l, l, o, a], then I would like "helloa" to be the result, not "hellofoo".

If we add the following

            "from": {
                "key_code": "f1"
            },
            "to": [
                {
                    "set_variable": {
                        "name": "my_leader",
                        "value": 1
                    }
                }
            ],
            "to_delayed_action": {
                "to_if_canceled": [
                    {
                        "set_variable": {
                            "name": "my_leader",
                            "value": 0
                        }
                    }
                ],
...

then the leader functionality breaks – apparently the to_if_canceled fires as soon as a is pressed, so the condition

{
    "name": "my_leader",
    "type": "variable_if",
    "value": 1
}

is not satisfied.


Edit: I am generating the JSON code above using Goku, which is much easier to read. Here's the Goku source.

  {:des "Sample leader"
   :rules [[:f1 ["my_leader" 1]  ;; F1 → my_leader=1
            nil ; This mapping is unconditional.
            ;; If nothing else is pressed within 500 ms
            ;; (to_delayed_action_delay_milliseconds),
            ;; then this fires (my_leader=0).
            {:delayed {:invoked ["my_leader" 0]
                       ;; The line below fires if anything
                       ;; is pressed before 500 ms.
                       :canceled ["my_leader" 0]}}]

           ;; Using my_leader in mappings
           [:a [:f :o :o         ;; a → output "foo" and
                ["my_leader" 0]] ;;     set my_leader=0
            :my_leader]          ;; if my_leader=1
           ]}
MuhammedZakir commented 2 years ago

Yours looks correct. Adding to_if_canceled should not create that problem. See the example in doc which does exactly what you want: https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/to-delayed-action/#example. I have also used this before, but not in the latest version. Can you check whether that example works for you?

As a temporary workaround - if you just want to get things working - as a last resort, use a shell command to set the variable to 0 in to_if_canceled. This should be slower than directly setting that variable.

https://karabiner-elements.pqrs.org/docs/manual/misc/command-line-interface/

andmis commented 2 years ago

I tried the example in the docs. It looks like the example in the docs works, but if you switch the order of the two rules in the example, it stops working. So it seems that the order is critical. Is this intended behavior?

MuhammedZakir commented 2 years ago

I tried the example in the docs. It looks like the example in the docs works, but if you switch the order of the two rules in the example, it stops working. So it seems that the order is critical. Is this intended behavior?

Order will become critical depending on the manipulator. The example is different from yours as it uses same key. In it, if you switched the order, the (current) second manipulator (the one that sets variable to 1) will always be triggered as it doesn't have any condition, which means that it will match command+q regardless of command-q variable's value. To prevent that, either put the manipulator with condition first, or add a condition to it so that it will only trigger when variable's value is 0.

Regardless of above, the order shouldn't matter in your case. If changing the order of your manipulators work, then this is most probably a bug. I will do some testing on mine.

MuhammedZakir commented 2 years ago

After testing, I found that order really matters. Personally, I feel like this is a bug except for rules like in the example. But I don't know how the internals work, so... For now, just move "leader manipulator" to the bottom and it will work. ~Or, you can remove to_if_canceled and explicitly set the variable to 0 in manipulators of X in <leader>X combinations. The latter will become a must if your modal mappings will have a depth of 2+. E.g. <leader>wq (window-quit) or wml` (window-move/tile-left).~

andmis commented 2 years ago

The problem with just setting the variable to 0 in the non-leader keystrokes of the modal mappings is that then the variable doesn't get set to 0 unless you actually fully execute one of the modal mappings. For example, if you are set up for <leader>x and you set the variable to 0 in the from: x, to: whatever, if: leader_variable rule, if you press <leader>z the variable is not cleared, and if you then press x later then from: x, to: whatever, if: leader_variable will fire.

MuhammedZakir commented 2 years ago

You're correct. So i guess the only solutions are either move leader manipulator to the bottom or set variable to 0 using cli.


I think this is a bug. You may change the title to reflect that and ping @tekezo.

andmis commented 2 years ago

@tekezo ping, looks like @MuhammedZakir has triaged this issue and identified it as a bug.

arabshapt commented 1 year ago

Hi @andmis, did you find a solution? Are there any alternatives for leader key on mac? maybe hammerspoon or dedicated app?

arabshapt commented 1 year ago

What I would wish to do is the following: e.g. leader-key => w => c ===this would output===> hyper+backtick i.e. all modifiers and backtick ` . This way you would tap on keys very near to homerow, but output keys that you never use for shortcuts. With leader key you get to reuse the same keys, but just change the order of tapping and then you have a new shortcut.