keyszer
keyszer copied to clipboard
(enh) Add `Key.ANY` for handling any keypress inside of nested keymaps
I think this could be a literal enum in Key (just with a very high number that will never be used)... and then transform_key needs to get a little smarter so after checking and not finding any matching keymaps (because there is no literal ANY key) the active keymap(s) should be checked one last time for a Key.ANY entry and if found that command sequence should be executed.
This will require restructuring transform_key slightly since right now it assumes the ONLY way a match could happen is an explicit match rather than wildcard... and that's where the logging is at, etc...
Perhaps need to abstract out a find_matching_combo_in_keymaps function...
See #92, this may not be strictly necessary as (for common cases) it's easy to do manually and trivial to do with a loop that builds keymaps by hand:
# instead of ANY.KEY just write all the possibilities by hand
# or generate them programmatically
keymap("Escape actions for dead keys", {
C("a"): [getDK(),C("a"),setDK(None)],
C("b"): [getDK(),C("b"),setDK(None)],
C("c"): [getDK(),C("c"),setDK(None)],
C("d"): [getDK(),C("d"),setDK(None)],
C("e"): [getDK(),C("e"),setDK(None)],
How I originally imagined this is it would only apply to regular keypresses (A-Z, etc), not modifier [keys]... as it would be handled at the very bottom of the stack... but does that mean it should also trigger for combos? (which it would be default).
So if one has a nested keymap that you get into by hitting Ctrl-A... and it has an Key.ANY...
Then if one hit:
- Ctrl-A
- Ctrl -Z
Ctrl-A would enter the keymap... and Ctrl-Z would be captured by Key.ANY... and then should it still be output as well? Or would the user need to manually opt-back into it being output?
Note: I'm assuming Key.ANY would (by default) escape the keymap (like other keys)... so if you wanted to stay you'd need a stay_here helper or some such to accomplish that behavior...
@joshgoebel
Ctrl-Z would be captured by Key.ANY... and then should it still be output as well? Or would the user need to manually opt-back into it being output?
I don't think that the "default/any" exit path should automatically send the unmatched input. The input combo should be stored in a variable that could be accessed by the user explicitly on the way out of the nested keymap. I can only reference again the way I had to do things in AHK.
; Grave accent: Option+`, then key to accent
$!SC029::
Send, {U+0060}{Shift down}{Left}{Shift up}
StringCaseSense, On
; watch next input string
Input, UserInput, L1, {BS}{Del}{Left}{Right}{Up}{Down}{LAlt}{RAlt}{LCtrl}{RCtrl}
; MsgBox % GetKeyName(UserInput) ; Debugging: Show normalized key name
; MsgBox % ErrorLevel ; Debugging: Show ErrorLevel value
name := GetKeyName(UserInput) ; Get normalized name of UserInput
e := SubStr(ErrorLevel, 8) ; Get normalized name of EndKey
Switch {
Default: Send, {Right}%UserInput% ; No match? Leave accent char and send input.
Case name = "Escape": Send, {Right} ; Escape: Leave accent character, exit dead key
Case name = "Space": Send, {Right} ; Space: Leave accent character, exit dead key
Case e = "Up": Send, {Right}{Up} ; Up: Leave accent character, exit dead key, move
Case e = "Down": Send, {Right}{Down} ; Down: Leave accent character, exit dead key, move
Case e = "Left": Send, {Right}{Left} ; Left: Leave accent character, exit dead key, move
Case e = "Right": Send, {Right}{Right} ; Right: Leave accent character, exit dead key, move
Case e = "Backspace": Send, {Right}{BS} ; Backspace: Delete accent character
Case UserInput == "a": Send, {U+00E0} ; à {U+00E0} (Alt+0224)
Case UserInput == "e": Send, {U+00E8} ; è {U+00E8} (Alt+0232)
Case UserInput == "i": Send, {U+00EC} ; ì {U+00EC} (Alt+0236)
Case UserInput == "o": Send, {U+00F2} ; ò {U+00F2} (Alt+0242)
Case UserInput == "u": Send, {U+00F9} ; ù {U+00F9} (Alt+0249)
Case UserInput == "A": Send, {U+00C0} ; À {U+00C0} (Alt+0192)
Case UserInput == "E": Send, {U+00C8} ; È {U+00C8} (Alt+0200)
Case UserInput == "I": Send, {U+00CC} ; Ì {U+00CC} (Alt+0204)
Case UserInput == "O": Send, {U+00D2} ; Ò {U+00D2} (Alt+0210)
Case UserInput == "U": Send, {U+00D9} ; Ù {U+00D9} (Alt+0217) }
Return
Here the input is stored in "UserInput" (just a variable made by whoever originally wrote the code that I modified) and then sent deliberately via the the "Default" branch of the Switch/Case if there is no match. But the default branch could just as easily have a "return" without sending anything at all.
Storing the input in a variable will also let it be easily used in a macro, similar to how I ended up using it here in the new enhanced version of the Option key scheme.
Wouldn't you just likely use:
# nested map
{
Key.ANY: passthru # and return by default
}
?
With the "passthru" evaluating to the input combo? So that could be part of a macro?
And if you didn't want any output for unmatched keys, you'd do...
{
Key.ANY: None,
}
?
Not sure what you're calling a macro... passthru would be another ComboHint... it's not a variable...
Key.ANY: None,
Well, that would return by default... so you might want Key.ANY: no_return or whatever we come up with if you wanted to STAY in the nested keymap.
Not sure what you're calling a macro
I need something like this to happen...
{
Key.ANY: [C("Right"),input_combo],
}
Where if we're inside the nesting for Opt+Grave and we hit Opt+E, we do this macro (right arrow key, then send the input combo) and find ourselves inside the nesting for Opt+E (Acute accent dead key).
Yeah, can't do that... the outputs are all straight out - they purposely do not loop back thru the input pipeline. You can't write something to the output that will trigger another combo. At first blush I think that's a pretty bad idea.
If Opt+E leads to another nested map then it should be listed explicitly.
Yeah, can't do that... the outputs are all straight out - they purposely do not loop back thru the input pipeline. You can't write something to the output that will trigger another combo. At first blush I think that's a pretty bad idea.
I guess AHK's scripts are evaluated in a different way. With the simple default output sending the unmatched input from the Switch/Case you can jump between the dead keys with no issue, or repeat the same dead key shortcut over and over and it will type another accent character and be back inside the Switch/Case again. And there are no more named subroutines to accomplish this, I forgot I removed those and they only existed in an earlier version of the code. They turned out to be unnecessary.
If Opt+E is another nested map then it should be listed explicitly.
That's just going to be untenable with the ABC Extended layout's 25 different dead keys shortcuts. That's 25 different lines in 25 different nested keymaps.
If the input can't be sent out back at the parent level then I will have to try to use a custom function somehow. Or make every dead key its own function that can be called from anywhere, including within the other dead key nestings.
I thought for sure with a minor tweak somewhere you could send out the input combo after leaving the nested keymap.
The post you made from a couple of days ago above seemed to indicate that you could do this in some way.
I have to sign off for now, but I'll be thinking about this.
That's 25 different lines in 25 different nested keymaps.
This is why loops were invented... :-) Things within things are exactly when you should prefer loops though...
If the input can't be sent out back at the parent level t
Not sure I understand what you mean here when you say "sent out"... the sink for all commands is output... and output doesn't care about levels at all - it's just output...
send out the input combo after leaving the nested keymap.
That wouldn't return it to the input... the data only flows downstream:
INPUT -> transform -> [commands] -> OUTPUT
Output goes to output, it does not go back to input or the transform.
The post you made from a couple of days ago above seemed to indicate that you could do this in some way.
Possibly I was confused or not understanding your properly. Or thinking of another approach.
Not sure I understand what you mean here when you say "sent out"... the sink for all commands is output... and output doesn't care about levels at all - it's just output...
But right now an unmatched combo inside a nested keymap just seems to cause the keymap to be exited and then the combo otherwise disappears. If the "passthru" ComboHint would cause the input to go through without being transformed into something else, why would that combo not be able to activate the same or any other set of nested keys? (Because it can't be "input" at that point, I guess is what you're saying, and only "input" can activate a nested key. Or do anything, really. When it becomes "output" it's past the point where it can trigger an internal action, it can only trigger an action in an application.)
When I say the "parent" level it means "outside of a nested keys construct". Because what happens inside a nested keymap is if you try to use any normal shortcut like Cmd+A/Z/X/C/V that isn't listed inside the nested keymap, nothing will happen, other than exiting the nested keymap. In other words, the observed effect of nested keymaps is that they block all other shortcuts from working. So it's not just about jumping from one dead key to another.
On the other hand if you do the same thing in AHK with the code above, Cmd+A will do the usual "select all" even if you were in the middle of one of the dead keys operations.
So maybe... the "default" option needs to actually be a function. But still, if the function is only the right side of the colon it would need a way to know what the "input" was. And if the function was on the left side of the colon, well AFAIK you can't do that inside a keymap unless the keymap processing code would know what to do with it.
This is why loops were invented
Yeah, but we're talking about the difference between needing to create a loop and use it in a bunch of different places, and not needing such a construct at all. And the loop for the dead keys wouldn't fix any of the other shortcuts that get blocked (or "disappeared") by the nested keymap.
It's fine if a Cmd+C shortcut listed inside a nested keymap takes precedence and temporarily changes what Cmd+C does, but when Cmd+C is not a listed shortcut inside the nesting it shouldn't be blocked. There has to be some way around this so that nested keymaps are more like just another overlaid keymap on top of the others.
Or another way of saying it, let nested keymaps be concurrent, like we just did with modmaps, rather than exclusive, which is how they behave right now.
Is this making any sense at all?
Or maybe the simplest current solution is to actually use separate keymaps instead of nested keymaps for all the dead keys, which activate when a variable equals a value set in the keymap conditions.
Hmmm... 🤔
That would mean potentially 25 different keymaps, but only one would be active at any given time, and none would be active unless a dead key is triggered.
Obviously I already know how to set up the conditions and toggle the variables.
If the "passthru" ComboHint would cause the input to go through without being transformed into something else, why would that combo not be able to activate the same or any other set of nested keys?
INPUT -> transform => [commands] -> OUTPUT
passthru (as a command) is running at the very end of the transform/commands process... it can pass the original (modmapped) input thru to output (everything flows left to right)... but it can't go backwards, nothing goes backwards.
There has to be some way around this so that nested keymaps are more like just another overlaid keymap on top of the others.
You should open a separate issue for this and make a small case for it there. To me this "breaks" a lot of the benefit of the nesting though (modality). Would we support both modal and non-modal nested keymaps? Things to explore in a new issue.
Or maybe the simplest current solution is to actually use separate keymaps instead of nested keymaps for all the dead keys,
I think you may be onto something here... if what you want is really dynamic/overlapping keymaps/modes then I think it makes a lot of sense to build that on top of the top-level keymap functionality.
@joshgoebel
Pretty big problem, I think. Top level keymaps would have a similar issue as the nested keys, and it's kind of worse. The nested keys at least have the feature of basically disabling themselves if you use a combo that isn't inside the nesting, right? But with a regular keymap for dead keys I'll have to flip a switch on the way out on each line to make sure it gets disabled. No big deal, just use a small helper function to zero out the variable that enabled the keymap.
Buuuut... If I were to use a combo that isn't inside that keymap, there's no way I can think of to disable that keymap. With nested keys the nesting would just automatically exit in that circumstance.
So a regular keymap would need to be able to use the "Key.ANY" concept, to at least run the helper function that "kills" (deactivates) that keymap. Otherwise it would just remain active.
Can't think of a way to solve this yet without adding the feature from this thread to the main keymap function. Basically, give normal keymaps an exit trigger.
Besides this fairly large problem I'm fairly sure I could do all the other things I need with just the regular keymap functionality and a couple of helper functions.
Can you solve this by ordering the key maps?
Put the key maps you need to exit from first. Then a dummy key map with a conditional. And then all the other key maps.
As soon as you reach the dummy key map you can be certain none of the keys from the prior key maps triggered. Remember that the conditional code is run every time a key is pressed.
Yeah, I'm not really imagining how that would work.
Problem is that all of the "dead keys" have slightly different sets of valid keys and Shift+keys combos that they will combine with. But I guess I see what you're saying, the "dummy" somehow deactivates the variable.
Remember that the conditional code is run every time a key is pressed.
Are you implying here that instead of just a "test" criteria there would be a more active expression within the condition that would cause an action all by itself? Intriguing, if I'm grasping the concept.
Well your actions are limited but you could easily toggle some state which could have all sorts of effects on active keymaps.
So it can be like a tripwire keymap. Not something I would have thought of on my own, although I suppose it's similar to the way I was setting the variable inside a macro.
If you could provide a couple of simple examples I will work on that when I get some time.
The tripwire keymap keeps deactivating the dead key keymap before it has a chance to work. I can't think of a way to structure the logic to stop the tripwire from acting immediately.
I don't know what piece of knowledge could be used to cause the dummy keymap condition to take effect only when using a combo that isn't inside the dead key keymap.
Without just writing my own entire function to act like the Switch/Case from AHK, I don't see how this is workable.
Show me what you're playing with... you may need a small state machine to keep track of what state the system is currently in - so you known which keymaps to activate and deactivate...
Oh, n/m... it can't work with keymaps (yet) because they aren't optimized like modmaps.... hmmm... sorry I was going off of my tweaks to modmaps the other day, which are now much more efficient than keymaps.
it can't work with keymaps (yet) because they aren't optimized like modmaps
I did wonder if you were perhaps thinking about modmaps. But I'm not sure I see how even that would work, now that they cascade.
I see a few possible ways forward, but I'm not sure which actually make any sense.
-
Give keymaps a
Key.ANYaction, that lets a keymap raise a flag or perform an action (such as disabling one of its own conditions, or perhaps another keymap's condition), when you zip through that keymap in processing a combo and there was no match. I know it's a little bit of a weird concept since keymaps aren't modal and you don't really go "in" or "out" of them. A keymap would need to have the ability to be semi-modal and "care" about whether a combo is not inside itself. Or the thing processing the contents of the keymap would need to have the awareness to say, "Nope, combo is not in this keymap," as it goes on to the next one. -
Give nested keymaps the
Key.ANYexit action. But this will mean somehow supporting non-modal nested keymaps so the input combo could go on to be seen as input in a top-level keymap (including the same keymap, outside the nesting). A little awkward based on the current modal nature of nested keymaps. -
Build a whole special function to process the dead keys, but I think that would end up needing to call something like
handle_commandsdirectly using its own logic and checking whether the input combo was designated inside the function or not.
I'm leaning toward messing with nested keymaps still being the best place to make this work.
I'm still not sold on the benefit of nested keymaps being modal in the first place, to the point where they even interfere with Cmd+Tab and Cmd+Grave task switching. I can't think of circumstances where that kind of total blockage of all possible keys/combos that aren't involved in the nested keys mappings is really necessary or even helpful.
In the case that such modality is needed, surely the more appropriate way to do that kind of blocking is having the user set up an exit action that explicitly performs no action and stays within the nesting. Or just blocks the combo with a "None" yet still exits, which would be exactly like how it currently works by default. So it could still be "modal" if desired, and optionally even more modal than it is already, where it could be forced to stay active until you deliberately "exit" with a designated key.
I feel strongly that the more sensible way for nested keymaps to work by default is to A) yes, perform a different action than usual for any designated key/combo inside the nesting, but then B) get the heck out of the way and vanish without complaint if the user tries a key/combo that isn't inside that temporary "pocket universe".
The way it works now feels more like hitting a speed bump if you fail to use one of the "correct" combos after the initiating combo.
when you zip through that keymap in processing a combo
That's what you could do in the conditional (what I was suggesting before), just we'd need to update how the keymaps are filtered/processed
I'm still not sold on the benefit of nested keymaps being modal in the first place, to the point where they even interfere with Cmd+Tab and Cmd+Grave task switching.
The original usage is multi-stroke commands such as in Emacs you might want to Ctrl-T Ctrl-G (made up), this is a single "emacs combo" it makes zero sense to "tab" to another program in the middle of it. So for the original intended use case I think modality is correct. So far it feels like your use case is the edge case. It seems if anything perhaps modality might be configurable - though I'm not sure if that alone fully solves your problem.
Perhaps we could open a new issue with a TINY example of your [current] special character stuff with some comments in the code and also some bullet points of the places it's still falling over? I do much better with real life examples. I know we've probably covered some of this before but I think we're a bit further down the road now so hopefully another visit might help clarify next steps.
Build a whole special function to process the dead keys, but I think that would end up needing to call something like handle_commands directly using its own logic
Am curious what you think this might look like... I'm very interested in APIs (and how they feel, if they make sense). I'm not sure why you need to "call" handle_commands though vs say returning the list of commands you want to execute?
And again this is Python, nothing is stopping you from calling handle_commands... you just have to import it, but that is "unsupported" territory. :-) If you got something working that way I'd be curious to see it though and consider how to do it otherwise.
perhaps modality might be configurable - though I'm not sure if that alone fully solves your problem
That should pretty much solve the problem.
Am curious what you think this might look like
I only have a vague idea. All I know is that it would have to accept a combo and then check if it was in a list, then do one thing if it was in the list and a different thing if it wasn't. And the different thing would involve causing the combo to do what it would have originally done... which in my imagination is feeding it to something like handle_commands or whatever usually happens to a combo when you're not trying to do a nested keymap.
which in my imagination is feeding it to something like handle_commands
No, you'd have to feed it to on_event or at least on_key but then that type of "going backwards" is exactly what I'd like to avoid. So you're just asking if you can build "reinject input" yourself... I don't think we want to support that at this time - for sure not until we've exhausted all the other options.
So you're just asking if you can build "reinject input" yourself... I don't think we want to support that at this time - for sure not until we've exhausted all the other options.
Basically. Like the Switch/Case with its default output case from AHK. I don't know what other option there could possibly be.