146185892Smrg#!/usr/bin/env python3 246185892Smrg# 346185892Smrg# Builds a tree view of a symbols file (showing all includes) 446185892Smrg# 546185892Smrg# This file is formatted with Python Black 646185892Smrg 746185892Smrgimport argparse 846185892Smrgimport pathlib 946185892Smrgimport os 1046185892Smrgfrom pyparsing import ( 1146185892Smrg Word, 1246185892Smrg Literal, 1346185892Smrg LineEnd, 1446185892Smrg OneOrMore, 1546185892Smrg oneOf, 1646185892Smrg Or, 1746185892Smrg And, 1846185892Smrg QuotedString, 1946185892Smrg Regex, 2046185892Smrg cppStyleComment, 2146185892Smrg alphanums, 2246185892Smrg Optional, 2346185892Smrg ParseException, 2446185892Smrg) 2546185892Smrg 2646185892Smrgxkb_basedir = None 2746185892Smrg 2846185892Smrg 2946185892Smrgclass XkbSymbols: 3046185892Smrg def __init__(self, file, name): 3146185892Smrg self.file = file # Path to the file this section came from 3246185892Smrg self.layout = file.name # XKb - filename is the layout name 3346185892Smrg self.name = name 3446185892Smrg self.includes = [] 3546185892Smrg 3646185892Smrg def __str__(self): 3746185892Smrg return f"{self.layout}({self.name}): {self.includes}" 3846185892Smrg 3946185892Smrg 4046185892Smrgclass XkbLoader: 4146185892Smrg """ 4246185892Smrg Wrapper class to avoid loading the same symbols file over and over 4346185892Smrg again. 4446185892Smrg """ 4546185892Smrg 4646185892Smrg class XkbParserException(Exception): 4746185892Smrg pass 4846185892Smrg 4946185892Smrg _instance = None 5046185892Smrg 5146185892Smrg def __init__(self, xkb_basedir): 5246185892Smrg self.xkb_basedir = xkb_basedir 5346185892Smrg self.loaded = {} 5446185892Smrg 5546185892Smrg @classmethod 5646185892Smrg def create(cls, xkb_basedir): 5746185892Smrg assert cls._instance is None 5846185892Smrg cls._instance = XkbLoader(xkb_basedir) 5946185892Smrg 6046185892Smrg @classmethod 6146185892Smrg def instance(cls): 6246185892Smrg assert cls._instance is not None 6346185892Smrg return cls._instance 6446185892Smrg 6546185892Smrg @classmethod 6646185892Smrg def load_symbols(cls, file): 6746185892Smrg return cls.instance().load_symbols_file(file) 6846185892Smrg 6946185892Smrg def load_symbols_file(self, file): 7046185892Smrg file = self.xkb_basedir / file 7146185892Smrg try: 7246185892Smrg return self.loaded[file] 7346185892Smrg except KeyError: 7446185892Smrg pass 7546185892Smrg 7646185892Smrg sections = [] 7746185892Smrg 7846185892Smrg def quoted(name): 7946185892Smrg return QuotedString(quoteChar='"', unquoteResults=True) 8046185892Smrg 8146185892Smrg # Callback, toks[0] is "foo" for xkb_symbols "foo" 8246185892Smrg def new_symbols_section(name, loc, toks): 8346185892Smrg assert len(toks) == 1 8446185892Smrg sections.append(XkbSymbols(file, toks[0])) 8546185892Smrg 8646185892Smrg # Callback, toks[0] is "foo(bar)" for include "foo(bar)" 8746185892Smrg def append_includes(name, loc, toks): 8846185892Smrg assert len(toks) == 1 8946185892Smrg sections[-1].includes.append(toks[0]) 9046185892Smrg 9146185892Smrg EOL = LineEnd().suppress() 9246185892Smrg SECTIONTYPE = ( 9346185892Smrg "default", 9446185892Smrg "partial", 9546185892Smrg "hidden", 9646185892Smrg "alphanumeric_keys", 9746185892Smrg "modifier_keys", 9846185892Smrg "keypad_keys", 9946185892Smrg "function_keys", 10046185892Smrg "alternate_group", 10146185892Smrg ) 10246185892Smrg NAME = quoted("name").setParseAction(new_symbols_section) 10346185892Smrg INCLUDE = ( 10446185892Smrg lit("include") + quoted("include").setParseAction(append_includes) + EOL 10546185892Smrg ) 10646185892Smrg # We only care about includes 10746185892Smrg OTHERLINE = And([~lit("};"), ~lit("include") + Regex(".*")]) + EOL 10846185892Smrg 10946185892Smrg with open(file) as fd: 11046185892Smrg types = OneOrMore(oneOf(SECTIONTYPE)).suppress() 11146185892Smrg include_or_other = Or([INCLUDE, OTHERLINE.suppress()]) 11246185892Smrg section = ( 11346185892Smrg types 11446185892Smrg + lit("xkb_symbols") 11546185892Smrg + NAME 11646185892Smrg + lit("{") 11746185892Smrg + OneOrMore(include_or_other) 11846185892Smrg + lit("};") 11946185892Smrg ) 12046185892Smrg grammar = OneOrMore(section) 12146185892Smrg grammar.ignore(cppStyleComment) 12246185892Smrg try: 12346185892Smrg result = grammar.parseFile(fd) 12446185892Smrg except ParseException as e: 12546185892Smrg raise XkbLoader.XkbParserException(str(e)) 12646185892Smrg 12746185892Smrg self.loaded[file] = sections 12846185892Smrg 12946185892Smrg return sections 13046185892Smrg 13146185892Smrg 13246185892Smrgdef lit(string): 13346185892Smrg return Literal(string).suppress() 13446185892Smrg 13546185892Smrg 13646185892Smrgdef print_section(s, filter_section=None, indent=0): 13746185892Smrg if filter_section and s.name != filter_section: 13846185892Smrg return 13946185892Smrg 14046185892Smrg layout = Word(alphanums + "_/").setResultsName("layout") 14146185892Smrg variant = Optional( 14246185892Smrg lit("(") + Word(alphanums + "_").setResultsName("variant") + lit(")") 14346185892Smrg ) 14446185892Smrg grammar = layout + variant 14546185892Smrg 14646185892Smrg prefix = "" 14746185892Smrg if indent > 0: 14846185892Smrg prefix = " " * (indent - 2) + "|-> " 14946185892Smrg print(f"{prefix}{s.layout}({s.name})") 15046185892Smrg for include in s.includes: 15146185892Smrg result = grammar.parseString(include) 15246185892Smrg # Should really find the "default" section but for this script 15346185892Smrg # hardcoding "basic" is good enough 15446185892Smrg layout, variant = result.layout, result.variant or "basic" 15546185892Smrg 15646185892Smrg # include "foo(bar)" means file "foo", section bar 15746185892Smrg includefile = xkb_basedir / layout 15846185892Smrg include_sections = XkbLoader.load_symbols(layout) 15946185892Smrg for include_section in include_sections: 16046185892Smrg print_section(include_section, filter_section=variant, indent=indent + 4) 16146185892Smrg 16246185892Smrg 16346185892Smrgdef list_sections(sections, filter_section=None, indent=0): 16446185892Smrg for section in sections: 16546185892Smrg print_section(section, filter_section) 16646185892Smrg 16746185892Smrg 16846185892Smrgif __name__ == "__main__": 16946185892Smrg parser = argparse.ArgumentParser(description="XKB symbol tree viewer") 17046185892Smrg parser.add_argument( 17146185892Smrg "file", 17246185892Smrg metavar="file-or-directory", 17346185892Smrg type=pathlib.Path, 17446185892Smrg help="The XKB symbols file or directory", 17546185892Smrg ) 17646185892Smrg parser.add_argument( 17746185892Smrg "section", type=str, default=None, nargs="?", help="The section (optional)" 17846185892Smrg ) 17946185892Smrg ns = parser.parse_args() 18046185892Smrg 18146185892Smrg if ns.file.is_dir(): 18246185892Smrg xkb_basedir = ns.file.resolve() 18346185892Smrg files = sorted([f for f in ns.file.iterdir() if not f.is_dir()]) 18446185892Smrg else: 18546185892Smrg # Note: this requires that the file given on the cmdline is not one of 18646185892Smrg # the sun_vdr/de or others inside a subdirectory. meh. 18746185892Smrg xkb_basedir = ns.file.parent.resolve() 18846185892Smrg files = [ns.file] 18946185892Smrg 19046185892Smrg XkbLoader.create(xkb_basedir) 19146185892Smrg 19246185892Smrg for file in files: 19346185892Smrg try: 19446185892Smrg sections = XkbLoader.load_symbols(file.resolve()) 19546185892Smrg list_sections(sections, filter_section=ns.section) 19646185892Smrg except XkbLoader.XkbParserException: 19746185892Smrg pass 198