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
2024-02-17 13:28:40 +01:00
from typing import Callable , List , Optional , 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
2024-02-17 13:28:40 +01:00
TargetInfo = Tuple [ str , str , dict ]
# by using a class for filters, we dont need to worry about capturing values
# see details <https://github.com/qmk/qmk_firmware/pull/21090>
class FilterFunction :
""" Base class for filters.
It provides :
- __init__ : capture key and value
Each subclass should provide :
- func_name : how it will be specified on CLI
>> > qmk find - f < func_name > . . .
- apply : function that actually applies the filter
ie : return whether the input kb / km satisfies the condition
"""
key : str
value : Optional [ str ]
func_name : str
apply : Callable [ [ TargetInfo ] , bool ]
def __init__ ( self , key , value ) :
self . key = key
self . value = value
class Exists ( FilterFunction ) :
func_name = " exists "
def apply ( self , target_info : TargetInfo ) - > bool :
_kb , _km , info = target_info
return self . key in info
class Absent ( FilterFunction ) :
func_name = " absent "
def apply ( self , target_info : TargetInfo ) - > bool :
_kb , _km , info = target_info
return self . key not in info
class Length ( FilterFunction ) :
func_name = " length "
def apply ( self , target_info : TargetInfo ) - > bool :
_kb , _km , info = target_info
return ( self . key in info and len ( info [ self . key ] ) == int ( self . value ) )
class Contains ( FilterFunction ) :
func_name = " contains "
def apply ( self , target_info : TargetInfo ) - > bool :
_kb , _km , info = target_info
return ( self . key in info and self . value in info [ self . key ] )
def _get_filter_class ( func_name : str , key : str , value : str ) - > Optional [ FilterFunction ] :
""" Initialize a filter subclass based on regex findings and return it.
None if no there ' s no filter with the name queried.
"""
for subclass in FilterFunction . __subclasses__ ( ) :
if func_name == subclass . func_name :
return subclass ( key , value )
return None
def filter_help ( ) - > str :
names = [ f " ' { f . func_name } ' " for f in FilterFunction . __subclasses__ ( ) ]
return " , " . join ( names [ : - 1 ] ) + f " and { names [ - 1 ] } "
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
2024-02-17 13:28:40 +01:00
def _load_keymap_info ( target : Tuple [ str , str ] ) - > TargetInfo :
2023-09-28 22:48:20 +02:00
""" Returns a tuple of (keyboard, keymap, info.json) for the given keyboard/keymap combination.
"""
2024-02-17 13:28:40 +01:00
kb , km = target
2023-03-16 21:35:49 +01:00
with ignore_logging ( ) :
2024-02-17 13:28:40 +01:00
return ( kb , km , keymap_json ( kb , km ) )
2023-09-28 22:48:20 +02:00
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 ' )
2024-02-17 13:28:40 +01:00
filter_class = _get_filter_class ( func_name , key , value )
if filter_class is None :
cli . log . warning ( f ' Unrecognized filter expression: { function_match . group ( 0 ) } ' )
continue
valid_keymaps = filter ( filter_class . apply , valid_keymaps )
2024-05-11 11:03:16 +02:00
value_str = f " , {{ fg_cyan }} { value } {{ fg_reset }} " if value is not None else " "
cli . log . info ( f ' Filtering on condition: {{ fg_green }} { func_name } {{ fg_reset }} ( {{ fg_cyan }} { key } {{ fg_reset }} { value_str } )... ' )
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 )