Home | History | Annotate | Line # | Download | only in scripts
      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 
      7 import argparse
      8 import pathlib
      9 import os
     10 from 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 
     26 xkb_basedir = None
     27 
     28 
     29 class 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 
     40 class 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 
    132 def lit(string):
    133     return Literal(string).suppress()
    134 
    135 
    136 def 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 
    163 def list_sections(sections, filter_section=None, indent=0):
    164     for section in sections:
    165         print_section(section, filter_section)
    166 
    167 
    168 if __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