-
Notifications
You must be signed in to change notification settings - Fork 14
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
Make nested keymaps an abstraction around top-level keymaps #92
Comments
Not that they would literally be top-level... I'm more talking about cleaning up the abstrations a bit... like it seems like if we just go with "modality" alone and say it has the default behavior of "return to top after done/missed"... then now nested keymaps are:
So modal is just a flag... something you could (theoretically) turn on/off for global keymaps also... |
If we went all the way with this then nested keymaps could have conditions as well... though having a condition on a modal (active) keymap seems perhaps problematic... :-) |
I was also thinking perhaps a stack of keymaps... imagine we want to kill processes... KT (kill terminal), KC (kill chrome) {
# "Kill process" is a named "utility" keymap (not typically part of the global list)
C("Ctrl-K"): push_keymap("Kill process", modal = true)
} I think it's unclear how you'd escape from non-modals though... I feel like different use cases could want ALL the behaviors:
I'm not loving all those options. Perhaps you escape a non-modal by triggering a combo in another keymap higher up the stack... so if you start a multi-step combo but then "alt-tab" the tabbing cancels the multi-step combo. |
Maybe (commands):
Prepend and remove would work with the global list of keymaps and modal would "lock" you in, as it does now. |
Just trying to digest some of this. Basically you're looking at making keymaps a bit smarter in how they are used? Don't really understand your example about pushing a keymap for killing processes. |
I'm just showing you could have a key lead to any NAMED keymap... not a specific keymap... or you could have a variable determine what keymap you got. These would be equivalent: keymap("Kill process", { xyz })
{
C("Ctrl-K"): push_keymap("Kill process", modal = true)
} and {
C("Ctrl-K"): { xyz }
} But push_keymap gives you a lot more power in what should happen... |
Python 3.10 introduced a structure like Switch/Case: https://docs.python.org/3.10/whatsnew/3.10.html#pep-634-structural-pattern-matching Wonder if that could be somehow supported as an alternative to the way nested keys are done now. Maybe it would be simpler. |
You could push/remove multiple keymaps with a combo, etc... |
So the
Yeah, interesting. |
That is program code, where-as the map data we need really needs to be furnished as a data structure that we can process and alter (to fill in right/left keys, etc). |
I kind of expected that. |
OK, so wait, would this mean if you had a "default" option like |
I think if you want outside combos to work then the nested keymap just shouldn't be modal. Do you have an example where you desire a modal keymap that can redirect input to other keymaps? |
Well... the way I'm doing it now in AHK, to fully imitate the macOS dead key experience, the default case from the Switch/Case needs to deselect the previously selected accent character. This means there needs to actually be an "exit" action before the input combo goes on to do whatever it would normally do. So just having the keymap be non-modal wouldn't quite do the trick in the same way. It would need to work something like this, I think. keymap("DK - Grave", {
C("a"): C("x"),
C("b"): C("y"),
Key.ANY: C("Right"), remove_keymap(),
}
keymap("DK - Acute", {
C("a"): C("b"),
Key.ANY: C("Right"), remove_keymap(),
}
keymap("Option Key Special Characters", {
C("Alt-Grave"): push_keymap("DK - Grave", modal=True),
C("Alt-E"): push_keymap("DK - Acute", modal=True),
} But if you activated one of the dead keys keymaps and then did something unrelated like Cmd+A, it would perform the "exit" actions (right arrow and remove keymap) and also let the Cmd+A go on to do "select all" as it normally would. The question is if the "exit" action could be triggered without the keymap being modal. If it can, there's no problem. And the exit action(s) would need to happen prior to the input combo doing what it would normally do. This is explicit in the AHK version with the "%UserInput%" variable being sent out right at the end. I assume that "remove_keymap" if it doesn't reference anything would just automatically remove the keymap it's inside of. |
What problem did you have the other day when trying to build using non-nested keymaps? Do you have your code from that? Looking at it now it should work - it just doesn't STOP evaluating the match conditions when it finds a matching key.... but I don't think that would matter for your use case. keymap("DK - Grave", {
C("a"): C("x"),
C("b"): C("y"),
}, when = lambda: if_unicode_enabled("Grave"))
keymap("DK - Acute", {
C("a"): C("b"),
}, when = lambda: if_unicode_enabled("Acute"))
# toggles the variable
keymap("disable unicode trigger", {
}, when = lambda: disable_all_unicode()
)
keymap("Option Key Special Characters", {
C("Alt-Grave"): push_keymap("Grave"),
C("Alt-E"): push_keymap("Acute"),
} push_keymap would be your own helper function that sets the global state correctly and enabled the keymaps... |
A problem here is |
In my testing the dead key keymap was getting disabled immediately. Because the thing that kills the keymap was also getting evaluated on every key press. What circumstance do you think would delay "disable_all_unicode" from running within a couple of milliseconds of the conditions that activate any of the dead keys keymaps? There's nothing that causes the code to pause inside the newly active keymap. The dead key keymap would work fine for me as long as I disabled the tripwire keymap. But then of course the only way to disable the dead key keymap was to use one of the designated shortcuts contained within it, and have the same function kill the keymap on the way out. I didn't actually progress to that point, but it would have looked something more like this. keymap("DK - Grave", {
C("a"): [C("x"), KDK()],
C("b"): [C("y"), KDK()],
}, when = lambda: if_DK_enabled("Grave"))
keymap("DK - Acute", {
C("a"): [C("b"), KDK()],
}, when = lambda: if_DK_enabled("Acute"))
# toggles the variable
keymap("disable unicode trigger", {
}, when = lambda: KDK() # KDK = kill_dead_keys
)
keymap("Option Key Special Characters", {
C("Alt-Grave"): push_keymap("Grave"),
C("Alt-E"): push_keymap("Acute"),
} But there's just no way I know of to keep the deactivation condition from killing the newly active keymap before you can even press another key. |
I got rid of the original because it seemed fairly pointless. But it was something like this. Populate the "ac_Chr" variable, use it to type the correct diacritic, select it, then the appropriate keystroke (from the keymap that was activated by ac_Chr having that value) should overwrite the selection with the full accented character coming from the relevant activated keymap. Worked fine as long as I completely disabled the tripwire that would automatically kill it. keymap("DK - Grave", {
C("a"): [C("x"), KDK()],
C("b"): [C("y"), KDK()],
}, when = lambda _: ac_Chr == 0x0060)
keymap("DK - Acute", {
C("a"): [C("b"), KDK()],
}, when = lambda _: ac_Chr == 0x00B4)
# toggles the variable
keymap("disable dead keys trigger", {
}, when = lambda: KDK() # KDK = kill_dead_keys
)
keymap("Option Key Special Characters", {
C("Alt-Grave"): [set_dead_key_char(0x0060), UC(ac_Chr), C("Shift-Left")],
C("Alt-E"): [set_dead_key_char(0x00B4), UC(ac_Chr), C("Shift-Left")],
} |
What should happen:
So if you hit
If you hit
Step 3 does toggle the variable, but it doesn't matter because at that point DK - grace has already been checked AND added to the active keymaps... The only trick is the "trigger" needs to come AFTER the "submaps" and before the "enable" commands... it must sit in the middle. Now the active keymaps are searched in order:
If you want to whip up a test case again (and save it) and if it doesn't work pass it over to me to play with I'd be happy to take a look... |
The way I laid mine out was just like that, in the order in the example above. But I will put together a test case again.
Maybe this is the secret. I'm sending out key presses after setting the variable state. That just means I'll have to manually send the diacritic character out and select it before setting the variable, but that shouldn't be a big deal. I was trying to take too much of a shortcut. Makes sense. |
I was not referring to the order of the command sequence. That should be irrelevant since no input happens while commands are running anyways. |
That's kind of what I thought originally. Unfortunately, nothing that I'm trying is working the way I thought it would, at this point. Absolutely nothing. Either I can't even get the keymap to activate without the trigger keymap, or it activates inappropriately before I even type the input combo, and won't deactivate even with the trigger keymap enabled. I don't remember having nearly this much trouble the last time I tried to do this, but maybe my testing was too limited to display the same problems I'm having now. I'm about at the stage where I want to run my laptop through an industrial paper shredder and just forget the whole thing. 🤣 _optspecialchars = True
ac_Chr = 0x0000
def set_dead_key_char(hex_unicode_addr):
global ac_Chr
ac_Chr = hex_unicode_addr
# return ac_Chr
def get_dead_key_char(hex_unicode_addr):
global ac_Chr
if ac_Chr == hex_unicode_addr:
return True
else:
return False
keymap("DK - Grave", {
# C("A"): C("x"), # à Latin Small a with Grave
C("A"): UC(0x00E0),# , set_dead_key_char(0x0000)], # à Latin Small a with Grave
}, when = lambda ctx: ctx.wm_class not in remotes and _optspecialchars is True and get_dead_key_char(0x0060) is True)
# }, when = lambda ctx: ctx.wm_class not in remotes and _optspecialchars is True and ac_Chr == 0x0060)
# }, when = lambda _: ac_Chr == 0x0060)
# keymap("Disable Dead Keys",{
# # Nothing here. Tripwire keymap to disable active dead keys keymap(s)
# }, when = lambda _: set_dead_key_char(0x0000))
keymap("Option key special characters US", {
# Number keys row with Option
######################################################
C("Alt-Grave"): [UC(0x0060), C("Shift-Left"), set_dead_key_char(0x0060)],
}, when = lambda ctx: ctx.wm_class not in remotes and _optspecialchars is True) |
I'll test tomorrow and see if I can get this working. |
Ok, it indeed works, but I'm not sure why you've started so complex. First lets get it working, then make it more complex. I'm not sure if any state is needed for the alt-grave:
I simplified the keymap to the variable check variant:
Now we need to remember that
And the one BIG thing I think you're doing wrong is that helpers need to return FUNCTIONS... otherwise they only run once when the config is evaluate, and that accomplishes nothing:
Not sure if both globals are necessary there or not. I just stopped when I got it working.. |
IE, |
Oh, this dead key stuff is pretty nifty too! 💙💙💙 |
I wonder if we could make this mistake hard to make somehow... hmmm... |
Oh, so if it returns a function, that's the kind of object that can be evaluated over and over again, but just returning a "True" or something is a logical dead end? Kind of makes sense, I guess.
Putting an extra "()" after the initial set of parentheses with something inside is not something that would have EVER made sense to me to try. It still doesn't. I've never seen anything like that. That's just weird. But as long as it works. 🤷🏽♂️
I don't think the second one is necessary. Pretty sure the "globalness" of the variable from the first time carries over into any sub-functions. But I'll double-check. [Edit: Wrong. I think the global is necessary at each level. Good job. 👍🏽 ]
I did try that, it was commented out in the sample because even that simple form wasn't behaving as I expected. Probably going wrong somewhere else.
Yeah, I kind of see the difference. It shouldn't just "do the thing" but make an object (a simple machine made of code) that can "do the thing" later as many times as it needs to.
I'm sure I won't be the only one that will have trouble grasping exactly what it is that
The entire Option key scheme has to be disabled by default, because it covers the ENTIRE keyboard with alternate characters when using Option or Shift+Option. That means any shortcut combo that normally uses Alt+key or Shift+Alt+key will be blocked when the special character scheme is active. So I had to set it up to be inactive and then activated/deactivated by Shift+Opt+Cmd+O. Unfortunately both Windows and Linux apps rely on too many Alt-based shortcuts for there to be any easy way around this. Although, if you take Kinto's mimicry of the Apple keyboard to its logical conclusion, I guess you could argue that the Option key scheme should be active by default, and any Alt-based shortcuts that happen to be interfered with should actually be remapped to be based on Cmd (same physical location) or Ctrl rather than still trying to use the Alt/Option key. Hmmm... 🤔 I guess I've been thinking that the Option key scheme should "stay out of the way", but in reality none of those Alt-based shortcuts that it steps on would work the same way on macOS. So they should be fixed. This is really something to think about. That would actually kind of be neat if it was just active all the time by default. Like any Mac you sit down to use.
Heck yeah, lots of useful characters even on the standard US layout. I lost count at about 600 characters on the "ABC Extended" layout. I don't know if I'll ever have the time and focus to complete that one. Kind of halfway through building the catalog that I'll need for the implementation process. |
In my imagination just now I had an idea that maybe the |
The dead keys keymaps are working, but this consolidated "escape keys" keymap is not able to access the "ac_Chr" variable the same way the condition can. I even tried a small function to go out and "globalize" the variable value and return it. Doesn't work. The way this was working with nested keys is the entry action would set the variable, so it was usable inside the nested keymap. But if I can get the variable value inside this keymap I'll be able to avoid repeating these lines 30 times with different static values for each dead key. Edit: Only way I can think of is to make a function that gets access to the variable and then processes the combos, kind of like the smart "Enter to rename" function. def get_dead_key_char():
global ac_Chr
def fn():
return ac_Chr
return fn
GDK = get_dead_key_char
deadkeys_US = [
0x0060, # Dead Keys Accent: Grave
0x00B4, # Dead Keys Accent: Acute
0x00A8, # Dead Keys Accent: Umlaut
0x02C6, # Dead Keys Accent: Circumflex
0x02DC, # Dead Keys Accent: Tilde
]
keymap("Escape actions for dead keys", {
C("Esc"): [GDK(),UC(ac_Chr),DDK(None)], # Leave accent character if dead keys Escaped
C("Space"): [GDK(),UC(ac_Chr),DDK(None)], # Leave accent character if user hits Space
C("Delete"): [GDK(),UC(ac_Chr),DDK(None)], # Leave accent char if user hits Delete (not Backspace)
C("Backspace"): [GDK(),UC(ac_Chr),C("Backspace"),DDK(None)], # Delete character if user hits Backspace
C("Tab"): [GDK(),UC(ac_Chr),C("Tab"),DDK(None)], # Leave accent char, insert Tab
C("Up"): [GDK(),UC(ac_Chr),C("Up"),DDK(None)], # Leave accent char, insert Tab
C("Down"): [GDK(),UC(ac_Chr),C("Down"),DDK(None)], # Leave accent char, insert Tab
C("Left"): [GDK(),UC(ac_Chr),C("Left"),DDK(None)], # Leave accent char, insert Tab
C("Right"): [GDK(),UC(ac_Chr),C("Right"),DDK(None)], # Leave accent char, insert Tab
C("RC-Tab"): [GDK(),UC(ac_Chr),bind,C("Alt-Tab"),DDK(None)], # Leave accent char, task switch
C("Shift-RC-Tab"): [GDK(),UC(ac_Chr),bind,C("Shift-Alt-Tab"),DDK(None)], # Leave accent char, task switch (reverse)
C("RC-Grave"): [GDK(),UC(ac_Chr),bind,C("Alt-Grave"),DDK(None)], # Leave accent char, in-app window switch
C("Shift-RC-Tab"): [GDK(),UC(ac_Chr),bind,C("Shift-Alt-Grave"),DDK(None)], # Leave accent char, in-app window switch (reverse)
}, when = lambda _: ac_Chr in deadkeys_US) |
You need to read the helpers and understand what they do. Let's take UC/unicode_keystrokes first... read that actual function - what does it return?... it takes a unicode number as an input and returns a combo_list as an output... this happens when those functions are first executed, when the config file is evaluated... so The variable is used once to generate the static combo_list... changing the variable won't rebuild the kemap to have a different combo list... Keymaps are mostly static things, not dynamic. The only way to make them dynamic by including actual functions in the list - which are run at runtime - not config time. Now what you could do (like so many other places) is write a function that RETURNS a function - and that function closes around a variable that was passed in (or global)... You'd need to read up on closures, and pass by value vs pass by reference. For this to work in Python I think you'd need to use a container (which is passed by reference), such as a dict. So perhaps you need a |
Just a guess: def print_current_dead_key():
def fn():
# do we need to handle None edge case?
return UC(deadkeys["chr"])
return fn |
So maybe you had the right idea with |
Couldn't you just compare it against None or not? |
Awesome... though I'm still not certain this approach is something we should encourage... (trigger keymaps) Can't the trigger be entirely avoided if the last command in every dead key sequence is a function that turns the dead key system back off? |
The trigger is needed to disable the dead key keymap if you don't use one of the keys inside the dead key keymap. Of course, it also serves double duty in that using a key/combo from inside the dead key keymap will then continue on, and the tripwire will disable the keymap in that case as well. Normal keymaps don't have the auto-disabling feature of the nested keymaps when you use a key/combo that is NOT in the nested keymap. I have to always allow for the possibility that the user will use an "invalid" key that doesn't go with that accent, or just change their mind in the middle of the dead key sequence. And they should always be able to, safely. So there needs to be a universal disabler keymap.
Mmm. I want that keymap to be active only when one of the dead keys keymaps is active, and they are only active when set to one of the values from the list. Seems less reliable to compare it to something like "not None". I'm also not certain that setting the variable to None is the best idea. I keep running into the error where the Unicode processor doesn't like NoneType, and it's been hard to track down exactly where that happens. The trace keeps pointing me at a blank line, for some reason. Still doing a lot of experimenting.
I took some inspiration from the to_keystrokes function. Mostly working. But I have a strange problem that repeated usage has a continuously cumulative output of often unrelated diacritic characters. Still trying to track down the source of that. ac_Chr = 0x0000
_ac_Chr = 0x0000
def set_dead_key_char(hex_unicode_addr):
global ac_Chr
global _ac_Chr
if hex_unicode_addr == None or hex_unicode_addr == 0x0000:
pass
else:
(_ac_Chr := hex_unicode_addr)
def fn():
global ac_Chr
debug(f"################### setDK | dead key set to: {hex_unicode_addr}")
ac_Chr = hex_unicode_addr
return fn
def get_dead_key_char():
global _ac_Chr
def fn():
combo_list = []
global _ac_Chr
debug(f"################## GDK | _ac_Chr is: {_ac_Chr}")
debug(f"########### GDK | Unicode ac_Chr is: {UC(_ac_Chr)}")
combo_list.append(UC(_ac_Chr))
# _ac_Chr = 0x0000
return combo_list
return fn
GDK = get_dead_key_char
DDK = set_dead_key_char
setDK = set_dead_key_char |
Of course you have to have the necessary bits inside the function that you actually return. This works ever so much better. def set_dead_key_char(hex_unicode_addr):
global ac_Chr
global _ac_Chr
def fn():
global ac_Chr
global _ac_Chr
if hex_unicode_addr == None or hex_unicode_addr == 0x0000:
pass
else:
_ac_Chr = hex_unicode_addr
debug(f"################### setDK | dead key set to: {hex_unicode_addr}")
ac_Chr = hex_unicode_addr
return fn |
The global lines don't need seem to need to be in the outside function, if you're not going to bother doing anything with the variables at that level. The variable can be pulled in just as easily from within the sub-function level directly. So it just needs to happen within the function where you're actually going to use it. Backwards from what I was thinking. |
So there are two remaining problems. One is not too bad, at the moment. Jumping from one dead key to another in the middle of the process will at least replace the highlighted diacritic with the new one you just started, rather than just appearing to do nothing, like the nested keys. This is a huge advance over the nested keys method. Not yet exactly the same as macOS, which would leave the old diacritic in place and then put the new one in a highlight next to it. But the other issue is when you use pretty much any other key. There's still no "exit" action. So instead of typing the current dead key char and then leaving the new character beside it, it overwrites the highlighted dead key accent. I was hoping I could get set_dead_key_char to create the exit action when it gets used by the tripwire, but haven't found a way to make that work. If you type the 5 dead keys shortcuts one after the other, the result on macOS would be:
But since they all get overwritten, the result here is just the last one in the sequence:
If you do Opt+Grave and then type an "invalid" letter like "m" the result should be:
Instead the result is just:
I can sort of get around some of this by just putting the whole alphabet, digits, the other dead key combos, and some additional common combos in the consolidated "escape" keymap, now that it's working. Since it's below the dead keys and just above the tripwire, all the "valid" keys for each dead key should still override the "escape" keymap and keep working. But there would come a point when it would be a little ridiculous, and there would still be many combos that just wouldn't react quite right with the dead keys. If that's the only possible way for now, I'll just have to go for it. |
Alright, well it's a bit ungainly, but by golly it works. To the point where it's highly unlikely anyone will ever notice or care about the remaining edge cases. If there is ever a Try putting the contents of this file in your Kinto config, just below things like modmaps and above pretty much all other keymaps. This will guarantee the whole thing works, and it will be obvious if it's interfering with any shortcuts from your applications. We really need to fix those, if they exist. The _optspecialchars variable is set to True, so everything will be enabled by default. You can disable it with Shift+Opt+Cmd+O. The dead keys are: Opt+Grave: Grave They pretty much all accept the vowels, except Tilde which accepts only a/n/o/A/N/O, and Umlaut which also accepts y/Y. If you use any other letter, digit, or punctuation the action should be universally the same. It leaves the accent char, then types your new character next to it. Jumping between the dead keys should leave an accumulation of the accent characters as you go:
This is how it's supposed to work. Don't forget to try the entire rest of the keyboard, including the numbers, with Option and Shift+Option. Everything that I've managed to remember to test is working perfectly. Including the Enter key, which I finally realized I needed to add to the special escape keys. |
Thanks for all your assistance with getting past the problems with this. Would have taken me weeks of additional fiddling to get to this solution without your suggestions and explanations of exactly what was going wrong. Now I have a basically complete config containing all the things I've wished Kinto had for a couple of years. So many things all working together at the same time. Everything after this is just fine-tuning little annoyances, like adding more keymaps to make various Linux windows respond to Cmd+W the way they should. But as of now I don't know of any other basic mechanisms that need to be put in place. 🎉 🎉 🎉 🎉 🎉 🎉 🎉 Of course knowing me I'll have an entirely new idea for an essential feature tomorrow. 🤣 🤣 🤣 |
With some programming one could easily build this, or at least And this is more powerful because it already works - AND you know what the character is... vs with KEY.ANY the next question is "how do i know which key is pressed so i can behave differently"... just coding them all gives you that by default. |
Not sure why you're still using |
I just realized this is thinking about it in a too limited fashion. So Key.ANY is actually inadequate, if it would only expand out into individual keys, even if it expands to the entire contents of the key definition file. No, what I need is Combo.ANY. And that expansion would be ridiculous. There are thousands of combinations, including all the keys in the key definition file individually as "combos" before adding modifiers. So I believe the potential solution you just talked about in issue #100 is what I've actually been looking for. A true "exit" action for a keymap when the "combo" (including individual keys) passing through is not found in the keymap.
If you're just talking about my own additions to Kinto's config, I haven't bothered converting some things that were already working and are still working. If there is some specific benefit to switching to |
Except not really, to the computer. In Ruby you could possibly do this enumeration with one line of code because it's built-in library functionality is so great. Computers are great at generating 1,000,000 possible combos in a split second. And since this is a Remember we already half do this. Anything like For many uses |
Yes. This sounds very right.
Makes sense. Interesting to think about.
So, your primary objection to my "insane numbers of combos" that I was doing previously to handle the "media arrows fix" and the "GTK3 numpad nav keys fix" was just that I was doing it explicitly? You'd be fine with enumerating all possible combos (including all possible L/R variations) to be used as a static dict? I'm not that great at math but if we ignore permutations (we can ignore permutations, right?) and just stick with combinations, that feels like it would at least be 100K possible entries in that dict. And it wouldn't cover custom defined modifiers like your Hyper key (unless all possible additional/unusual modifiers were added to the loop that creates the dict). Even if it makes a static dict in the end, seems like that would take a noticeable amount of time at startup. And also should be a moot point if issue #100 follows through with a condition that could do the job of "combo ain't here" rather than searching a huge dict. |
I dunno about "primary" but it's often the first thing I think of when I see your configs with 200 lines that are identical except for a single tiny variable, yes. Your escape keymap for dead keys is one such example that could be largely built dynamically. Some things need nicer abstractions, some things just need a simple loop.
Oh I hadn't considered that. :-) That makes it a bit worse... but I wasn't trying to make a hard point, but rather say "it works in more cases than you'd think"... such as working in your case with only 100-200 combos... I know I add some things pretty fast but overall things that are nebulous (or have other solutions) need to prove that they are worthy of adding - and exploring the other solutions first often educations the design of the actual feature in the first place.
Which is easy enough to do.
Probably not. Write some code, test it out. :-) People forget how fast computers are.
Searching a huge dict is fast by design, that's how hash tables work. That's some good reading if you're not familiar with data structures. |
If we could identify the differences and then make the software flexible enough to build nested keymaps on top of top-level keymaps that would be a big win. Key differences now:
The text was updated successfully, but these errors were encountered: