1d63b911fSmrg#!/usr/bin/env python3
2d63b911fSmrg#
3d63b911fSmrg# SPDX-License-Identifier: MIT
4d63b911fSmrg#
5d63b911fSmrg# This script checks XF86keysym.h for the reserved evdev keysym range and/or
6d63b911fSmrg# appends new keysym to that range. An up-to-date libevdev must be
7d63b911fSmrg# available to guarantee the correct keycode ranges and names.
8d63b911fSmrg#
9d63b911fSmrg# Run with --help for usage information.
10d63b911fSmrg#
11d63b911fSmrg#
12d63b911fSmrg# File is formatted with Python Black
13d63b911fSmrg
14d63b911fSmrgimport argparse
15d63b911fSmrgimport logging
165395ea76Smrgimport os
17d63b911fSmrgimport sys
18d63b911fSmrgimport re
19d63b911fSmrgimport subprocess
20d63b911fSmrgfrom pathlib import Path
21d63b911fSmrg
225395ea76Smrgtry:
235395ea76Smrg    import libevdev
245395ea76Smrgexcept ModuleNotFoundError as e:
255395ea76Smrg    print(f"Error: {e}", file=sys.stderr)
265395ea76Smrg    print(
275395ea76Smrg        "One or more python modules are missing. Please install those "
285395ea76Smrg        "modules and re-run this tool."
295395ea76Smrg    )
305395ea76Smrg    sys.exit(77)
315395ea76Smrg
325395ea76Smrg
33d63b911fSmrglogging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
34d63b911fSmrglogger = logging.getLogger("ksgen")
35d63b911fSmrg
36d63b911fSmrgstart_token = re.compile(r"#define _EVDEVK.*")
37d63b911fSmrgend_token = re.compile(r"#undef _EVDEVK\n")
38d63b911fSmrg
39d63b911fSmrg
40d63b911fSmrgdef die(msg):
41d63b911fSmrg    logger.critical(msg)
42d63b911fSmrg    sys.exit(1)
43d63b911fSmrg
44d63b911fSmrg
45d63b911fSmrgdef all_keysyms(directory):
46d63b911fSmrg    """
47d63b911fSmrg    Extract the key names for all keysyms we have in our repo and return
48d63b911fSmrg    them as list.
49d63b911fSmrg    """
50d63b911fSmrg    keysym_names = []
51d63b911fSmrg    pattern = re.compile(r"^#define\s+(?P<name>\w+)\s+(0x[0-9A-Fa-f]+)")
52d63b911fSmrg    for path in directory.glob("*keysym*.h"):
53d63b911fSmrg        with open(path) as fd:
54d63b911fSmrg            for line in fd:
55d63b911fSmrg                match = re.match(pattern, line)
56d63b911fSmrg                if match:
57d63b911fSmrg                    keysym_names.append(match.group("name"))
58d63b911fSmrg    return keysym_names
59d63b911fSmrg
60d63b911fSmrg
61d63b911fSmrgclass Kernel(object):
62d63b911fSmrg    """
63d63b911fSmrg    Wrapper around the kernel git tree to simplify searching for when a
64d63b911fSmrg    particular keycode was introduced.
65d63b911fSmrg    """
66d63b911fSmrg
67d63b911fSmrg    def __init__(self, repo):
68d63b911fSmrg        self.repo = repo
69d63b911fSmrg
70d63b911fSmrg        exitcode, stdout, stderr = self.git_command("git branch --show-current")
71d63b911fSmrg        if exitcode != 0:
72d63b911fSmrg            die(f"{stderr}")
73d63b911fSmrg        if stdout.strip() != "master":
74d63b911fSmrg            die(f"Kernel repo must be on the master branch (current: {stdout.strip()})")
75d63b911fSmrg
76d63b911fSmrg        exitcode, stdout, stderr = self.git_command("git tag --sort=version:refname")
77d63b911fSmrg        tags = stdout.split("\n")
78d63b911fSmrg        self.versions = list(
79d63b911fSmrg            filter(lambda v: re.match(r"^v[2-6]\.[0-9]+(\.[0-9]+)?$", v), tags)
80d63b911fSmrg        )
81d63b911fSmrg        logger.debug(f"Kernel versions: {', '.join(self.versions)}")
82d63b911fSmrg
83d63b911fSmrg    def git_command(self, cmd):
84d63b911fSmrg        """
85d63b911fSmrg        Takes a single-string git command and runs it in the repo.
86d63b911fSmrg
87d63b911fSmrg        Returns the tuple (exitcode, stdout, stderr)
88d63b911fSmrg        """
89d63b911fSmrg        # logger.debug(f"git command: {cmd}")
90d63b911fSmrg        try:
91d63b911fSmrg            result = subprocess.run(
92d63b911fSmrg                cmd.split(" "), cwd=self.repo, capture_output=True, encoding="utf8"
93d63b911fSmrg            )
94d63b911fSmrg            if result.returncode == 128:
95d63b911fSmrg                die(f"{result.stderr}")
96d63b911fSmrg
97d63b911fSmrg            return result.returncode, result.stdout, result.stderr
98d63b911fSmrg        except FileNotFoundError:
99d63b911fSmrg            die(f"{self.repo} is not a git repository")
100d63b911fSmrg
101d63b911fSmrg    def introduced_in_version(self, string):
102d63b911fSmrg        """
103d63b911fSmrg        Search this repo for the first version with string in the headers.
104d63b911fSmrg
105d63b911fSmrg        Returns the kernel version number (e.g. "v5.10") or None
106d63b911fSmrg        """
107d63b911fSmrg
108d63b911fSmrg        # The fastest approach is to git grep every version for the string
109d63b911fSmrg        # and return the first. Using git log -G and then git tag --contains
110d63b911fSmrg        # is an order of magnitude slower.
111d63b911fSmrg        def found_in_version(v):
112d63b911fSmrg            cmd = f"git grep -E \\<{string}\\> {v} -- include/"
113d63b911fSmrg            exitcode, _, _ = self.git_command(cmd)
114d63b911fSmrg            return exitcode == 0
115d63b911fSmrg
116d63b911fSmrg        def bisect(iterable, func):
117d63b911fSmrg            """
118d63b911fSmrg            Return the first element in iterable for which func
119d63b911fSmrg            returns True.
120d63b911fSmrg            """
121d63b911fSmrg            # bias to speed things up: most keycodes will be in the first
122d63b911fSmrg            # kernel version
123d63b911fSmrg            if func(iterable[0]):
124d63b911fSmrg                return iterable[0]
125d63b911fSmrg
126d63b911fSmrg            lo, hi = 0, len(iterable)
127d63b911fSmrg            while lo < hi:
128d63b911fSmrg                mid = (lo + hi) // 2
129d63b911fSmrg                if func(iterable[mid]):
130d63b911fSmrg                    hi = mid
131d63b911fSmrg                else:
132d63b911fSmrg                    lo = mid + 1
133d63b911fSmrg            return iterable[hi]
134d63b911fSmrg
135d63b911fSmrg        version = bisect(self.versions, found_in_version)
136d63b911fSmrg        logger.debug(f"Bisected {string} to {version}")
137d63b911fSmrg        # 2.6.11 doesn't count, that's the start of git
138d63b911fSmrg        return version if version != self.versions[0] else None
139d63b911fSmrg
140d63b911fSmrg
141635a5eb8SmrgTARGET_KEYSYM_COLUMN = 54
142635a5eb8Smrg"""Column in the file we want the keysym codes to end"""
143635a5eb8SmrgKEYSYM_NAME_MAX_LENGTH = TARGET_KEYSYM_COLUMN - len("#define ") - len("_EVDEVK(0xNNN)")
144635a5eb8SmrgKERNEL_VERSION_PADDING = 7
145635a5eb8Smrg
146635a5eb8Smrg
147d63b911fSmrgdef generate_keysym_line(code, kernel, kver_list=[]):
148d63b911fSmrg    """
149d63b911fSmrg    Generate the line to append to the keysym file.
150d63b911fSmrg
151d63b911fSmrg    This format is semi-ABI, scripts rely on the format of this line (e.g. in
152d63b911fSmrg    xkeyboard-config).
153d63b911fSmrg    """
154d63b911fSmrg    evcode = libevdev.evbit(libevdev.EV_KEY.value, code)
155d63b911fSmrg    if not evcode.is_defined:  # codes without a #define in the kernel
156d63b911fSmrg        return None
157d63b911fSmrg    if evcode.name.startswith("BTN_"):
158d63b911fSmrg        return None
159d63b911fSmrg
160d63b911fSmrg    name = "".join([s.capitalize() for s in evcode.name[4:].lower().split("_")])
161d63b911fSmrg    keysym = f"XF86XK_{name}"
162635a5eb8Smrg    spaces = KEYSYM_NAME_MAX_LENGTH - len(keysym)
163635a5eb8Smrg    if not spaces:
164635a5eb8Smrg        raise ValueError(f"Insufficient padding for keysym “{keysym}”.")
165d63b911fSmrg    kver = kernel.introduced_in_version(evcode.name) or " "
166d63b911fSmrg    if kver_list:
167d63b911fSmrg        from fnmatch import fnmatch
168d63b911fSmrg
169d63b911fSmrg        allowed_kvers = [v.strip() for v in kver_list.split(",")]
170d63b911fSmrg        for allowed in allowed_kvers:
171d63b911fSmrg            if fnmatch(kver, allowed):
172d63b911fSmrg                break
173d63b911fSmrg        else:  # no match
174d63b911fSmrg            return None
175d63b911fSmrg
176635a5eb8Smrg    return f"#define {keysym}{' ' * spaces}_EVDEVK(0x{code:03x})  /* {kver: <{KERNEL_VERSION_PADDING}s} {evcode.name} */"
177d63b911fSmrg
178d63b911fSmrg
179d63b911fSmrgdef verify(ns):
180d63b911fSmrg    """
181d63b911fSmrg    Verify that the XF86keysym.h file follows the requirements. Since we expect
182d63b911fSmrg    the header file to be parsed by outside scripts, the requirements for the format
183d63b911fSmrg    are quite strict, including things like correct-case hex codes.
184d63b911fSmrg    """
185d63b911fSmrg
186d63b911fSmrg    # No other keysym must use this range
187d63b911fSmrg    reserved_range = re.compile(r"#define.*0x10081.*")
188d63b911fSmrg    normal_range = re.compile(r"#define.*0x1008.*")
189d63b911fSmrg
190d63b911fSmrg    # This is the full pattern we expect.
191d63b911fSmrg    expected_pattern = re.compile(
192635a5eb8Smrg        r"#define XF86XK_\w+ +_EVDEVK\(0x([0-9A-Fa-f]{3})\) +/\* (v[2-6]\.[0-9]+(\.[0-9]+)?)? +KEY_\w+ \*/"
193d63b911fSmrg    )
194d63b911fSmrg    # This is the comment pattern we expect
195d63b911fSmrg    expected_comment_pattern = re.compile(
196635a5eb8Smrg        r"/\* Use: (?P<name>\w+) +_EVDEVK\(0x(?P<value>[0-9A-Fa-f]{3})\) +   (v[2-6]\.[0-9]+(\.[0-9]+)?)? +KEY_\w+ \*/"
197d63b911fSmrg    )
198d63b911fSmrg
199d63b911fSmrg    # Some patterns to spot specific errors, just so we can print useful errors
200d63b911fSmrg    define = re.compile(r"^#define .*")
201d63b911fSmrg    name_pattern = re.compile(r"#define (XF86XK_[^\s]*)")
202635a5eb8Smrg    space_check = re.compile(r"#define \w+(\s+)[^\s]+(\s+)")
203d63b911fSmrg    hex_pattern = re.compile(r".*0x([a-f0-9]+).*", re.I)
204d63b911fSmrg    comment_format = re.compile(r".*/\* ([^\s]+)?\s+(\w+)")
205d63b911fSmrg    kver_format = re.compile(r"v[2-6]\.[0-9]+(\.[0-9]+)?")
206d63b911fSmrg
207d63b911fSmrg    in_evdev_codes_section = False
208d63b911fSmrg    had_evdev_codes_section = False
209d63b911fSmrg    success = True
210d63b911fSmrg
211d63b911fSmrg    all_defines = []
212d63b911fSmrg
213d63b911fSmrg    all_keysym_names = all_keysyms(ns.header.parent)
214d63b911fSmrg
215d63b911fSmrg    class ParserError(Exception):
216d63b911fSmrg        pass
217d63b911fSmrg
218d63b911fSmrg    def error(msg, line):
219d63b911fSmrg        raise ParserError(f"{msg} in '{line.strip()}'")
220d63b911fSmrg
221d63b911fSmrg    last_keycode = 0
222d63b911fSmrg    for line in open(ns.header):
223d63b911fSmrg        try:
224d63b911fSmrg            if not in_evdev_codes_section:
225d63b911fSmrg                if re.match(start_token, line):
226d63b911fSmrg                    in_evdev_codes_section = True
227d63b911fSmrg                    had_evdev_codes_section = True
228d63b911fSmrg                    continue
229d63b911fSmrg
230d63b911fSmrg                if re.match(reserved_range, line):
231d63b911fSmrg                    error("Using reserved range", line)
232d63b911fSmrg                match = re.match(name_pattern, line)
233d63b911fSmrg                if match:
234d63b911fSmrg                    all_defines.append(match.group(1))
235d63b911fSmrg            else:
236d63b911fSmrg                # Within the evdev defines section
237d63b911fSmrg                if re.match(end_token, line):
238d63b911fSmrg                    in_evdev_codes_section = False
239d63b911fSmrg                    continue
240d63b911fSmrg
241d63b911fSmrg                # Comments we only search for a hex pattern and where there is one present
242635a5eb8Smrg                # we only check for lower case format, ordering and update our last_keycode.
243d63b911fSmrg                if not re.match(define, line):
244d63b911fSmrg                    match = re.match(expected_comment_pattern, line)
245d63b911fSmrg                    if match:
246d63b911fSmrg                        hexcode = match.group("value")
247635a5eb8Smrg                        if hexcode != hexcode.lower():
248635a5eb8Smrg                            error(f"Hex code 0x{hexcode} must be lower case", line)
249d63b911fSmrg                        if hexcode:
250d63b911fSmrg                            keycode = int(hexcode, 16)
251d63b911fSmrg                            if keycode < last_keycode:
252d63b911fSmrg                                error("Keycode must be ascending", line)
253d63b911fSmrg                            if keycode == last_keycode:
254d63b911fSmrg                                error("Duplicate keycode", line)
255d63b911fSmrg                            last_keycode = keycode
256d63b911fSmrg
257d63b911fSmrg                        name = match.group("name")
258d63b911fSmrg                        if name not in all_keysym_names:
259d63b911fSmrg                            error(f"Unknown keysym {name}", line)
260d63b911fSmrg                    elif re.match(hex_pattern, line):
261d63b911fSmrg                        logger.warning(f"Unexpected hex code in {line}")
262d63b911fSmrg                    continue
263d63b911fSmrg
264d63b911fSmrg                # Anything below here is a #define line
265d63b911fSmrg                # Let's check for specific errors
266d63b911fSmrg                if re.match(normal_range, line):
267d63b911fSmrg                    error("Define must use _EVDEVK", line)
268d63b911fSmrg
269d63b911fSmrg                match = re.match(name_pattern, line)
270d63b911fSmrg                if match:
271d63b911fSmrg                    if match.group(1) in all_defines:
272d63b911fSmrg                        error("Duplicate define", line)
273d63b911fSmrg                    all_defines.append(match.group(1))
274d63b911fSmrg                else:
275d63b911fSmrg                    error("Typo", line)
276d63b911fSmrg
277d63b911fSmrg                match = re.match(hex_pattern, line)
278d63b911fSmrg                if not match:
279d63b911fSmrg                    error("No hex code", line)
280635a5eb8Smrg                if match.group(1) != match.group(1).lower():
281635a5eb8Smrg                    error(f"Hex code 0x{match.group(1)} must be lowercase", line)
282d63b911fSmrg
283635a5eb8Smrg                spaces = re.match(space_check, line)
284635a5eb8Smrg                if not spaces:  # bug
285d63b911fSmrg                    error("Matching error", line)
286635a5eb8Smrg                if "\t" in spaces.group(1) or "\t" in spaces.group(2):
287635a5eb8Smrg                    error("Use spaces, not tabs", line)
288d63b911fSmrg
289d63b911fSmrg                comment = re.match(comment_format, line)
290d63b911fSmrg                if not comment:
291d63b911fSmrg                    error("Invalid comment format", line)
292d63b911fSmrg                kver = comment.group(1)
293d63b911fSmrg                if kver and not re.match(kver_format, kver):
294d63b911fSmrg                    error("Invalid kernel version format", line)
295d63b911fSmrg
296d63b911fSmrg                keyname = comment.group(2)
297d63b911fSmrg                if not keyname.startswith("KEY_") or keyname.upper() != keyname:
298d63b911fSmrg                    error("Kernel keycode name invalid", line)
299d63b911fSmrg
300d63b911fSmrg                # This could be an old libevdev
301d63b911fSmrg                if keyname not in [c.name for c in libevdev.EV_KEY.codes]:
302d63b911fSmrg                    logger.warning(f"Unknown kernel keycode name {keyname}")
303d63b911fSmrg
304d63b911fSmrg                # Check the full expected format, no better error messages
305d63b911fSmrg                # available if this fails
306d63b911fSmrg                match = re.match(expected_pattern, line)
307d63b911fSmrg                if not match:
308d63b911fSmrg                    error("Failed match", line)
309d63b911fSmrg
310d63b911fSmrg                keycode = int(match.group(1), 16)
311d63b911fSmrg                if keycode < last_keycode:
312d63b911fSmrg                    error("Keycode must be ascending", line)
313d63b911fSmrg                if keycode == last_keycode:
314d63b911fSmrg                    error("Duplicate keycode", line)
315d63b911fSmrg
316d63b911fSmrg                # May cause a false positive for old libevdev if KEY_MAX is bumped
317d63b911fSmrg                if keycode < 0x0A0 or keycode > libevdev.EV_KEY.KEY_MAX.value:
318d63b911fSmrg                    error("Keycode outside range", line)
319d63b911fSmrg
320d63b911fSmrg                last_keycode = keycode
321d63b911fSmrg        except ParserError as e:
322d63b911fSmrg            logger.error(e)
323d63b911fSmrg            success = False
324d63b911fSmrg
325d63b911fSmrg    if not had_evdev_codes_section:
326d63b911fSmrg        logger.error("Unable to locate EVDEVK section")
327d63b911fSmrg        success = False
328d63b911fSmrg    elif in_evdev_codes_section:
329d63b911fSmrg        logger.error("Unterminated EVDEVK section")
330d63b911fSmrg        success = False
331d63b911fSmrg
332d63b911fSmrg    if success:
333d63b911fSmrg        logger.info("Verification succeeded")
334d63b911fSmrg
335d63b911fSmrg    return 0 if success else 1
336d63b911fSmrg
337d63b911fSmrg
338d63b911fSmrgdef add_keysyms(ns):
339d63b911fSmrg    """
340d63b911fSmrg    Print a new XF86keysym.h file, adding any *missing* keycodes to the existing file.
341d63b911fSmrg    """
342d63b911fSmrg    if verify(ns) != 0:
343d63b911fSmrg        die("Header file verification failed")
344d63b911fSmrg
345d63b911fSmrg    # If verification succeeds, we can be a bit more lenient here because we already know
346d63b911fSmrg    # what the format of the field is. Specifically, we're searching for
347d63b911fSmrg    # 3-digit hexcode in brackets and use that as keycode.
348635a5eb8Smrg    pattern = re.compile(r".*_EVDEVK\((0x[0-9A-Fa-f]{3})\).*")
349d63b911fSmrg    max_code = max(
350d63b911fSmrg        [
351d63b911fSmrg            c.value
352d63b911fSmrg            for c in libevdev.EV_KEY.codes
353d63b911fSmrg            if c.is_defined
354d63b911fSmrg            and c != libevdev.EV_KEY.KEY_MAX
355d63b911fSmrg            and not c.name.startswith("BTN")
356d63b911fSmrg        ]
357d63b911fSmrg    )
358d63b911fSmrg
359d63b911fSmrg    def defined_keycodes(path):
360d63b911fSmrg        """
361d63b911fSmrg        Returns an iterator to the next #defined (or otherwise mentioned)
362d63b911fSmrg        keycode, all other lines (including the returned one) are passed
363d63b911fSmrg        through to printf.
364d63b911fSmrg        """
365d63b911fSmrg        with open(path) as fd:
366d63b911fSmrg            in_evdev_codes_section = False
367d63b911fSmrg
368d63b911fSmrg            for line in fd:
369d63b911fSmrg                if not in_evdev_codes_section:
370d63b911fSmrg                    if re.match(start_token, line):
371d63b911fSmrg                        in_evdev_codes_section = True
372d63b911fSmrg                    # passthrough for all other lines
373d63b911fSmrg                    print(line, end="")
374d63b911fSmrg                else:
375d63b911fSmrg                    if re.match(r"#undef _EVDEVK\n", line):
376d63b911fSmrg                        in_evdev_codes_section = False
377d63b911fSmrg                        yield max_code
378d63b911fSmrg                    else:
379d63b911fSmrg                        match = re.match(pattern, line)
380d63b911fSmrg                        if match:
381d63b911fSmrg                            logger.debug(f"Found keycode in {line.strip()}")
382d63b911fSmrg                            yield int(match.group(1), 16)
383d63b911fSmrg                    print(line, end="")
384d63b911fSmrg
385d63b911fSmrg    kernel = Kernel(ns.kernel_git_tree)
386d63b911fSmrg    prev_code = 255 - 8  # the last keycode we can map directly in X
387d63b911fSmrg    for code in defined_keycodes(ns.header):
388d63b911fSmrg        for missing in range(prev_code + 1, code):
389d63b911fSmrg            newline = generate_keysym_line(
390d63b911fSmrg                missing, kernel, kver_list=ns.kernel_versions
391d63b911fSmrg            )
392d63b911fSmrg            if newline:
393d63b911fSmrg                print(newline)
394d63b911fSmrg        prev_code = code
395d63b911fSmrg
396d63b911fSmrg    return 0
397d63b911fSmrg
398d63b911fSmrg
399d63b911fSmrgdef find_xf86keysym_header():
400d63b911fSmrg    """
401d63b911fSmrg    Search for the XF86keysym.h file in the current tree or use the system one
402d63b911fSmrg    as last resort. This is a convenience function for running the script
403d63b911fSmrg    locally, it should not be relied on in the CI.
404d63b911fSmrg    """
405d63b911fSmrg    paths = tuple(Path.cwd().glob("**/XF86keysym.h"))
406d63b911fSmrg    if not paths:
4075395ea76Smrg        fallbackdir = Path(os.getenv("INCLUDESDIR") or "/usr/include/")
4085395ea76Smrg        path = fallbackdir / "X11" / "XF86keysym.h"
409d63b911fSmrg        if not path.exists():
4105395ea76Smrg            die(f"Unable to find XF86keysym.h in CWD or {fallbackdir}")
411d63b911fSmrg    else:
412d63b911fSmrg        if len(paths) > 1:
413d63b911fSmrg            die("Multiple XF86keysym.h in CWD, please use --header")
414d63b911fSmrg        path = paths[0]
415d63b911fSmrg
416d63b911fSmrg    logger.info(f"Using header file {path}")
417d63b911fSmrg    return path
418d63b911fSmrg
419d63b911fSmrg
420d63b911fSmrgdef main():
421d63b911fSmrg    parser = argparse.ArgumentParser(description="Keysym parser script")
422d63b911fSmrg    parser.add_argument("--verbose", "-v", action="count", default=0)
423d63b911fSmrg    parser.add_argument(
424d63b911fSmrg        "--header",
425d63b911fSmrg        type=str,
426d63b911fSmrg        default=None,
427d63b911fSmrg        help="Path to the XF86Keysym.h header file (default: search $CWD)",
428d63b911fSmrg    )
429d63b911fSmrg
430d63b911fSmrg    subparsers = parser.add_subparsers(help="command-specific help", dest="command")
431d63b911fSmrg    parser_verify = subparsers.add_parser(
4325395ea76Smrg        "verify", help="Verify the XF86keysym.h matches requirements (default)"
433d63b911fSmrg    )
434d63b911fSmrg    parser_verify.set_defaults(func=verify)
435d63b911fSmrg
436d63b911fSmrg    parser_generate = subparsers.add_parser(
437d63b911fSmrg        "add-keysyms", help="Add missing keysyms to the existing ones"
438d63b911fSmrg    )
439d63b911fSmrg    parser_generate.add_argument(
440d63b911fSmrg        "--kernel-git-tree",
441d63b911fSmrg        type=str,
442d63b911fSmrg        default=None,
443d63b911fSmrg        required=True,
444d63b911fSmrg        help="Path to a kernel git repo, required to find git tags",
445d63b911fSmrg    )
446d63b911fSmrg    parser_generate.add_argument(
447d63b911fSmrg        "--kernel-versions",
448d63b911fSmrg        type=str,
449d63b911fSmrg        default=[],
450d63b911fSmrg        required=False,
451d63b911fSmrg        help="Comma-separated list of kernel versions to limit ourselves to (e.g. 'v5.10,v5.9'). Supports fnmatch.",
452d63b911fSmrg    )
453d63b911fSmrg    parser_generate.set_defaults(func=add_keysyms)
454d63b911fSmrg    ns = parser.parse_args()
455d63b911fSmrg
456d63b911fSmrg    logger.setLevel(
457d63b911fSmrg        {2: logging.DEBUG, 1: logging.INFO, 0: logging.WARNING}.get(ns.verbose, 2)
458d63b911fSmrg    )
459d63b911fSmrg
460d63b911fSmrg    if not ns.header:
461d63b911fSmrg        ns.header = find_xf86keysym_header()
462d63b911fSmrg    else:
463d63b911fSmrg        ns.header = Path(ns.header)
464d63b911fSmrg
465d63b911fSmrg    if ns.command is None:
4665395ea76Smrg        print("No command specified, defaulting to verify'")
4675395ea76Smrg        ns.func = verify
468d63b911fSmrg
469d63b911fSmrg    sys.exit(ns.func(ns))
470d63b911fSmrg
471d63b911fSmrg
472d63b911fSmrgif __name__ == "__main__":
473d63b911fSmrg    main()
474