Skip to content

Helper Function Examples

RedBearAK edited this page Jan 16, 2023 · 10 revisions

Due to the inherently flexible nature of an app written in Python, there will be numerous different possible solutions to making the usage of a utility like keyszer simpler for the end user. New functions can simply be included in the "config" file, which is just another Python file. This page will list some examples.

1. Matching Multiple Window/Context Properties Together

Function: matchProps()

This function allows access to any of the available window/context properties, in any combination:

  • wm_class (obtained with xprop WM_CLASS)
  • wm_name (obtained with xprop _NET_WM_NAME)
  • device_name (obtained with keyszer --list-devices)
  • capslock_on (bool)
  • numlock_on (bool)

It will also accept and process lists of Python dicts containing the named parameters corresponding to the above properties (or another list of dicts):

mylist = [
    {'cls':"^some-app$", 'nme':"^some-app-window$", 'clk':False},
    {'cls':"^other-app$", 'nlk':False},
    {'cls':"^third-app$", 'nme':"^Preferences$"},
    {'lst':another_list},
]

Using this trick of defining the parameter names as strings in the same file can allow the keys: in any lists of dicts to be used without quotes. (The parameters that take strings will still need their values quoted. Bools, variables or lists used as values should not be quoted, of course.)

cls = 'cls' # key label for matchProps() arg to match: wm_class
nme = 'nme' # key label for matchProps() arg to match: wm_name
dvn = 'dvn' # key label for matchProps() arg to match: device_name
not_cls = 'not_cls' # key label for matchProps() arg to NEGATIVE match: wm_class
not_nme = 'not_nme' # key label for matchProps() arg to NEGATIVE match: wm_name
not_dvn = 'not_dvn' # key label for matchProps() arg to NEGATIVE match: device_name
nlk = 'nlk' # key label for matchProps() arg to match: numlock_on
clk = 'clk' # key label for matchProps() arg to match: capslock_on
cse = 'cse' # key label for matchProps() arg to enable: case sensitivity
lst = 'lst' # key label for matchProps() arg to pass in a [list] of {dicts}
dbg = 'dbg' # debugging info param

mylist = [
    {cls:"^some-app$", nme:"^some-app-window$", clk:False},
    {cls:"^other-app$", nlk:False},
    {cls:"^third-app$", not_nme:"^Preferences$"},
    {lst:another_list},
]

Usage examples as a "when = " argument:

keymap("test keymap", {
    # empty
}, when = matchProps(cls="^Firefox.*$", nme="^asdf$", dbg="Debugging string"))

}, when = matchProps(cls="^org.gnome.Software$", not_nme="^Software$"))

}, when = matchProps(dvn="^Name of Your Keyboard Device$"))

}, when = matchProps(dvn="^Name of Your Keyboard Device$", nlk=True))

}, when = matchProps(lst=mylist))

The function:

