joshgoebel / keyszer

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

Question: Can modmaps be dynamic per keystroke? #132

Closed RedBearAK closed 1 year ago

RedBearAK commented 1 year ago

Right now I have some modmaps that are dynamic, but only in the sense that a global variable can be toggled on or off. For instance, the modmap that forces my numpad to always be a numpad and ignore the Numlock state. I can enable or disable it.

What I want to know is if it would be sane to set up a dictionary of device names and activate a different modmap depending on, as an example, whether the dictionary says the device is an Apple or IBM or Windows/PC keyboard.

In other words I’m thinking of trying to automate the “keyboard type” feature in Kinto’s config, so that the user can move between different keyboard types without needing to flip a switch manually to fix the modifier locations.

As long as the modmaps are arranged before the keymaps in the config, so that their conditionals get evaluated before the keymaps and their conditionals on each pass through the config, this should present no problem, correct?

joshgoebel commented 1 year ago

modmaps have condition fns... which can reference globals... so you could toggle them on and off any number of different ways via global state...

As long as the modmaps are arranged before the keymaps in the config,

Order of modmap vs keymap doesn't matter. The conditionals for modmaps always run before keymaps... key remapping (modmap) is the first thing that happens - before any combos or further processing is considered.

RedBearAK commented 1 year ago

Order of modmap vs keymap doesn't matter. The conditionals for modmaps always run before keymaps... key remapping (modmap) is the first thing that happens - before any combos or further processing is considered.

Makes sense that would be something to do before checking keymaps. OK, got it. Thanks.

Already started testing, actually. Seems to be working, but I'll need to do some more testing.

RedBearAK commented 1 year ago

I really wish there was a bit more compact way to do this, but it seems to be necessary, when combining multiple criteria, to use a lambda whatevs and then explicitly give each function the context object as an argument. With the name "whatevs" from the lambda as the argument name. I always use "ctx" just to be consistent. Is there something we can do to simplify how this works?

when = lambda ctx: matchProps(lst=terminals_lod)(ctx) and detect_kb_type()(ctx) == "Mac" )

When not trying to combine multiple criteria it just needs to be:

when = matchProps(lst=terminals_lod) )

I wish it could be more like:

when = matchProps(lst=terminals_lod) and detect_kb_type() == "Mac" )

There must be a way to let the conditionals process this and give the context to each criteria in turn without doing it so verbosely.

RedBearAK commented 1 year ago

FYI, the dynamic modmaps are working quite well, once I remembered needing to add the extra "(ctx)" even to the "matchProps()" part of the criteria.

Pretty neat. If this gets integrated into Kinto, the manual switching of keyboard types will be a thing of the past.

Closing this, since the question was answered.

joshgoebel commented 1 year ago

when expects a function as an argument... matchProps(lst=terminals_lod) fulfills this on it's own (assuming it returns a lambda function)...

matchProps(lst=terminals_lod) and detect_kb_type() == "Mac" )

This does NOT return a function... it evaluates to something like [lambda function] and [boolean] which will evaluate to either True or False, which is NOT a lambda...

You might be better off just trying to pass a dict or array of requirements instead... ie, write a helper function that takes an array of lambdas and then returns a single lambda that makes sure they are all true (passing the context down into each), etc...

when = allTrue([ 
   matchProps(lst=terminals_lod),
   detect_kb_type() == "Mac" # just an expression
])

when = anyTrue([ 
   matchProps(lst=terminals_lod),
   lambda ctx: detect_kb_type(ctx) == "Mac" # a dynamic lambda
])

Note that detect_kb_type() == "Mac" here is just an expression, but your outer function can deal with allowing that (assuming you were using the same kb the whole time)... if you really needed it dynamic then you'd likely still need to make it a lambda and pass in the context, etc...

RedBearAK commented 1 year ago

Interesting examples, but if it has to be that complicated it actually feels simpler to stick with the lambda and handing each function inside it an extra "(ctx)" to play with. Between "and", "or" and parentheses (I remember using some in another conditional) you can do all those conditions and more without needing to write any kind of wrapper function(s). I mean, if you've got some really complicated criteria to deal with, you write matchProps(). 😆 🤣

It's just a bit awkward. And took me quite a few tries to realize that the original function that was doing fine on its own needed another "(ctx)" once it was part of another lambda.

As someone said, "It's all just Python." 😆

will evaluate to either True or False, which is NOT a lambda

I think I grasp it better now. matchProps() does indeed return functions rather than static values or booleans. So it works by itself normally.

So the whole thing in the "when" just always needs to be a function of some kind. That's why simple comparison operations need to be in a lambda, where quite often the argument to the lambda is meaningless and can just be "_". Got it. 👍🏽

It always feels like the conditional should be happy with a simple condition that returns a boolean, but it's just not designed to accept such a thing. This is non-obvious to the inexperienced observer.

joshgoebel commented 1 year ago

That's why simple comparison operations need to be in a lambda, where quite often the argument to the lambda is meaningless

That could be fixed, but I'm not sure it's worth the inconsistency... what type of comparison are you doing that doesn't depend on context? Probably depends on global context? In which case you still need a lambda so it runs later, not now.

RedBearAK commented 1 year ago

what type of comparison are you doing that doesn't depend on context? Probably depends on global context? In which case you still need a lambda so it runs later, not now.

