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