joshgoebel / keyszer

a smart, flexible keymapper for X11 (a fork/reboot of xkeysnail )
Other
69 stars 15 forks source link

Make nested keymaps an abstraction around top-level keymaps #92

Open joshgoebel opened 2 years ago

joshgoebel commented 2 years ago

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:

joshgoebel commented 2 years ago

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...

joshgoebel commented 2 years ago

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... :-)

joshgoebel commented 2 years ago

@RedBearAK

joshgoebel commented 2 years ago

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.

joshgoebel commented 2 years ago

Maybe (commands):

Prepend and remove would work with the global list of keymaps and modal would "lock" you in, as it does now.

RedBearAK commented 2 years ago

@joshgoebel

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.

joshgoebel commented 2 years ago

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...

RedBearAK commented 2 years ago

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.

joshgoebel commented 2 years ago

You could push/remove multiple keymaps with a combo, etc...

RedBearAK commented 2 years ago

So the push_keymap would literally put you inside the named keymap?

You could push/remove multiple keymaps with a combo, etc...

Yeah, interesting.

joshgoebel commented 2 years ago

Python 3.10 introduced a structure like Switch/Case:

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).

RedBearAK commented 2 years ago

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.

RedBearAK commented 2 years ago

OK, so wait, would this mean if you had a "default" option like Key.ANY inside a keymap, that could be made to... push back to the original keymap? But there still needs to be a way to carry the input through so that it's still treated as "input" outside of the modal keymap. I'm not really seeing a solution to that discussed here.

joshgoebel commented 2 years ago

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?

RedBearAK commented 2 years ago

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.

joshgoebel commented 2 years ago

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...

joshgoebel commented 2 years ago

A problem here is disable_all_unicode() runs after every keypress, but that should still allow the first two keymaps to be active briefly and trigger a single combo (isn't that all nested keymaps do anyways?)... (before being disabled on the next pass)

RedBearAK commented 2 years ago

A problem here is disable_all_unicode() runs after every keypress, but that should still allow the first two keymaps to be active briefly

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.

RedBearAK commented 2 years ago

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")],
}
joshgoebel commented 2 years ago

What should happen:

So if you hit x (a non combo)...

If you hit b:

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...

RedBearAK commented 2 years ago

The only trick is the "trigger" needs to come AFTER the "submaps" and before the "enable" commands... it must sit in the middle.

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.

You Alt-Grave, set_dead_key_char is turned on to 0x60 (as the very last step)

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.

joshgoebel commented 2 years ago

Maybe this is the secret

I was not referring to the order of the command sequence. That should be irrelevant since no input happens while commands are running anyways.

RedBearAK commented 2 years ago

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)
joshgoebel commented 2 years ago

I'll test tomorrow and see if I can get this working.

joshgoebel commented 2 years ago

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:

keymap("Option key special characters US", {
    C("Alt-Grave"): [UC(0x0060), C("Shift-Left"), set_dead_key_char(0x0060)],
})

I simplified the keymap to the variable check variant:

keymap("DK - Grave", {
        C("a"): UC(0x00E0), # ร  Latin Small a with Grave
}, when = lambda _: ac_Chr == 0x0060)

Now we need to remember that set_dead_key_char returns a function so we need to call it, THEN call the function it returns, hence the extra ().

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

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:

def set_dead_key_char(hex_unicode_addr):
    global ac_Chr
    def fn():
        global ac_Chr
        debug("dead key set to", hex_unicode_addr)
        ac_Chr = hex_unicode_addr

    return fn

Not sure if both globals are necessary there or not. I just stopped when I got it working..

joshgoebel commented 2 years ago

IE, set_dead_key_char doesn't need to DO That... because doing that inside the configuration makes zero sense... it needs to return a function to do that LATER, while Keyszer is running.

joshgoebel commented 2 years ago

Oh, this dead key stuff is pretty nifty too! ๐Ÿ’™๐Ÿ’™๐Ÿ’™

joshgoebel commented 2 years ago

is that helpers need to return FUNCTIONS...

I wonder if we could make this mistake hard to make somehow... hmmm...

RedBearAK commented 2 years ago

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.

Now we need to remember that set_dead_key_char returns a function so we need to call it, THEN call the function it returns, hence the extra ().

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. ๐Ÿคท๐Ÿฝโ€โ™‚๏ธ

Not sure if both globals are necessary there or not. I just stopped when I got it working..

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 simplified the keymap to the variable check variant:

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.

IE, set_dead_key_char doesn't need to DO That... because doing that inside the configuration makes zero sense... it needs to return a function to do that LATER, while Keyszer is running.

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 wonder if we could make this mistake hard to make somehow... hmmm...

