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

Higher powered modality with callbacks #102

Open
joshgoebel opened this issue Sep 7, 2022 · 8 comments
Open

Higher powered modality with callbacks #102

joshgoebel opened this issue Sep 7, 2022 · 8 comments

Comments

@joshgoebel
Copy link
Owner

joshgoebel commented Sep 7, 2022

I think I'm coming back around on modality as an explicit concept... so you could have a modal keymap with different exit behaviors rather than the current "one and done" or "throw away invalid input"... one easily images "allow repeat" (you can stay in the modal) or "don't consume exit" so that an unrecognized keystroke could cancel modality but also return to the entire world of keymaps allowing them to handle the keystroke.

Just trying to think of a simple way to manage all this.

Now let's imagine building callbacks instead of building these features:

  • one and done (after:hit => cancel_modal)
  • output unrecognized, done (after:miss => cancel_modal)
  • throw away invalid, done (after:miss => ignore_key; cancel_modal)
  • allow repeat (persistent modal) (after:hit => []) (vs the default of cancel)
  • don't consume on exit (after:miss: cancel_modal; recycle_input)

So you might build your dead keys with:

"after:miss": [cancel_modal, disable_deadkeys(), recycle_input]

Recycle input being something like what you've been looking for, but it would technically be running in a special modality pre-handler - so it's not really reinjecting the input, just allowing the modality to quickly change before the input is regularly processed.

Of course default behaviors for all these things would need to be considered, I'm not sure the current defaults are "appropriate".

Originally posted by @joshgoebel in #100 (comment)

@joshgoebel
Copy link
Owner Author

joshgoebel commented Sep 7, 2022

I'm not sure the current defaults are "appropriate".

For example I'm not sure input should be silently discarded... perhaps the default is for a miss is to send the input straight to output instead - unless a user decides to suppress it. This isn't backwards compatible but makes more sense to me.

@joshgoebel
Copy link
Owner Author

Are there other desirable behaviors one might want here that couldn't be modeled on the two hit/miss callbacks?

@joshgoebel joshgoebel changed the title High powered modality with callbacks Higher powered modality with callbacks Sep 7, 2022
@joshgoebel
Copy link
Owner Author

joshgoebel commented Sep 7, 2022

You could possibly then have self-focusing modals:

keymap("Grave"), {
    C("Alt-Grave"): [UC(0x0060), C("Shift-Left"), go_modal],  
    C("A"):                     UC(0x00E0),                     # à Latin Small a with Grave
    C("E"):                     UC(0x00E8),                     # è Latin Small e with Grave
    C("I"):                     UC(0x00EC),                     # ì Latin Small i with Grave
    C("O"):                     UC(0x00F2),                     # ò Latin Small o with Grave
    C("U"):                     UC(0x00F9),                     # ù Latin Small u with Grave
    "after:hit": [cancel_modal], # default
    "after:miss": [UC(0x0060), cancel_modal, reinject_input]
})

No need to track the dead key character... the modality system would handle all that for you... locking you into the grave modal until you chose to exit.

Though maybe that's half-baked since we can't have the whole keymap "out there" when it's not modal yet... and if we hide it in a special "modals" group then the go_modal keystroke would never be exposed... oh well... neat idea still. Might be other uses for such things.

Just change it to a nested keymap though with immediately and I think it'd work perfectly?

keymap("dead keys"), {
    C("Alt-Grave"): {
      immediately: [UC(0x0060), C("Shift-Left") ],  
      C("A"):                     UC(0x00E0),                     # à Latin Small a with Grave
      C("E"):                     UC(0x00E8),                     # è Latin Small e with Grave
      C("I"):                     UC(0x00EC),                     # ì Latin Small i with Grave
      C("O"):                     UC(0x00F2),                     # ò Latin Small o with Grave
      C("U"):                     UC(0x00F9),                     # ù Latin Small u with Grave
      "after:hit": [cancel_modal], # default
      "after:miss": [UC(0x0060), cancel_modal, reinject_input]
      }
    }
)

@RedBearAK
Copy link
Contributor

RedBearAK commented Sep 7, 2022

@joshgoebel

For example I'm not sure input should be silently discarded... perhaps the default is for a miss is to send the input straight to output instead - unless a user decides to suppress it. This isn't backwards compatible but makes more sense to me.

Due to the issue of many combos needing to be transformed, it doesn't make much sense to me to just pass combos to output by default. Like I brought up with the tab nav shortcuts, many combos that are just passed to output without being transformed will end up doing unexpected things.

Are there other desirable behaviors one might want here that couldn't be modeled on the two hit/miss callbacks?

Can't think of anything right now. The choices are pretty comprehensive.

Though maybe that's half-baked since we can't have the whole keymap "out there" when it's not modal yet... and if we hide it in a special "modals" group then the go_modal keystroke would never be exposed... oh well... neat idea still. Might be other uses for such things.

I can't see how that would ever work. The trigger has to somehow be on the "outside" of the modality, like there has to be a combo that triggers a nested keymap, but it's not part of the "inner" nested keys.

@RedBearAK
Copy link
Contributor

RedBearAK commented Sep 7, 2022

Just change it to a nested keymap though with immediately and I think it'd work perfectly?

