Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit vs implicit "triggers" before/after keymaps are considered/used #100

Open
joshgoebel opened this issue Sep 5, 2022 · 10 comments
Open
Labels
discussion Discussion topic enhancement New feature or request help welcome Help/contrib is esp welcome

Comments

@joshgoebel
Copy link
Owner

joshgoebel commented Sep 5, 2022

Related: #92 The context is allowing uses to build their own keymaps that mimic nested keymaps, but with slightly different/programmatic behavior.


Potential callbacks per keymap:

  • after_match
  • after_no_match
  • before_match
  • before_no_match

Or perhaps just two with context:

  • after(context)
  • before(context)

context:

  • match : boolean

Currently @RedBearAK is using a "trigger keymap" to support their deadkeys config:

keymap("Escape actions for dead keys", {
    C("a"):                     [getDK(),C("a"),setDK(None)],
}, when = lambda _: ac_Chr in deadkeys_US)

keymap("Disable Dead Keys",{
    # Nothing needs to be here. Tripwire keymap to disable active dead keys keymap(s)
}, when = lambda _: setDK(None)())

First we deal with "escaping" because of normal keypresses, then a fake keymap sets the dead key character to None as the mapper passes over it's conditional to consider if it should be ac active keymap or not. This is all a bit "hacky" and implicit... I'm considering if this should be handled with explicit callbacks:

keymap("Escape actions for dead keys", {
    C("a"):                     [getDK(),C("a"),setDK(None)],
  }, when = lambda _: ac_Chr in deadkeys_US,
  # disable dead keys after this map is considered
  afterConsideration = lambda _: setDK(None)()
)

And of course it's worth mentioning this could all be done inside a single lambda/function as well, though again that's slightly more implicit/hacky... changing state inside a conditional vs it being a "pure" conditional.

Creating this as a discussion topic. I like that we are powerful enough to do this, but I'm not sure we've found the correct abstraction yet - and I worry the current way is going to break in the future when internals change since it's based too much on the internal behavior.

@joshgoebel joshgoebel added enhancement New feature or request discussion Discussion topic request Feature request help welcome Help/contrib is esp welcome and removed request Feature request labels Sep 5, 2022
@RedBearAK
Copy link
Contributor

I like that we are powerful enough to do this, but I'm not sure we've found the correct abstraction yet - and I worry the current way is going to break in the future when internals change since it's based too much on the internal behavior.

I am in full agreement that the solution feels quite delicate. It's more of, "Oh, it's interesting that this actually works," rather than "Yeah, we did that on purpose."

Any solution that would make it more robust and explicit would be welcome.

As in your example here, the solution could still exist in what would essentially be a "trigger" or "tripwire" keymap, but that could also coincidentally be a useful keymap like the special escape keys keymap, where some keys will still need to do something a little different from the "default" escape action.

I still think a good name for the condition would be "noMatch".

This kind of exit action of course always has the potential to create a loop where it just keeps happening for every key press if the condition that activates the keymap isn't correctly deactivated by the exit action. I wonder if there is a good way to put a fence around that, or if it just has to be left up to the user to avoid that problem.

@joshgoebel
Copy link
Owner Author

joshgoebel commented Sep 5, 2022

I still think a good name for the condition would be "noMatch".

Oh now that's very different... I was imagining generic before/after triggers for the keymap being considered... whether it matches a combo or not is something much, much more specific.

In fact with the existing system one can quite easily build before/after callbacks just by wrapping keymaps with much smarter conditionals:

