houmain / keymapper

A cross-platform context-aware key remapper.
GNU General Public License v3.0
295 stars 25 forks source link

[Feature] Context aware alias interpretation #97

Closed ristomatti closed 8 months ago

ristomatti commented 9 months ago

I managed to spend two hours trying to solve a "bug" with my keyboard specific mappings. I tried to define a device/application specific "leader" key in this manner:

[default]
  Leader = Comma !150ms

[device=/Keychron K15 Pro/]
  Leader = EscMiddle !150ms # Esc between a split spacebar

[device=/Keychron K3 Pro/]
  Leader = Comma !150ms

[class="slack"]
  Leader F          >> slack_switch_channel
  Leader 1          >> slack_goto_workspace1
  Leader 2          >> slack_goto_workspace2
  Leader H          >> slack_goto_home
  Leader A          >> slack_goto_activity

Initially I thought the device specific aliases aren't applying at all as the leader key was Comma regardless of the keyboard. I removed the default block which didn't help. I then made sure the device specific blocks are handled with a T >> 'K3' and 'K15' debug print which worked as expected.

I then thought the issue is with having both keyboards connected as it's just for testing anyway, but after disconnecting my K3 Pro, I still got Comma as the leader. I made sure to check xinput list to verify only K15 Pro is visible and just in case restarted both the daemon and the app. No diffference.

I then replaced the block for K3 with this:

[device="Foobar"]
  Leader = Comma !150ms

...but still got the Leader reassigned. (A bug perhaps?).

The workaround I found was to name the leader key differently and duplicate the keymappings separately for each keyboard which is not an ideal solution as my config file seems to grow at a rapid pace. :sweat_smile:

So, it'd be great if the aliases could be context aware in some way. I can see it gettting messy given on how the config works now, so let's say device specific context awareness would be the most useful (IMHO). But just skipping the non-matching sections would be a slight* improvement.

*) I assume most dual keyboard use cases would include differentiating an external keyboard and a laptop keyboard which isn't exactly simple to disconnect. Another relatively common use case could be sharing a config file between two or more computers.

ristomatti commented 8 months ago

Possibly related: https://github.com/houmain/keymapper/issues/41#issuecomment-1887937515.

houmain commented 8 months ago

Hi, sorry I did not answer earlier. Yes, the aliases are not context aware. They are applied when the configuration is loaded. Making them context aware would be quite complicated. I am not sure if I should attempt it.

The configuration you posted in #41:

[modifier="!AutoShift"]
  toggle_autoshift >> $(show_indicator) ^ AutoShift

[modifier="AutoShift"]
  toggle_autoshift >> $(hide_indicator) ^ AutoShift

I tested it with the following configuration, which should behave the same (A/B are output instead of your terminal commands) but I did not see a problem :

toggle_autoshift = F1
AutoShift = Virtual1

[modifier="!AutoShift"]
  toggle_autoshift >> A ^ AutoShift

[modifier="AutoShift"]
  toggle_autoshift >> B ^ AutoShift

This can also be achived this way (the toggling of the virtual key can also trigger output):

toggle_autoshift = F1
AutoShift = Virtual1

toggle_autoshift >> AutoShift ^
AutoShift >> A ^ B
ristomatti commented 8 months ago

My simplified example failed to make it apparent, toggle_autoshift was not defined as an alias, only used as an abstract command. I had not realized the syntax on your examples is valid. Are aliases and abstract commands the same thing, I assume so? On the example config in the repo, nothing is ever assigned to a lowercase identifier, this created the confusion.

Now after reading the section on abstract commands again, I realize the "issue" I noticed, appears to be documented behavior:

Subsequently this command can be mapped to one output expression per context. The last active mapping overrides the previous ones

It looks like I'll also need to read the functional principle section thoroughly again. My prior understanding of it seems to be contradicting at several places. I had figured it's the first match from top to bottom that is selected and thought I've noticed it when tweaking the config. But now when I think about it, it could be this what I was seeing:

When the key sequence can no longer match any input expression (because more strokes followed), the longest exact match is looked for (by ignoring the last strokes). As long as still nothing can match, the first strokes are removed and forwarded as output.

Do I read this correctly know, that even if I have a config like:

[device=\Keychron K15 Pro\ class="google-chrome"]
A >> B

[device=\Keychron K15 Pro\]
A !250ms B >> C
A >> D

Typing A, then B within 250ms within a Chrome window would result in the output being C? If so, no wonder I've had some pretty damn difficult to debug situations with my config. :smile:

I guess the best way ahead is to create a simplified test config and just output characters with different type of contexts and see what comes out. It's funny how with all things with computers, you might get pretty far with the wrong understanding of how things work with just plain luck. Then under the false idea of understanding you might fail to remember to try to recreate a simplified setup to verify. With 15 years in the industry, I should not fall for this anymore.