I'm sure I won't be the only one that will have trouble grasping exactly what it is that when wants. But I don't have any idea of how to simplify or put a fence around it.

I'm not sure if any state is needed for the alt-grave:

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.

Oh, this dead key stuff is pretty nifty too!

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.

RedBearAK commented 2 years ago

In my imagination just now I had an idea that maybe the set_dead_key_char function could actually do the "exit" action I've been looking for, if the variable isn't "None". Am I losing it? I guess we'll see soon.

RedBearAK commented 2 years ago

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)
joshgoebel commented 2 years ago

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 UC(ac_Chr) on all those lines returns a STATIC combo list... after that the variable is nowhere to be found.

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 print_current_dead_key helper that returns a function that calls UC (now at runtime) and references the global dict to get the correct character to use.

joshgoebel commented 2 years ago

deadkeys = { "chr": None }, etc...

Just a guess:

def print_current_dead_key():
  def fn():
    # do we need to handle None edge case?
    return UC(deadkeys["chr"])
  return fn
joshgoebel commented 2 years ago

So maybe you had the right idea with get_dead_key_char, but just returning the number doesn't do anything useful... you have to return a valid command.

joshgoebel commented 2 years ago

}, when = lambda _: ac_Chr in deadkeys_US)

Couldn't you just compare it against None or not?

joshgoebel commented 2 years ago

The dead keys keymaps are working,

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?

RedBearAK commented 2 years ago

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.

Couldn't you just compare it against None or not?

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.

So maybe you had the right idea with get_dead_key_char, but just returning the number doesn't do anything useful... you have to return a valid command.

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
RedBearAK commented 2 years ago

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
RedBearAK commented 2 years ago

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.

RedBearAK commented 2 years ago

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:

`m

Instead the result is just:

m

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.

RedBearAK commented 2 years ago

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 Key.ANY type of thing available, this can be trimmed down significantly.

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.

New_OptSpecialChars-US.py.txt

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 Opt+E: Acute Opt+U: Umlaut Opt+I: Circumflex Opt+N: Tilde

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.

RedBearAK commented 2 years ago

@joshgoebel

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. ๐Ÿคฃ ๐Ÿคฃ ๐Ÿคฃ

joshgoebel commented 2 years ago

If there is ever a Key.ANY

With some programming one could easily build this, or at least KEY_ASCII... ie, you'd just write your own wrapper that looks at a raw keymap and if it sees KEY_ASCII it would expand it to include all the ASCII characters manually. This is exactly how the default keymaps turn Ctrl-A into TWO combos, LCtrl-A and RCtrl-A, etc...

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.

joshgoebel commented 2 years ago

Not sure why you're still using define_keymap.

RedBearAK commented 2 years ago

With some programming one could easily build this, or at least KEY_ASCII... ie, you'd just write your own wrapper that looks at a raw keymap and if it sees KEY_ASCII it would expand it to include all the ASCII characters manually.

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.

Not sure why you're still using define_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 keymap that I'm missing, let me know and I can speed up the process of getting rid of the older define_keymap instances.

joshgoebel commented 2 years ago

And that expansion would be ridiculous.

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 dict the lookups are still cheap.

Remember we already half do this. Anything like Ctrl-Alt-Shift-Cmd-A isn't a single combo - it's 16... (given all the left/right variations)

For many uses Combo.ANY would also be bad because it consumes the keypress... since we can't reinject input you want something (like a trigger) that can make sure you're pointing to the correct keymap BEFORE the combo is mapped...

RedBearAK commented 2 years ago

For many uses Combo.ANY would also be bad because it consumes the keypress... since we can't reinject input you want something (like a trigger) that can make sure you're pointing to the correct keymap BEFORE the combo is mapped...

Yes. This sounds very right.

Remember we already half do this. Anything like Ctrl-Alt-Shift-Cmd-A isn't a single combo - it's 16... (given all the left/right variations)

Makes sense. Interesting to think about.

Computers are great at generating 1,000,000 possible combos in a split second. And since this is a dict the lookups are still cheap.

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.

joshgoebel commented 2 years ago

So, your primary objection ... was just that I was doing it explicitly?

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.

You'd be fine with enumerating all possible combos (including all possible L/R variations)

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.

unless all possible additional/unusual modifiers were added to the loop that creates the dict

Which is easy enough to do.

seems like that would take a noticeable amount of time at startup.

Probably not. Write some code, test it out. :-) People forget how fast computers are.

rather than searching a huge dict.

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.