1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
|
"""Functions for searching through QMK keyboards and keymaps.
"""
from dataclasses import dataclass
import contextlib
import functools
import fnmatch
import json
import logging
import re
from typing import Callable, Dict, List, Optional, Tuple, Union
from dotty_dict import dotty, Dotty
from milc import cli
from qmk.util import parallel_map
from qmk.info import keymap_json
from qmk.keyboard import list_keyboards, keyboard_folder
from qmk.keymap import list_keymaps, locate_keymap
from qmk.build_targets import KeyboardKeymapBuildTarget, BuildTarget
@dataclass
class KeyboardKeymapDesc:
keyboard: str
keymap: str
data: dict = None
extra_args: dict = None
def __hash__(self) -> int:
return self.keyboard.__hash__() ^ self.keymap.__hash__() ^ json.dumps(self.extra_args, sort_keys=True).__hash__()
def __lt__(self, other) -> bool:
return (self.keyboard, self.keymap, json.dumps(self.extra_args, sort_keys=True)) < (other.keyboard, other.keymap, json.dumps(other.extra_args, sort_keys=True))
def load_data(self):
data = keymap_json(self.keyboard, self.keymap)
self.data = data.to_dict() if isinstance(data, Dotty) else data
@property
def dotty(self) -> Dotty:
return dotty(self.data) if self.data is not None else None
def to_build_target(self) -> KeyboardKeymapBuildTarget:
target = KeyboardKeymapBuildTarget(keyboard=self.keyboard, keymap=self.keymap, json=self.data)
target.extra_args = self.extra_args
return target
# 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[[KeyboardKeymapDesc], bool]
def __init__(self, key, value):
self.key = key
self.value = value
class Exists(FilterFunction):
func_name = "exists"
def apply(self, target_info: KeyboardKeymapDesc) -> bool:
return self.key in target_info.dotty
class Absent(FilterFunction):
func_name = "absent"
def apply(self, target_info: KeyboardKeymapDesc) -> bool:
return self.key not in target_info.dotty
class Length(FilterFunction):
func_name = "length"
def apply(self, target_info: KeyboardKeymapDesc) -> bool:
info_dotty = target_info.dotty
return (self.key in info_dotty and len(info_dotty[self.key]) == int(self.value))
class Contains(FilterFunction):
func_name = "contains"
def apply(self, target_info: KeyboardKeymapDesc) -> bool:
info_dotty = target_info.dotty
return (self.key in info_dotty and self.value in info_dotty[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]}"
def _set_log_level(level):
cli.acquire_lock()
try:
old = cli.log_level
cli.log_level = level
except AttributeError:
old = cli.log.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) -> List[KeyboardKeymapDesc]:
"""Returns a list of KeyboardKeymapDesc for all keymaps for the given keyboard.
"""
with ignore_logging():
keyboard = keyboard_folder(keyboard)
return [KeyboardKeymapDesc(keyboard, keymap) for keymap in list_keymaps(keyboard)]
def _keymap_exists(keyboard, keymap):
"""Returns the keyboard name if the keyboard+keymap combination exists, otherwise None.
"""
with ignore_logging():
return keyboard if locate_keymap(keyboard, keymap) is not None else None
def _load_keymap_info(target: KeyboardKeymapDesc) -> KeyboardKeymapDesc:
"""Ensures a KeyboardKeymapDesc has its data loaded.
"""
with ignore_logging():
target.load_data() # Ensure we load the data first
return target
def expand_make_targets(targets: List[Union[str, Tuple[str, Dict[str, str]]]]) -> List[KeyboardKeymapDesc]:
"""Expand a list of make targets into a list of KeyboardKeymapDesc.
Caters for 'all' in either keyboard or keymap, or both.
"""
split_targets = []
for target in targets:
extra_args = None
if isinstance(target, tuple):
split_target = target[0].split(':')
extra_args = target[1]
else:
split_target = target.split(':')
if len(split_target) != 2:
cli.log.error(f"Invalid build target: {target}")
return []
split_targets.append(KeyboardKeymapDesc(split_target[0], split_target[1], extra_args=extra_args))
return expand_keymap_targets(split_targets)
def _expand_keymap_target(target: KeyboardKeymapDesc, all_keyboards: List[str] = None) -> List[KeyboardKeymapDesc]:
"""Expand a keyboard input and keymap input into a list of KeyboardKeymapDesc.
Caters for 'all' in either keyboard or keymap, or both.
"""
if all_keyboards is None:
all_keyboards = list_keyboards()
if target.keyboard == 'all':
if target.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)
for t in targets:
t.extra_args = target.extra_args
return targets
else:
cli.log.info(f'Retrieving list of keyboards with keymap "{target.keymap}"...')
keyboard_filter = functools.partial(_keymap_exists, keymap=target.keymap)
return [KeyboardKeymapDesc(kb, target.keymap, extra_args=target.extra_args) for kb in filter(lambda e: e is not None, parallel_map(keyboard_filter, all_keyboards))]
else:
if target.keymap == 'all':
cli.log.info(f'Retrieving list of keymaps for keyboard "{target.keyboard}"...')
targets = _all_keymaps(target.keyboard)
for t in targets:
t.extra_args = target.extra_args
return targets
else:
return [target]
def expand_keymap_targets(targets: List[KeyboardKeymapDesc]) -> List[KeyboardKeymapDesc]:
"""Expand a list of KeyboardKeymapDesc inclusive of 'all', into a list of explicit KeyboardKeymapDesc.
"""
overall_targets = []
all_keyboards = list_keyboards()
for target in targets:
overall_targets.extend(_expand_keymap_target(target, all_keyboards))
return list(sorted(set(overall_targets)))
def _construct_build_target(e: KeyboardKeymapDesc):
return e.to_build_target()
def _filter_keymap_targets(target_list: List[KeyboardKeymapDesc], filters: List[str] = []) -> List[KeyboardKeymapDesc]:
"""Filter a list of KeyboardKeymapDesc based on the supplied filters.
Optionally includes the values of the queried info.json keys.
"""
if len(filters) == 0:
cli.log.info('Preparing target list...')
targets = target_list
else:
cli.log.info('Parsing data for all matching keyboard/keymap combinations...')
valid_targets = parallel_map(_load_keymap_info, target_list)
function_re = re.compile(r'^(?P<function>[a-zA-Z]+)\((?P<key>[a-zA-Z0-9_\.]+)(,\s*(?P<value>[^#]+))?\)$')
comparison_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*(?P<op>[\<\>\!=]=|\<|\>)\s*(?P<value>[^#]+)$')
for filter_expr in filters:
function_match = function_re.match(filter_expr)
comparison_match = comparison_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')
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_targets = filter(filter_class.apply, valid_targets)
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})...')
elif comparison_match is not None:
key = comparison_match.group('key')
op = comparison_match.group('op')
value = comparison_match.group('value')
cli.log.info(f'Filtering on condition: {{fg_cyan}}{key}{{fg_reset}} {op} {{fg_cyan}}{value}{{fg_reset}}...')
def _make_filter(k, o, v):
expr = fnmatch.translate(v)
rule = re.compile(f'^{expr}$', re.IGNORECASE)
def f(e: KeyboardKeymapDesc):
lhs = e.dotty.get(k)
rhs = v
if o in ['<', '>', '<=', '>=']:
lhs = int(False if lhs is None else lhs)
rhs = int(rhs)
if o == '<':
return lhs < rhs
elif o == '>':
return lhs > rhs
elif o == '<=':
return lhs <= rhs
elif o == '>=':
return lhs >= rhs
else:
lhs = str(False if lhs is None else lhs)
if o == '!=':
return rule.search(lhs) is None
elif o == '==':
return rule.search(lhs) is not None
return f
valid_targets = filter(_make_filter(key, op, value), valid_targets)
else:
cli.log.warning(f'Unrecognized filter expression: {filter_expr}')
continue
cli.log.info('Preparing target list...')
targets = list(sorted(set(valid_targets)))
return targets
def search_keymap_targets(targets: List[Union[Tuple[str, str], Tuple[str, str, Dict[str, str]]]] = [('all', 'default')], filters: List[str] = []) -> List[BuildTarget]:
"""Search for build targets matching the supplied criteria.
"""
def _make_desc(e):
if len(e) == 3:
return KeyboardKeymapDesc(keyboard=e[0], keymap=e[1], extra_args=e[2])
else:
return KeyboardKeymapDesc(keyboard=e[0], keymap=e[1])
targets = map(_make_desc, targets)
targets = _filter_keymap_targets(expand_keymap_targets(targets), filters)
targets = list(set(parallel_map(_construct_build_target, list(targets))))
return sorted(targets)
def search_make_targets(targets: List[Union[str, Tuple[str, Dict[str, str]]]], filters: List[str] = []) -> List[BuildTarget]:
"""Search for build targets matching the supplied criteria.
"""
targets = _filter_keymap_targets(expand_make_targets(targets), filters)
targets = list(set(parallel_map(_construct_build_target, list(targets))))
return sorted(targets)
|