def matchProps(
    dpm=None,                                   # None    (dummy parameter)
    cls=None, nme=None, dvn=None,               # string  (positive match)
    not_cls=None, not_nme=None, not_dvn=None,   # string  (negative match)
    nlk=None, clk=None, cse=None,               # bool
    lst=None,                                   # list of dicts (positive)
    not_lst=None,                               # list of dicts (negative)
    dbg=None,                                   # string  (debugging info)
):
    """
    ### Match all given properties to current window context.   \n
    - Parameters must be _named_, no positional arguments.      \n
    - All parameters optional, but at least one must be given.  \n
    - Defaults to case insensitive matching of:                 \n
        - WM_CLASS, WM_NAME, device_name                        \n
    - To negate/invert regex pattern match use:                 \n
        - `not_cls` `not_nme` `not_dvn` params or...            \n
        - "^(?:(?!pattern).)*$"                                 \n
    - To force case insensitive pattern match use:              \n 
        - "^(?i:pattern)$" or...                                \n
        - "^(?i)pattern$"                                       \n

    ### Accepted Parameters:                                    \n
    `dpm` = IGNORE THIS - For enforcing use of named parameters \n
    `cls` = WM_CLASS    (regex/string) [xprop WM_CLASS]         \n
    `nme` = WM_NAME     (regex/string) [xprop _NET_WM_NAME]     \n
    `dvn` = Device Name (regex/string) [keyszer --list-devices] \n
    `not_cls` = `cls` but inverted, matches when "not"          \n
    `not_nme` = `nme` but inverted, matches when "not"          \n
    `not_dvn` = `dvn` but inverted, matches when "not"          \n
    `nlk` = Num Lock LED state         (bool)                   \n
    `clk` = Caps Lock LED state        (bool)                   \n
    `cse` = Case Sensitive matching    (bool)                   \n
    `lst` = List of dicts of the above arguments                \n
    `not_lst` = `lst` but inverted, matches when "not"          \n
    `dbg` = Debugging info             (string)                 \n

    ### Negative match parameters: 
    - `not_cls` `not_nme` `not_dvn`                             \n
    Parameters take the same regex patterns as `cls` `nme` `dvn`\n
    but result in a True condition only if pattern is NOT found.\n
    Negative parameters cannot be used together with the normal \n
    positive matching equivalent parameter.                     \n

    ### List of Dicts parameter: `lst` `not_lst`
    A [list] of {dicts} with each dict containing 1 to 6 of the \n
    named parameters above, to be processed recursively as args.\n

    ### Debugging info parameter: `dbg`
    A string that will print as part of logging output. Use to  \n
    help identify origin of logging output.                     \n
    -                                                           \n
    """
    # Reference for successful negative lookahead pattern, and explanation of why it works:
    # https://stackoverflow.com/questions/406230/regular-expression-to-match-a-line-that-doesnt-contain-a-word

    logging_enabled = False

    allowed_params  = (cls, nme, dvn, not_cls, not_nme, not_dvn, nlk, clk, cse, lst, not_lst, dbg)
    lst_dct_params  = (cls, nme, dvn, not_cls, not_nme, not_dvn, nlk, clk, cse)
    string_params   = (cls, nme, dvn, not_cls, not_nme, not_dvn, dbg)

    dct_param_strs  = [
        'cls', 'nme', 'dvn', 
        'not_cls', 'not_nme', 'not_dvn', 
        'nlk', 'clk', 'cse', 
        'lst', 'not_lst', 'dbg'
    ]

    if dpm is not None: 
        raise ValueError(f"\n\n(DD) matchProps requires named parameters\n")
    if all([x is None for x in allowed_params]): 
        raise ValueError(f"\n\n(DD) matchProps received no valid argument\n")
    if any([x not in (True, False, None) for x in (nlk, clk, cse)]): 
        raise TypeError(f"\n\n(DD) matchProps params 'nlk'|'clk'|'cse' are bools\n")
    if any([x is not None and not isinstance(x, str) for x in string_params]): raise TypeError(\
        f"\n\n(DD) matchProps params 'cls|nme|dvn|not_cls|not_nme|not_dvn|dbg' must be strings\n")
    if cls and not_cls or nme and not_nme or dvn and not_dvn or lst and not_lst: raise ValueError(\
        f"\n\n(DD) matchProps - don't mix positive and negative match params for same property\n")

    # consolidate positive and negative matching params into new vars
    _cls = not_cls if cls is None else cls
    _nme = not_nme if nme is None else nme
    _dvn = not_dvn if dvn is None else dvn
    _lst = not_lst if lst is None else lst

    # process lists of conditions
    if _lst is not None:
        if any([x is not None for x in lst_dct_params]): 
            raise TypeError(f"\n\n(DD) matchProps param 'lst|not_lst' must be used alone\n")
        if not isinstance(_lst, list) or not all(isinstance(item, dict) for item in _lst): 
            raise TypeError(f"\n\n(DD) matchProps param 'lst|not_lst' wants a [list] of {{dicts}}\n")
        # verify that every dict only contains valid parameter names
        for dct in _lst:
            for prm in list(dct.keys()):
                if prm not in dct_param_strs:
                    print(f"\n(EE) Invalid matchProps() parameter: {prm}\n")
                    print(f"(DD) Parameter is in dict: {dct}\n")
                    print(f"(DD) Dict is in list:")
                    for x in _lst:
                        print(f"(DD)   {x}")
                    raise ValueError(f"\n\n(DD) matchProps - invalid parameter found in dict in list\n")

        def _matchProps_Lst(ctx):
            if not_lst is not None:
                if logging_enabled: print(f"## _matchProps_Lst()[not_lst] ## {dbg=}")
                return ( None if next(filter(lambda dct: 
                                    matchProps(**dct)(ctx), not_lst), False) else True )
            else:
                if logging_enabled: print(f"## _matchProps_Lst()[lst] ## {dbg=}")
                return next(filter(lambda dct: matchProps(**dct)(ctx), lst), False)

        return _matchProps_Lst

    # compile case insentitive regex object for given params unless cse=True
    if _cls is not None: cls_rgx = re.compile(_cls) if cse else re.compile(_cls, re.I)
    if _nme is not None: nme_rgx = re.compile(_nme) if cse else re.compile(_nme, re.I)
    if _dvn is not None: dvn_rgx = re.compile(_dvn) if cse else re.compile(_dvn, re.I)

    def _matchProps(ctx):
        cnd_lst = []

        if _cls is not None:                            # param is given?
            if not_cls is not None:                     # negative match requested?
                cnd_lst.append( None if re.search(cls_rgx, ctx.wm_class) else True )
            else:                                       # positive match requested?
                cnd_lst.append( re.search(cls_rgx, ctx.wm_class) )
        if _nme is not None:                            # param is given?
            if not_nme is not None:                     # negative match requested?
                cnd_lst.append( None if re.search(nme_rgx, ctx.wm_name) else True )
            else:                                       # positive match requested?
                cnd_lst.append( re.search(nme_rgx, ctx.wm_name) )
        if _dvn is not None:                            # param is given?
            if not_dvn is not None:                     # negative match requested?
                cnd_lst.append( None if re.search(dvn_rgx, ctx.device_name) else True )
            else:                                       # positive match requested?
                cnd_lst.append( re.search(dvn_rgx, ctx.device_name) )

        # these two MUST check for "not None" because external input is bool
        if nlk is not None: cnd_lst.append( nlk is ctx.numlock_on  )
        if clk is not None: cnd_lst.append( clk is ctx.capslock_on )

        if logging_enabled: # and all(cnd_lst): # << to show only "True" condition lists
            print(f'####  CND_LST ({all(cnd_lst)})  ####  {dbg=}')
            for elem in cnd_lst:
                print('##', re.sub('^.*span=.*\), ', '', str(elem)).replace('>',''))
            print('-------------------------------------------------------------------')

        return all(cnd_lst)

    return _matchProps

