summaryrefslogtreecommitdiff
path: root/lib/python/qmk/search.py
blob: 2bbbc7806fc6d69454aadc7094adba2a81c49c4b (plain)
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
"""Functions for searching through QMK keyboards and keymaps.
"""
import contextlib
import fnmatch
import logging
import multiprocessing
import re
from dotty_dict import dotty
from milc import cli

from qmk.info import keymap_json
import qmk.keyboard
import qmk.keymap


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):
    with ignore_logging():
        return (keyboard, qmk.keymap.list_keymaps(keyboard))


def _keymap_exists(keyboard, keymap):
    with ignore_logging():
        return keyboard if qmk.keymap.locate_keymap(keyboard, keymap) is not None else None


def _load_keymap_info(keyboard, keymap):
    with ignore_logging():
        return (keyboard, keymap, keymap_json(keyboard, keymap))


def search_keymap_targets(keymap='default', filters=[], print_vals=[]):
    targets = []

    with multiprocessing.Pool() as pool:
        cli.log.info(f'Retrieving list of keyboards with keymap "{keymap}"...')
        target_list = []
        if keymap == 'all':
            kb_to_kms = pool.map(_all_keymaps, qmk.keyboard.list_keyboards())
            for targets in kb_to_kms:
                keyboard = targets[0]
                keymaps = targets[1]
                target_list.extend([(keyboard, keymap) for keymap in keymaps])
        else:
            target_list = [(kb, keymap) for kb in filter(lambda kb: kb is not None, pool.starmap(_keymap_exists, [(kb, keymap) for kb in qmk.keyboard.list_keyboards()]))]

        if len(filters) == 0:
            targets = [(kb, km, {}) for kb, km in target_list]
        else:
            cli.log.info('Parsing data for all matching keyboard/keymap combinations...')
            valid_keymaps = [(e[0], e[1], dotty(e[2])) for e in pool.starmap(_load_keymap_info, target_list)]

            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

                        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)
                        else:
                            cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}')
                            continue

                        cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}})...')

                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}}...')

                    def _make_filter(k, v):
                        expr = fnmatch.translate(v)
                        rule = re.compile(f'^{expr}$', re.IGNORECASE)

                        def f(e):
                            lhs = e[2].get(k)
                            lhs = str(False if lhs is None else lhs)
                            return rule.search(lhs) is not None

                        return f

                    valid_keymaps = filter(_make_filter(key, value), valid_keymaps)
                else:
                    cli.log.warning(f'Unrecognized filter expression: {filter_expr}')
                    continue

            targets = [(e[0], e[1], [(p, e[2].get(p)) for p in print_vals]) for e in valid_keymaps]

    return targets