-
Notifications
You must be signed in to change notification settings - Fork 14
Helper Function Examples
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.
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},
]
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
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.
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
# 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