2. Match Only Window Class and Name together

Case Insensitive or Case Sensitive Matching for WM_CLASS and/or WM_NAME

This function asks for a string or regex pattern to be used to match on WM_CLASS (cls) and/or WM_NAME (nm). By default, input and window properties are both casefolded so the match becomes case insensitive. Pass the optional "case" parameter as 's' to make matching case sensitive.

Returns a function. Cannot be combined with other conditions. Requires either the "cls" or "nm" arguments, but one can be "None" (without the quotes). See usage examples below.

Function: isWindow()

def isWindow(cls, nm, case='i'):
    """
    Return WM_CLASS and/or WM_NAME match for current window.
    Default is case insensitive matching. Case param is optional.
    
    Accepts regex patterns and literal strings.
    Requires either "cls" or "nm" parameter, other can be None.

    cls = WM_CLASS | nm = WM_NAME | case = 'i' or 's'
    """
    if case != 'i' and case != 's':
        print(f"(DD) ###  isWindow case parameter invalid: Use 'i' or 's'  ###")
        return

    if case == 'i':
        if cls == None and nm == None:
            raise ValueError("No valid argument given to isWindow")
        if cls != None:
            cls_rgx = re.compile(cls.casefold())
        if nm != None:
            nm_rgx = re.compile(nm.casefold())
        
        def cond(ctx):
            if cls != None and nm != None:
                return cls_rgx.search(ctx.wm_class.casefold()) and nm_rgx.search(ctx.wm_name.casefold())
            elif cls == None and nm != None: 
                return nm_rgx.search(ctx.wm_name.casefold())
            elif cls != None and nm == None: 
                return cls_rgx.search(ctx.wm_class.casefold())

    if case == 's':
        if cls == None and nm == None:
            raise ValueError("No valid argument given to isWindow")
        if cls != None:
            cls_rgx = re.compile(cls)
        if nm != None:
            nm_rgx = re.compile(nm)
        
        def cond(ctx):
            if cls != None and nm != None:
                return cls_rgx.search(ctx.wm_class) and nm_rgx.search(ctx.wm_name)
            elif cls == None and nm != None: 
                return nm_rgx.search(ctx.wm_name)
            elif cls != None and nm == None: 
                return cls_rgx.search(ctx.wm_class)

    return cond

Usage examples as a "when = " argument:

# Flagging WM_NAME as inverse "not" match with (?!pattern) syntax.
# Matches all windows of Transmission-gtk app except the main app window. 
}, when = isWindow("^Transmission-gtk$", "(?!^Transmission$)"))

# WM_CLASS is "SpaceFM", WM_NAME is "Find Files". Case insensitive (default).
}, when = isWindow("^SpaceFM$", "^Find Files$"))

# WM_CLASS is "SpaceFM, no WM_NAME provided. Case insensitive (default).
}, when = isWindow("^SpaceFM$", None))

# WM_CLASS is not provided, WM_NAME is "Find Files". Case insensitive (default).
}, when = isWindow(None, "Find Files"))

# Using named parameters in default order.
}, when = isWindow(cls = "^SpaceFM$", nm = "Find Files"))

# Using named parameters in non-default order.
}, when = isWindow(nm = "Find Files", cls = "^SpaceFM$"))

# Including the optional "case" parameter. Case insensitive (default).
}, when = isWindow("DolPhin","SeArch", 'i')

# Setting the option "case" parameter to case sensitive matching. 
}, when = isWindow("Dolphin","Search", 's')  # case sensitive matching