A benefit of the multi-keymap approach you helped me implement is that I'm able to cascade through the dead keys keymaps to the "escape keys" keymap and therefore didn't have to include any of those keys with unusual escape actions in any of the dead keys keymaps. The dead keys keymaps at this point are fairly small. They only need to have the "valid" keys that would normally combine with the diacritic to make an accented character.

      "after:hit": [cancel_modal], # default
      "after:miss": [UC(0x0060), cancel_modal, reinject_input]

If there was something like reinject_input I would want to be putting it on the normal "escape keys" keymap, where the special escape keys would do something different from default and then if there was a "miss" for anything in that keymap, the default action would be triggered (which must include deactivating the dead keys, which includes deactivating the "escape keys"). Thus, I think, finally covering "all other keys/combos".

But that would not work if the keymap were modal like nested keys.

I'm sure it could work with nested keys, but ideally I'd have to build a function that inserts those special escape keys and their non-default actions inside of 30 different nested keymaps. (That's including the future ABC Extended layout.) At the moment they are still listed explicitly in the Switch/Case statements in the AHK version. So I could do that too, it would just make the config another (approximately) 300 lines longer.

If you think it's easier to implement the callbacks in nested keys for now, I could give it a test when it's available.

@joshgoebel
Copy link
Owner Author

I'm not sure I follow you at all. A nested keymap with after:miss : [UC(0x0060), cancel_modal, reinject] is going to behave ALMOST exactly like a top-level keymap in your current setup... if there is a miss (no match) you output the character, the modality is cancelled, and the input tries to find another keymap to "land" in...

There shouldn't need to be an "escape" keymap since you're handing escape in the after:miss callback...

So I wonder if I'm missing something or you are?

@RedBearAK
Copy link
Contributor

So I wonder if I'm missing something or you are?

Well, I kept thinking from remembering the AHK implementation that there would be several at the top that need to stay because they don't act exactly the same as other keys, but it looks like I was overestimating. If there were to be a true reinject most of even these cases at the top of the escape keymap could go away. Including, I'm assuming, the task/window switching lines that need to use bind.

keymap("Escape actions for dead keys", {
    # special case shortcuts that should cancel dead keys
    C("Esc"):                   [getDK(),setDK(None)],                              # Leave accent char if dead keys Escaped
    C("Space"):                 [getDK(),setDK(None)],                              # Leave accent char if user hits Space
    C("Delete"):                [getDK(),setDK(None)],                              # Leave accent char if user hits Delete
    C("Backspace"):             [getDK(),C("Backspace"),setDK(None)],               # Delete character if user hits Backspace
    C("Tab"):                   [getDK(),C("Tab"),setDK(None)],                     # Leave accent char, insert Tab
    C("Enter"):                 [getDK(),C("Enter"),setDK(None)],                   # Leave accent char, Enter key
    C("Up"):                    [getDK(),C("Up"),setDK(None)],                      # Leave accent char, up arrow
    C("Down"):                  [getDK(),C("Down"),setDK(None)],                    # Leave accent char, down arrow
    C("Left"):                  [getDK(),C("Left"),setDK(None)],                    # Leave accent char, left arrow
    C("Right"):                 [getDK(),C("Right"),setDK(None)],                   # Leave accent char, right arrow
    C("RC-Tab"):                [getDK(),bind,C("Alt-Tab"),setDK(None)],            # Leave accent char, task switch
    C("Shift-RC-Tab"):          [getDK(),bind,C("Shift-Alt-Tab"),setDK(None)],      # Leave accent char, task switch (reverse)
    C("RC-Grave"):              [getDK(),bind,C("Alt-Grave"),setDK(None)],          # Leave accent char, in-app window switch
    C("Shift-RC-Tab"):          [getDK(),bind,C("Shift-Alt-Grave"),setDK(None)],    # Leave accent char, in-app window switch (reverse)

So really that would just leave these two(?) lines that still need to be in every nested keymap.

keymap("Escape actions for dead keys", {
    # special case shortcuts that should cancel dead keys
    # C("Esc"):                   [getDK(),setDK(None)],                              # Leave accent char if dead keys Escaped
    C("Space"):                 [getDK(),setDK(None)],                              # Leave accent char if user hits Space
    C("Delete"):                [getDK(),setDK(None)],                              # Leave accent char if user hits Delete

So the situation is not nearly as bad as I thought. Again, that's if the reinject would allow things like task/window switching combos to go on and behave like they normally would, including proper binding. (If it's a real interruption of input and then normal processing of the combo as if it had been used outside the nesting, I can't imagine why it wouldn't work as expected.)

The special cases that will remain:

  • Space should just deselect the highlighted character, but not insert a space.
  • Delete should deselect the character, but not delete a character to the right of the cursor.
  • Technically the Escape key itself is a special case, but shouldn't do anything harmful if re-injected by the default action after typing the character. But it also wouldn't be a big deal to add it inside the nesting.

With a true default action and reinject capability, I think the job would be done.

I'm here for it. Let's do this. Hrrrg! [insert Hulk Hogan tearing his shirt GIF]

Now I remember why I had to put a lot of these in the AHK Switch/Case. The usual process that lets you grab and analyze input fails to catch a large selection of "special" keys, unless you put equally special traps and flags in place to notice them. Keyszer/evdev doesn't have that problem, so the default action should cover almost anything. Phew. 😅

@joshgoebel
Copy link
Owner Author

So really that would just leave these two(?) lines that still need to be in every nested keymap.

I think that sounds right since you're wanting special behavior from those two keys and we don't yet have a way to sniff which key was pressed...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants