2023-03-16 21:35:49 +01:00
""" Functions for searching through QMK keyboards and keymaps.
"""
import contextlib
2023-09-28 22:48:20 +02:00
import functools
2023-03-16 21:35:49 +01:00
import fnmatch
import logging
import re
2023-09-28 22:48:20 +02:00
from typing import List , Tuple
2023-11-22 01:14:34 +01:00
from dotty_dict import dotty , Dotty
2023-03-16 21:35:49 +01:00
from milc import cli
2023-10-17 00:43:50 +02:00
from qmk . util import parallel_map
2023-03-16 21:35:49 +01:00
from qmk . info import keymap_json
2023-11-15 06:24:54 +01:00
from qmk . keyboard import list_keyboards , keyboard_folder
from qmk . keymap import list_keymaps , locate_keymap
from qmk . build_targets import KeyboardKeymapBuildTarget , BuildTarget
2023-03-16 21:35:49 +01:00
def _set_log_level ( level ) :
cli . acquire_lock ( )
old = cli . log_level
cli . log_level = level
cli . log . setLevel ( level )
logging . root . setLevel ( level )
cli . release_lock ( )
return old
@contextlib.contextmanager
def ignore_logging ( ) :
old = _set_log_level ( logging . CRITICAL )
yield
_set_log_level ( old )
def _all_keymaps ( keyboard ) :
2023-09-28 22:48:20 +02:00
""" Returns a list of tuples of (keyboard, keymap) for all keymaps for the given keyboard.
"""
2023-03-16 21:35:49 +01:00
with ignore_logging ( ) :
2023-11-15 06:24:54 +01:00
keyboard = keyboard_folder ( keyboard )
return [ ( keyboard , keymap ) for keymap in list_keymaps ( keyboard ) ]
2023-03-16 21:35:49 +01:00
def _keymap_exists ( keyboard , keymap ) :
2023-09-28 22:48:20 +02:00
""" Returns the keyboard name if the keyboard+keymap combination exists, otherwise None.
"""
2023-03-16 21:35:49 +01:00
with ignore_logging ( ) :
2023-11-15 06:24:54 +01:00
return keyboard if locate_keymap ( keyboard , keymap ) is not None else None
2023-03-16 21:35:49 +01:00
2023-09-28 22:48:20 +02:00
def _load_keymap_info ( kb_km ) :
""" Returns a tuple of (keyboard, keymap, info.json) for the given keyboard/keymap combination.
"""
2023-03-16 21:35:49 +01:00
with ignore_logging ( ) :
2023-09-28 22:48:20 +02:00
return ( kb_km [ 0 ] , kb_km [ 1 ] , keymap_json ( kb_km [ 0 ] , kb_km [ 1 ] ) )
def expand_make_targets ( targets : List [ str ] ) - > List [ Tuple [ str , str ] ] :
""" Expand a list of make targets into a list of (keyboard, keymap) tuples.
Caters for ' all ' in either keyboard or keymap , or both .
"""
split_targets = [ ]
for target in targets :
split_target = target . split ( ' : ' )
if len ( split_target ) != 2 :
cli . log . error ( f " Invalid build target: { target } " )
return [ ]
split_targets . append ( ( split_target [ 0 ] , split_target [ 1 ] ) )
return expand_keymap_targets ( split_targets )
def _expand_keymap_target ( keyboard : str , keymap : str , all_keyboards : List [ str ] = None ) - > List [ Tuple [ str , str ] ] :
""" Expand a keyboard input and keymap input into a list of (keyboard, keymap) tuples.
Caters for ' all ' in either keyboard or keymap , or both .
"""
if all_keyboards is None :
2023-11-15 06:24:54 +01:00
all_keyboards = list_keyboards ( )
2023-09-28 22:48:20 +02:00
if keyboard == ' all ' :
2023-10-17 00:43:50 +02:00
if keymap == ' all ' :
cli . log . info ( ' Retrieving list of all keyboards and keymaps... ' )
targets = [ ]
for kb in parallel_map ( _all_keymaps , all_keyboards ) :
targets . extend ( kb )
return targets
else :
cli . log . info ( f ' Retrieving list of keyboards with keymap " { keymap } " ... ' )
keyboard_filter = functools . partial ( _keymap_exists , keymap = keymap )
return [ ( kb , keymap ) for kb in filter ( lambda e : e is not None , parallel_map ( keyboard_filter , all_keyboards ) ) ]
2023-09-28 22:48:20 +02:00
else :
2023-03-16 21:35:49 +01:00
if keymap == ' all ' :
2023-09-28 22:48:20 +02:00
cli . log . info ( f ' Retrieving list of keymaps for keyboard " { keyboard } " ... ' )
return _all_keymaps ( keyboard )
2023-03-16 21:35:49 +01:00
else :
2023-11-15 06:24:54 +01:00
return [ ( keyboard , keymap ) ]
2023-09-28 22:48:20 +02:00
def expand_keymap_targets ( targets : List [ Tuple [ str , str ] ] ) - > List [ Tuple [ str , str ] ] :
""" Expand a list of (keyboard, keymap) tuples inclusive of ' all ' , into a list of explicit (keyboard, keymap) tuples.
"""
overall_targets = [ ]
2023-11-15 06:24:54 +01:00
all_keyboards = list_keyboards ( )
2023-09-28 22:48:20 +02:00
for target in targets :
overall_targets . extend ( _expand_keymap_target ( target [ 0 ] , target [ 1 ] , all_keyboards ) )
return list ( sorted ( set ( overall_targets ) ) )
2023-11-22 01:14:34 +01:00
def _construct_build_target_kb_km ( e ) :
return KeyboardKeymapBuildTarget ( keyboard = e [ 0 ] , keymap = e [ 1 ] )
def _construct_build_target_kb_km_json ( e ) :
return KeyboardKeymapBuildTarget ( keyboard = e [ 0 ] , keymap = e [ 1 ] , json = e [ 2 ] )
2023-11-15 06:24:54 +01:00
def _filter_keymap_targets ( target_list : List [ Tuple [ str , str ] ] , filters : List [ str ] = [ ] ) - > List [ BuildTarget ] :
2023-09-28 22:48:20 +02:00
""" Filter a list of (keyboard, keymap) tuples based on the supplied filters.
Optionally includes the values of the queried info . json keys .
"""
2023-11-15 06:24:54 +01:00
if len ( filters ) == 0 :
2023-11-22 01:14:34 +01:00
cli . log . info ( ' Preparing target list... ' )
2023-11-22 02:08:26 +01:00
targets = list ( set ( parallel_map ( _construct_build_target_kb_km , target_list ) ) )
2023-09-28 22:48:20 +02:00
else :
cli . log . info ( ' Parsing data for all matching keyboard/keymap combinations... ' )
2023-10-17 00:43:50 +02:00
valid_keymaps = [ ( e [ 0 ] , e [ 1 ] , dotty ( e [ 2 ] ) ) for e in parallel_map ( _load_keymap_info , target_list ) ]
2023-09-28 22:48:20 +02:00
function_re = re . compile ( r ' ^(?P<function>[a-zA-Z]+) \ ((?P<key>[a-zA-Z0-9_ \ .]+)(, \ s*(?P<value>[^#]+))? \ )$ ' )
equals_re = re . compile ( r ' ^(?P<key>[a-zA-Z0-9_ \ .]+) \ s*= \ s*(?P<value>[^#]+)$ ' )
for filter_expr in filters :
function_match = function_re . match ( filter_expr )
equals_match = equals_re . match ( filter_expr )
if function_match is not None :
func_name = function_match . group ( ' function ' ) . lower ( )
key = function_match . group ( ' key ' )
value = function_match . group ( ' value ' )
if value is not None :
if func_name == ' length ' :
valid_keymaps = filter ( lambda e , key = key , value = value : key in e [ 2 ] and len ( e [ 2 ] . get ( key ) ) == int ( value ) , valid_keymaps )
elif func_name == ' contains ' :
valid_keymaps = filter ( lambda e , key = key , value = value : key in e [ 2 ] and value in e [ 2 ] . get ( key ) , valid_keymaps )
else :
cli . log . warning ( f ' Unrecognized filter expression: { function_match . group ( 0 ) } ' )
continue
2023-03-16 21:35:49 +01:00
2023-09-28 22:48:20 +02:00
cli . log . info ( f ' Filtering on condition: {{ fg_green }} { func_name } {{ fg_reset }} ( {{ fg_cyan }} { key } {{ fg_reset }} , {{ fg_cyan }} { value } {{ fg_reset }} )... ' )
else :
if func_name == ' exists ' :
valid_keymaps = filter ( lambda e , key = key : key in e [ 2 ] , valid_keymaps )
elif func_name == ' absent ' :
valid_keymaps = filter ( lambda e , key = key : key not in e [ 2 ] , valid_keymaps )
2023-05-20 14:14:43 +02:00
else :
2023-09-28 22:48:20 +02:00
cli . log . warning ( f ' Unrecognized filter expression: { function_match . group ( 0 ) } ' )
continue
2023-05-20 14:14:43 +02:00
2023-09-28 22:48:20 +02:00
cli . log . info ( f ' Filtering on condition: {{ fg_green }} { func_name } {{ fg_reset }} ( {{ fg_cyan }} { key } {{ fg_reset }} )... ' )
2023-05-20 14:14:43 +02:00
2023-09-28 22:48:20 +02:00
elif equals_match is not None :
key = equals_match . group ( ' key ' )
value = equals_match . group ( ' value ' )
cli . log . info ( f ' Filtering on condition: {{ fg_cyan }} { key } {{ fg_reset }} == {{ fg_cyan }} { value } {{ fg_reset }} ... ' )
2023-03-16 21:35:49 +01:00
2023-09-28 22:48:20 +02:00
def _make_filter ( k , v ) :
expr = fnmatch . translate ( v )
rule = re . compile ( f ' ^ { expr } $ ' , re . IGNORECASE )
2023-03-16 21:35:49 +01:00
2023-09-28 22:48:20 +02:00
def f ( e ) :
lhs = e [ 2 ] . get ( k )
lhs = str ( False if lhs is None else lhs )
return rule . search ( lhs ) is not None
2023-03-16 21:35:49 +01:00
2023-09-28 22:48:20 +02:00
return f
2023-03-16 21:35:49 +01:00
2023-09-28 22:48:20 +02:00
valid_keymaps = filter ( _make_filter ( key , value ) , valid_keymaps )
else :
cli . log . warning ( f ' Unrecognized filter expression: { filter_expr } ' )
continue
2023-03-16 21:35:49 +01:00
2023-11-22 01:14:34 +01:00
cli . log . info ( ' Preparing target list... ' )
valid_keymaps = [ ( e [ 0 ] , e [ 1 ] , e [ 2 ] . to_dict ( ) if isinstance ( e [ 2 ] , Dotty ) else e [ 2 ] ) for e in valid_keymaps ] # need to convert dotty_dict back to dict because it doesn't survive parallelisation
2023-11-22 02:08:26 +01:00
targets = list ( set ( parallel_map ( _construct_build_target_kb_km_json , list ( valid_keymaps ) ) ) )
2023-03-16 21:35:49 +01:00
return targets
2023-09-28 22:48:20 +02:00
2023-11-15 06:24:54 +01:00
def search_keymap_targets ( targets : List [ Tuple [ str , str ] ] = [ ( ' all ' , ' default ' ) ] , filters : List [ str ] = [ ] ) - > List [ BuildTarget ] :
2023-09-28 22:48:20 +02:00
""" Search for build targets matching the supplied criteria.
"""
2023-11-15 06:24:54 +01:00
return _filter_keymap_targets ( expand_keymap_targets ( targets ) , filters )
2023-09-28 22:48:20 +02:00
2023-11-15 06:24:54 +01:00
def search_make_targets ( targets : List [ str ] , filters : List [ str ] = [ ] ) - > List [ BuildTarget ] :
2023-09-28 22:48:20 +02:00
""" Search for build targets matching the supplied criteria.
"""
2023-11-15 06:24:54 +01:00
return _filter_keymap_targets ( expand_make_targets ( targets ) , filters )