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