I'll probably close this as it appears to not make much sense given the inner workings. I'll first test if what you suggested gets around the issue.

My plan was to later create another feature request suggesting nested context, to be able to define somehing like:

[class=\chrome|chromium]
  [device="KeyboardA"]
    # some mappings
  [end]

  [device="KeyboardB"]
    # some other mappings
  [end]

  # common mappings for Chrome/Chromium
[end]

I now doubt if this would make sense either. :thinking:

ristomatti commented 8 months ago

I did run into quite a few unexpected behavior while testing your suggestion and my understanding of the inner workings. For instance, I didn't expect this mapping:

AutoShift = Virtual1
debug_autoshift_toggled >> 'AutoShift toggled'

...to result in the string Virtual1 toggled to come out, but it was easy to avoid by defining th string to output before defining the alias.

Do aliases work pretty much like a #define macro, just replacing each occurrence in the config? I expected it to be more constrained but after knowing it works this way is actually pretty nice. It didn't occur to me to see if can also be used in place of regex matchers... That would be nice!

My test config is below. Your tip on using a virtual key press/release for the indicator was brilliant. I somehow assumed multiple of such active would not be supported nor did this occur to me.

After a while debugging with typed strings, I came up with this trick of using notify-send. I found it quite quite neat, even if I say myself.

Everything included in this config seems to work as expected, even with both keyboards attached at the same time, although both keyboards need to be connected when the config is loaded. At least the F1 mapping doesn't seem to trigger on either keyboard if I attach them after the config. At one point I also saw both keyboards output the same device specific string. Is it expected based on the implementation? I could not reproduce it while writing this though.

Anyway, it seems the concept of this feature request can at least partly be worked around by defining prefixed aliases and keeping in mind only the >> mappings can be context specific (please correct me if I'm wrong). I'll close this, thank you for the help!

# Forward mods
Meta    >> Meta
Control >> Control
AltLeft >> AltLeft

# Key aliases
Alt     = AltLeft
AltGr   = AltRight

# Key groups
Letter  = A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z
Number  = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

NOTIFY_TIMEOUT = 5000

# For debugging
notify = notify-send -t NOTIFY_TIMEOUT "keymapper"

# These simulate key indicators from my original config
notify_capsword_on = $(notify "CapsWord on")
notify_capsword_off = $(notify "CapsWord off")

notify_autoshift_on = $(notify "AutoShift on")
notify_autoshift_off = $(notify "AutoShift off")

# QMK CapsWord emulation (https://docs.qmk.fm/#/feature_caps_word)
[default]
  CapsWord    = Virtual1
  CapsWordKey = Letter | Number | Minus | Backspace

  Shift{200ms}          >> Shift
  ShiftLeft{ShiftRight} >> CapsWord CapsLock ^
  Shift{CapsLock}       >> CapsWord CapsLock ^
  CapsWord              >> notify_capsword_on ^ notify_capsword_off

[modifier="CapsWord"]
  Minus                 >> Shift{Minus}
  CapsWordKey           >> CapsWordKey
  !CapsWordKey Any      >> CapsWord CapsLock Any ^

# QMK AutoShift emulation (https://docs.qmk.fm/#/feature_auto_shift)
[default]
  AutoShiftLock = F12
  AutoShift     = Virtual2
  AutoShifted   = BracketLeft | BracketRight | Number | Minus | Equal | Comma | Period

  F12           >> AutoShift ^
  AutoShift     >> notify_autoshift_on ^ notify_autoshift_off

[modifier="AutoShift"]
  Shift{AutoShifted}                >> (Shift AutoShifted)
  AutoShifted{120ms} !AutoShifted   >> (Shift AutoShifted) ^
  AutoShifted{Any}                  >> AutoShifted Any
  AutoShifted                       >> AutoShifted

# Device context test
[device=/Keychron K3 Pro/]
  F3 >> AutoShift ^
  F1 >> $(notify "F1 press on K3 Pro") ^

[device=/Keychron K15 Pro/]
  F5 >> AutoShift ^
  F1 >> $(notify "F1 press on K15 Pro") ^

# vim: ft=toml sw=2 ts=2 sts=2 expandtab
houmain commented 8 months ago

Do aliases work pretty much like a #define macro, just replacing each occurrence in the config? I expected it to be more constrained but after knowing it works this way is actually pretty nice. It didn't occur to me to see if can also be used in place of regex matchers... That would be nice!

Yes, right, they are just substituted. In 3.5.0 I improved the behavior. Now they are no longer substituted in strings but they are in context filters.

although both keyboards need to be connected when the config is loaded. At least the F1 mapping doesn't seem to trigger on either keyboard if I attach them after the config. At one point I also saw both keyboards output the same device specific string. Is it expected based on the implementation?

This sounds strange. Please file another issue if you can definitely reproduce broken behavior.