summaryrefslogtreecommitdiff
path: root/.gitlab-ci/generate-evdev-keycodes.py
blob: 1d6ad774208e8d516d37b04f57427cecd7a56845 (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
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
#!/usr/bin/env python3
#
# Generate the keycodes/evdev file from the names defined in
# linux/input-event-codes.h
#
# Note that this script relies on libevdev to provide the key names and
# those are compiled in. Ensure you have a recent-enough libevdev to
# generate this list.
#

import argparse
import contextlib
import re
import sys

try:
    import libevdev
except ImportError:
    print(
        "WARNING: python-libevdev not available, cannot check for new evdev keycodes",
        file=sys.stderr,
    )
    sys.exit(77)


# The marker to search for in the template file, replaced with our generated
# codes.
replacement_marker = "@evdevkeys@"

# These markers are put into the result file and are used to detect
# the section that we added when parsing an existing file.
section_header = "Key codes below are autogenerated"
section_footer = "End of autogenerated key codes"


def evdev_codes():
    """
    Return the dict {code, name} for all known evdev codes.

    The list of names is compiled into libevdev.so, use a recent libevdev
    release to get the most up-to-date list.
    """
    codes = {}
    for c in libevdev.EV_KEY.codes:
        # 112 because that's where our 1:1 keycode entries historically
        # started.
        # Undefined keys are those with a code < KEY_MAX but without a
        # #define in the kernel header file
        if c.value < 112 or not c.is_defined:
            continue

        if c.name.startswith("BTN_") or c.name == "KEY_MAX":
            continue

        codes[c.value] = c.name

    return codes


def existing_keys(lines):
    """
    Return the dict {code, name} for all existing keycodes in the templates
    file.

    This is a very simple parser, good enough for the keycodes/evdev file
    but that's about it.
    """
    pattern = re.compile(r"\s+\<([^>]+)\>\s+=\s+(\d+);")
    keys = {}
    for line in lines:
        match = re.match(pattern, line)
        if not match:
            continue
        keys[int(match.group(2))] = match.group(1)

    return keys


def generate_keycodes_file(template, codes):
    """
    Generate a new keycodes/evdev file with line containing @evdevkeys@
    replaced by the full list of known evdev key codes, including our
    section_header/footer. Expected output:

    ::

    // $section_header
    <I$keycode> = <$keycode + 8> // #define $kernel_name
    ...
    // $section_footer

    """
    lines = template.readlines()
    existing = existing_keys(lines)

    output = []
    for line in lines:
        if replacement_marker not in line:
            output.append(line)
            continue

        output.append(f"\t// {section_header}\n")

        warned = False
        for code, name in codes.items():
            xkeycode = code + 8

            if xkeycode > 255 and not warned:
                warned = True
                output.append("\n")
                output.append("\t// Key codes below cannot be used in X\n")
                output.append("\n")

            if xkeycode in existing:
                output.append(
                    f"\talias <I{xkeycode}> = <{existing[xkeycode]}>;	// #define {name:23s} {code}\n"
                )
                continue

            output.append(
                f"\t<I{xkeycode}> = {xkeycode};\t\t// #define {name:23s} {code}\n"
            )

        output.append(f"\t// {section_footer}\n")

    return output


def extract_generated_keycodes(fp):
    """
    Return an iterator the keycode of any <I123> keys between the section
    header and footer.
    """
    in_generated_section = False
    pattern = re.compile(".*<I([0-9]*)>.*")

    for line in fp:
        if section_header in line:
            in_generated_section = True
            continue
        elif section_footer in line:
            return
        elif in_generated_section:
            match = pattern.match(line)
            if match:
                yield int(match[1])


def compare_with(codes, oldfile):
    """
    Extract the <I123> keycodes from between the section_header/footer of
    oldfile and return a list of keycodes that are in codes but not in
    oldfile.
    """
    old_keycodes = extract_generated_keycodes(oldfile)
    keycodes = [c + 8 for c in codes]  # X keycode offset

    # This does not detect keycodes in old_keycode but not in the new
    # generated list - should never happen anyway.
    return sorted(set(keycodes).difference(old_keycodes))


def log_msg(msg):
    print(msg, file=sys.stderr)


def main():
    parser = argparse.ArgumentParser(description="Generate the evdev keycode lists.")
    parser.add_argument(
        "--template",
        type=argparse.FileType("r"),
        default=open(".gitlab-ci/evdev.in"),
        help="The template file (default: .gitlab-ci/evdev.in)",
    )
    parser.add_argument(
        "--output",
        type=str,
        default="keycodes/evdev",
        required=False,
        help="The file to be written to (default: keycodes/evdev)",
    )
    parser.add_argument(
        "--compare-with",
        type=argparse.FileType("r"),
        default=open("keycodes/evdev"),
        help="Compare generated output with the given file (default: keycodes/evdev)",
    )
    parser.add_argument(
        "--verbose",
        action=argparse.BooleanOptionalAction,
        help="Print verbose output to stderr",
    )
    ns = parser.parse_args()

    codes = evdev_codes()
    rc = 0
    if ns.verbose:
        kmin, kmax = min(codes.keys()), max(codes.keys())
        log_msg(f"evdev keycode range: {kmin} ({kmin:#x}) → {kmax} ({kmax:#x})")

    # We compare before writing so we can use the same filename for
    # --compare-with and --output. That's also why --output has to be type
    # str instead of FileType('w').
    if ns.compare_with:
        diff = compare_with(codes, ns.compare_with)
        if diff:
            rc = 1
            if ns.verbose:
                log_msg(
                    f"File {ns.compare_with.name} is out of date, missing keycodes:"
                )
                for k in diff:
                    name = codes[k - 8]  # remove the X offset
                    log_msg(f"  <I{k}> // #define {name}")

    with contextlib.ExitStack() as stack:
        if ns.output == "-":
            fd = sys.stdout
        else:
            fd = stack.enter_context(open(ns.output, "w"))
        output = generate_keycodes_file(ns.template, codes)
        fd.write("".join(output))

    sys.exit(rc)


if __name__ == "__main__":
    main()