#!/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 = <$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 = <{existing[xkeycode]}>; // #define {name:23s} {code}\n" ) continue output.append( f"\t = {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 keys between the section header and footer. """ in_generated_section = False pattern = re.compile(".*.*") 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 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" // #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()