smart_keymap("global", { #...
before = lambda _: # ...
after = lambda _: # ...

You'd just compile that down to a keymap that inside it's conditional did:

# pseudo-code
run(:before)
result = run(:conditional)
run(:after)
return result

So I'll probably leave this open for now while we gather more info on what types of things users would actually do with such a trigger system.

@joshgoebel
Copy link
Owner Author

joshgoebel commented Sep 6, 2022

I still think a good name for the condition would be "noMatch".

I also think you need to be sure to understand the difference between combo/actions and the triggers... the triggers can change state (toggle a variable), but they don't generate output. You could hack them to do so, but the would purposely not have this functionality - as I don't think output in the middle of the processing pipeline is a good thing.

@RedBearAK
Copy link
Contributor

the triggers can change state (toggle a variable), but they don't generate output.

I did have trouble getting the tripwire to cause setDK to emit a Unicode keystroke. I could have sworn at one point I had the tripwire emitting a C("Right") successfully, but it was redundant with the other escape actions, so I removed it at some point. Are the triggers really not in the right context that they could emit output? Maybe when it was working it was before I made it return a proper function.

This feels like both the trigger and the "all possible combos" dict would both be necessary to finally perform a default "exit" action. But that would also mean the tripwire keymap would still need to be its own thing, separate from the other escape keymap. By nature the dict would include all the combos from the escape keymap, so it would need to go below the escape keymap.

wrapping keymaps with much smarter conditionals

That sounds pretty interesting. I'll have to think about that.

@RedBearAK
Copy link
Contributor

But even with a huge dict of all possible combos, there is the fundamental problem that while it would catch all of those combos and have them do "something", it still cannot have that combo go on and do what it would normally do.

I ran into this problem with the tab navigation shortcuts I use frequently in several different applications. Shift+Cmd+Braces. Everything else in the escape keymap works fine. Apps are fine with receiving Ctrl+Z/X/C/V/A and keys like Esc/Del/Backspace and so on. They don't need to be changed to something else. But with the tab nav shortcuts I have to transform them to something like Ctrl+PgUp/PgDn for some apps and Shift+Ctrl+Tab/Ctrl+Tab for other apps. So I can't include any combo like that in the escape keymap. The result would be correct in some apps but not in others.

Because, as always, the input cannot go on to do what it would have done as input. We can only repeat it as output.

@joshgoebel
Copy link
Owner Author

joshgoebel commented Sep 6, 2022

Are the triggers really not in the right context that they could emit output?

The condition functions are all evaluated BEFORE the keymap to be used for output is even chosen, so allowing them to generate output themselves is just strange and not part of the overall design flow, hence my calling it a hack.

I ran into this problem with the tab navigation shortcuts I use frequently in several different applications.

Why can't the original shortcuts handle this?

keymap("General GUI", {
  C("Cmd-Tab"): special_mode_clear_then(C("Ctrl-Tab")),

It's not super tidy (or modular), but it works. Or you could wrap entire keymaps:

special_mode_clear_then(
  keymap( ... )
)

Which would get be a nicer abstraction for doing the above (wrapping every combo in the map with a wrapper function)

@joshgoebel joshgoebel changed the title Explicit vs implicit "triggers" before/after keymaps are considered Explicit vs implicit "triggers" before/after keymaps are considered/used Sep 6, 2022
@joshgoebel
Copy link
Owner Author

joshgoebel commented Sep 6, 2022

I'm also considering more of a high-level callback:

def do_keymap_enter_and_exits(keymaps):
  # check global state and if certain keymaps are not
  # present we should fire their "exit" handlers

add_callback("after_decide_keymap", do_keymap_enter_and_exits)

Trying to imagine perhap a few super high-level hooks that would allow people to build out more extended functionality via plugins.

Except that wouldn't help here, since you need more of an before:combo or after:combo or something... here I think you'd want before before:combo and you'd need access to the context of which keymap we were thinking of fulfilling the combo from - that would allow you to detect an "exit" type event...

@RedBearAK
Copy link
Contributor

RedBearAK commented Sep 6, 2022

Why can't the original shortcuts handle this?

This is thinking about it slightly wrong. Technically it could, with a wrapper as in your example. But there are multiple places where the tab nav shortcut (and one or two other closely related tab nav shortcuts) have to exist in the config to map onto different output combos depending on context. And the tab nav shortcuts are just one example of the hundreds of combos that have nothing to do with the special character scheme and have variable transformed output, making them incompatible with the escape keymap. By extension this wrapper function would need to be on every existing combo in the entire config that doesn't already have an exit action attached (like the escape keymap and dead key combos that are part of the special character scheme). Which leads to...

special_mode_clear_then(
  keymap( ... )
)

This is funny because it mirrors something I was pondering, like whether I could just wrap the entire config in a kind of "smart" condition. But this would not work by itself since there are many combos that do not exist anywhere in anyone's config for the simple reason that they don't need to be transformed into something else. Nobody bothers to put combos that already work fine into their config.

So even if there were a way to wrap the entire config (right now it would have to be the same conditional wrapper around every individual keymap) it would have to be combined with a function to generate "all possible combos" at the very end of the config file, so that it catches everything that "falls through" the rest of the config without matching anything. But this is the point where it makes more sense to just build a completely customized keymap function and/or a customized combo function, or at least let the existing functions accept some sort of flags or special actions. Or a function that aliased keymap to a wrapper that then fed the contents on to the normal keymap after making some decisions and/or actions. But that could end up being redundant with the escape keymap if I'm not careful. Brain frying... 🤯

(Thing with the escape keymap is that the ones at the top that I started with mostly have to do something different from the default, so they will always need to be explicitly defined. I can't entirely get rid of the whole keymap.)

It's pretty fascinating at this point that AHK just doesn't seem to have any problem with this sort of re-injection of input as input. I really wonder how they do that so easily. Then again, AHK definitely has its own set of bizarre problems, due to some weird Windows quirks.

Except that wouldn't help here, since you need more of an before:combo or after:combo or something... here I think you'd want before before:combo and you'd need access to the context of which keymap we were thinking of fulfilling the combo from - that would allow you to detect an "exit" type event...

Yes. Something like that.

@joshgoebel
Copy link
Owner Author

joshgoebel commented Sep 7, 2022

But there are multiple places where the tab nav shortcut ... different output combos depending on context.

Right, so you could just wrap all those places. I was pointing out that this is an EASY way to solve that one last problem with your existing setup. You don't need to wrap the world (because you still have the trigger guard). I was just pointing out that it's not so hard to wrap the world.

I'm not suggesting any of this is particularly perfect or optimal, just highlighting what is already possible to do.

Good point about non-mapped combos...

I really wonder how they do that so easily.

Who says they do it "easily"? It's not impossible, it's just not the architecture we have right now, and I'm not [yet] convinced it's actually needed either. I think it adds a whole layer of confusion just to solve a small edge case.

@joshgoebel

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion Discussion topic enhancement New feature or request help welcome Help/contrib is esp welcome
Projects
None yet
Development

No branches or pull requests

2 participants