1#!/usr/bin/env python3 2# 3# Builds a tree view of a symbols file (showing all includes) 4# 5# This file is formatted with Python Black 6 7import argparse 8import pathlib 9import os 10from pyparsing import ( 11 Word, 12 Literal, 13 LineEnd, 14 OneOrMore, 15 oneOf, 16 Or, 17 And, 18 QuotedString, 19 Regex, 20 cppStyleComment, 21 alphanums, 22 Optional, 23 ParseException, 24) 25 26xkb_basedir = None 27 28 29class XkbSymbols: 30 def __init__(self, file, name): 31 self.file = file # Path to the file this section came from 32 self.layout = file.name # XKb - filename is the layout name 33 self.name = name 34 self.includes = [] 35 36 def __str__(self): 37 return f"{self.layout}({self.name}): {self.includes}" 38 39 40class XkbLoader: 41 """ 42 Wrapper class to avoid loading the same symbols file over and over 43 again. 44 """ 45 46 class XkbParserException(Exception): 47 pass 48 49 _instance = None 50 51 def __init__(self, xkb_basedir): 52 self.xkb_basedir = xkb_basedir 53 self.loaded = {} 54 55 @classmethod 56 def create(cls, xkb_basedir): 57 assert cls._instance is None 58 cls._instance = XkbLoader(xkb_basedir) 59 60 @classmethod 61 def instance(cls): 62 assert cls._instance is not None 63 return cls._instance 64 65 @classmethod 66 def load_symbols(cls, file): 67 return cls.instance().load_symbols_file(file) 68 69 def load_symbols_file(self, file): 70 file = self.xkb_basedir / file 71 try: 72 return self.loaded[file] 73 except KeyError: 74 pass 75 76 sections = [] 77 78 def quoted(name): 79 return QuotedString(quoteChar='"', unquoteResults=True) 80 81 # Callback, toks[0] is "foo" for xkb_symbols "foo" 82 def new_symbols_section(name, loc, toks): 83 assert len(toks) == 1 84 sections.append(XkbSymbols(file, toks[0])) 85 86 # Callback, toks[0] is "foo(bar)" for include "foo(bar)" 87 def append_includes(name, loc, toks): 88 assert len(toks) == 1 89 sections[-1].includes.append(toks[0]) 90 91 EOL = LineEnd().suppress() 92 SECTIONTYPE = ( 93 "default", 94 "partial", 95 "hidden", 96 "alphanumeric_keys", 97 "modifier_keys", 98 "keypad_keys", 99 "function_keys", 100 "alternate_group", 101 ) 102 NAME = quoted("name").setParseAction(new_symbols_section) 103 INCLUDE = ( 104 lit("include") + quoted("include").setParseAction(append_includes) + EOL 105 ) 106 # We only care about includes 107 OTHERLINE = And([~lit("};"), ~lit("include") + Regex(".*")]) + EOL 108 109 with open(file) as fd: 110 types = OneOrMore(oneOf(SECTIONTYPE)).suppress() 111 include_or_other = Or([INCLUDE, OTHERLINE.suppress()]) 112 section = ( 113 types 114 + lit("xkb_symbols") 115 + NAME 116 + lit("{") 117 + OneOrMore(include_or_other) 118 + lit("};") 119 ) 120 grammar = OneOrMore(section) 121 grammar.ignore(cppStyleComment) 122 try: 123 result = grammar.parseFile(fd) 124 except ParseException as e: 125 raise XkbLoader.XkbParserException(str(e)) 126 127 self.loaded[file] = sections 128 129 return sections 130 131 132def lit(string): 133 return Literal(string).suppress() 134 135 136def print_section(s, filter_section=None, indent=0): 137 if filter_section and s.name != filter_section: 138 return 139 140 layout = Word(alphanums + "_/").setResultsName("layout") 141 variant = Optional( 142 lit("(") + Word(alphanums + "_").setResultsName("variant") + lit(")") 143 ) 144 grammar = layout + variant 145 146 prefix = "" 147 if indent > 0: 148 prefix = " " * (indent - 2) + "|-> " 149 print(f"{prefix}{s.layout}({s.name})") 150 for include in s.includes: 151 result = grammar.parseString(include) 152 # Should really find the "default" section but for this script 153 # hardcoding "basic" is good enough 154 layout, variant = result.layout, result.variant or "basic" 155 156 # include "foo(bar)" means file "foo", section bar 157 includefile = xkb_basedir / layout 158 include_sections = XkbLoader.load_symbols(layout) 159 for include_section in include_sections: 160 print_section(include_section, filter_section=variant, indent=indent + 4) 161 162 163def list_sections(sections, filter_section=None, indent=0): 164 for section in sections: 165 print_section(section, filter_section) 166 167 168if __name__ == "__main__": 169 parser = argparse.ArgumentParser(description="XKB symbol tree viewer") 170 parser.add_argument( 171 "file", 172 metavar="file-or-directory", 173 type=pathlib.Path, 174 help="The XKB symbols file or directory", 175 ) 176 parser.add_argument( 177 "section", type=str, default=None, nargs="?", help="The section (optional)" 178 ) 179 ns = parser.parse_args() 180 181 if ns.file.is_dir(): 182 xkb_basedir = ns.file.resolve() 183 files = sorted([f for f in ns.file.iterdir() if not f.is_dir()]) 184 else: 185 # Note: this requires that the file given on the cmdline is not one of 186 # the sun_vdr/de or others inside a subdirectory. meh. 187 xkb_basedir = ns.file.parent.resolve() 188 files = [ns.file] 189 190 XkbLoader.create(xkb_basedir) 191 192 for file in files: 193 try: 194 sections = XkbLoader.load_symbols(file.resolve()) 195 list_sections(sections, filter_section=ns.section) 196 except XkbLoader.XkbParserException: 197 pass 198