Yes, global variables, within the scope of the config file.

Some shortcuts to toggle the global on and off with a function.

    C("Alt-Numlock"):           toggle_mac_numpad(),            # Turn the Mac Numpad feature on and off
    C("Fn-Numlock"):            toggle_mac_numpad(),            # Turn the Mac Numpad feature on and off
    C("Numlock"):               isNumlockClearKey(),            # Turn Numlock key into "Clear" (Esc) if mac_numpad enabled
def toggle_mac_numpad():
    """Toggle the value of the mac_numpad variable"""
    def _toggle_mac_numpad():
        global mac_numpad
        mac_numpad = not mac_numpad
        mac_numpad_alert()

    return _toggle_mac_numpad

And modmaps that depend on the global being true or false. Although the GTK3 fix also needs to know about the NumLock status, so it uses context for part of the condition.

# Change the mac_numpad variable to False to disable by default 
# (see above in "VARS"/"VARIABLES" section)
modmap("Conditional modmap - Mac Numpad feature",{
    # Make numpad be a numpad regardless of Numlock state (like an Apple keyboard in macOS)
    Key.KP1:                    Key.KEY_1,
    Key.KP2:                    Key.KEY_2,
    Key.KP3:                    Key.KEY_3,
    Key.KP4:                    Key.KEY_4,
    Key.KP5:                    Key.KEY_5,
    Key.KP6:                    Key.KEY_6,
    Key.KP7:                    Key.KEY_7,
    Key.KP8:                    Key.KEY_8,
    Key.KP9:                    Key.KEY_9,
    Key.KP0:                    Key.KEY_0,
    Key.KPDOT:                  Key.DOT,  
    Key.KPENTER:                Key.ENTER,
}, when = lambda _: mac_numpad is True)

modmap("Conditional modmap - GTK3 numpad nav keys fix",{
    # Make numpad nav keys work correctly in GTK3 apps
    # Key.KP5:                    Key.X,                          # GTK3 numpad fix - TEST TO SEE IF WORKING
    # Numpad PgUp/PgDn/Home/End keys
    Key.KP9:                    Key.PAGE_UP, 
    Key.KP3:                    Key.PAGE_DOWN, 
    Key.KP7:                    Key.HOME, 
    Key.KP1:                    Key.END,
    # Numpad arrow keys
    Key.KP8:                    Key.UP, 
    Key.KP2:                    Key.DOWN, 
    Key.KP4:                    Key.LEFT, 
    Key.KP6:                    Key.RIGHT,
    # Numpad Insert/Delete/Enter keys
    Key.KP0:                    Key.INSERT, 
    Key.KPDOT:                  Key.DELETE, 
    Key.KPENTER:                Key.ENTER,
}, when = lambda ctx: ctx.numlock_on is False and mac_numpad is False)

Something similar happens with the optional fix for weird laptops with arrow keys that have media functions instead of the standard MacBook PgUp/PgDn/Home/End functions.

# Change the media_arrows_fix variable to True to enable by default 
# (see above in "VARS"/"VARIABLES" section)
modmap("Conditional modmap - Media Arrows Fix",{
    # Fix arrow keys with media functions instead of PgUp/PgDn/Home/End
    Key.PLAYPAUSE:              Key.PAGE_UP,
    Key.STOPCD:                 Key.PAGE_DOWN,
    Key.PREVIOUSSONG:           Key.HOME,
    Key.NEXTSONG:               Key.END,
}, when = lambda _: media_arrows_fix is True)

So my understanding now is that I could define a separate function that checks the value of the global, and use that without "lambda" in the condition, or just use a lambda like this to turn a simple global comparison into a function. Either would work. But initially I would just try something like this, because it seemed logical:

}, when = media_arrows_fix is True)

This would of course fail with the "boolean is not callable" kind of error.

RedBearAK commented 1 year ago

Ha. Learning how to simplify if/else branching in a function to iterate over a dict instead. This keyboard types situation seemed like a good place to try it. Turned out pretty well. The function is essentially a one-liner now.

kbtype_lists = {
    'IBM': keyboards_IBM, 
    'Chromebook': keyboards_Chromebook, 
    'Windows': keyboards_Windows, 
    'Apple': keyboards_Apple
}

def is_kbtype(kbtype):
    """Check the keyboard type for conditional modmaps"""
    def _is_kbtype(ctx):
        dvn = ctx.device_name
        return True if dvn in kbtype_lists[kbtype] or re.search(kbtype, dvn, re.I) else False
    return _is_kbtype

And it lets the "when" be a little cleaner:

}, when = lambda ctx: matchProps(not_lst=terminals_and_remotes)(ctx) and is_kbtype('IBM')(ctx))
}, when = lambda ctx: matchProps(not_lst=terminals_and_remotes)(ctx) and is_kbtype('Chromebook')(ctx))
}, when = lambda ctx: matchProps(not_lst=terminals_and_remotes)(ctx) and is_kbtype('Windows')(ctx))
}, when = lambda ctx: matchProps(not_lst=terminals_and_remotes)(ctx) and is_kbtype('Apple')(ctx))

If it doesn't find the full device name in the matching list, it searches for the argument string directly in the name of the device, giving a pretty good chance of being able to automatically identify the correct type of many keyboards.