Blob Blame History Raw
#!/usr/bin/python
# -*- python -*-
#
# Keycode Map Generator
#
# Copyright (C) 2009-2017 Red Hat, Inc.
#
# This file is dual license under the terms of the GPLv2 or later
# and 3-clause BSD licenses.
#

# Requires >= 2.6
from __future__ import print_function

import csv
import argparse
import hashlib
import time

class Database:

    # Linux: linux/input.h
    MAP_LINUX = "linux"

    # OS-X: Carbon/HIToolbox/Events.h
    MAP_OSX = "osx"

    # AT Set 1: linux/drivers/input/keyboard/atkbd.c
    #           (atkbd_set2_keycode + atkbd_unxlate_table)
    MAP_ATSET1 = "atset1"

    # AT Set 2: linux/drivers/input/keyboard/atkbd.c
    #           (atkbd_set2_keycode)
    MAP_ATSET2 = "atset2"

    # AT Set 3: linux/drivers/input/keyboard/atkbd.c
    #           (atkbd_set3_keycode)
    MAP_ATSET3 = "atset3"

    # XT: linux/drivers/input/keyboard/xt.c (xtkbd_keycode)
    MAP_XT = "xt"

    # Linux RAW: linux/drivers/char/keyboard.c (x86_keycodes)
    MAP_XTKBD = "xtkbd"

    # USB HID: linux/drivers/hid/usbhid/usbkbd.c (usb_kbd_keycode)
    MAP_USB = "usb"

    # Win32: mingw32/winuser.h
    MAP_WIN32 = "win32"

    # XWin XT: xorg-server/hw/xwin/{winkeybd.c,winkeynames.h}
    #          (xt + manually transcribed)
    MAP_XWINXT = "xwinxt"

    # X11: http://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
    MAP_X11 = "x11"

    # XKBD XT: xf86-input-keyboard/src/at_scancode.c
    #          (xt + manually transcribed)
    MAP_XKBDXT = "xkbdxt"

    # Xorg with evdev: linux + an offset
    MAP_XORGEVDEV = "xorgevdev"

    # Xorg with kbd: xkbdxt + an offset
    MAP_XORGKBD = "xorgkbd"

    # Xorg with OS-X: osx + an offset
    MAP_XORGXQUARTZ = "xorgxquartz"

    # Xorg + Cygwin: xwinxt + an offset
    MAP_XORGXWIN = "xorgxwin"

    # XT over RFB: xtkbd + special re-encoding of high bit
    MAP_RFB = "rfb"

    MAP_LIST = (
        MAP_LINUX,
        MAP_OSX,
        MAP_ATSET1,
        MAP_ATSET2,
        MAP_ATSET3,
        MAP_XT,
        MAP_XTKBD,
        MAP_USB,
        MAP_WIN32,
        MAP_XWINXT,
        MAP_XKBDXT,
        MAP_X11,

        # These are derived from maps above
        MAP_XORGEVDEV,
        MAP_XORGKBD,
        MAP_XORGXQUARTZ,
        MAP_XORGXWIN,
        MAP_RFB,
    )

    CODE_COLUMNS = {
        MAP_LINUX: 1,
        MAP_OSX: 3,
        MAP_ATSET1: 4,
        MAP_ATSET2: 5,
        MAP_ATSET3: 6,
        MAP_XT: 7,
        MAP_XTKBD: 8,
        MAP_USB: 9,
        MAP_WIN32: 11,
        MAP_XWINXT: 12,
        MAP_XKBDXT: 13,
        MAP_X11: 15,
    }

    NAME_COLUMNS = {
        MAP_LINUX: 0,
        MAP_OSX: 2,
        MAP_WIN32: 10,
        MAP_X11: 14,
    }

    def __init__(self):

        self.mapto = {}
        self.mapfrom = {}
        self.mapname = {}
        self.mapchecksum = None

        for name in self.MAP_LIST:
            # Key is a MAP_LINUX, value is a MAP_XXX
            self.mapto[name] = {}
            # key is a MAP_XXX, value is a MAP_LINUX
            self.mapfrom[name] = {}

        for name in self.NAME_COLUMNS.keys():
            # key is a MAP_LINUX, value is a string
            self.mapname[name] = {}

    def _generate_checksum(self, filename):
        hash = hashlib.sha256()
        with open(filename, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hash.update(chunk)
        self.mapchecksum = hash.hexdigest()

    def load(self, filename):
        self._generate_checksum(filename)

        with open(filename, 'r') as f:
            reader = csv.reader(f)

            first = True

            for row in reader:
                # Discard column headings
                if first:
                    first = False
                    continue

                # We special case MAP_LINUX since that is out
                # master via which all other mappings are done
                linux = self.load_linux(row)

                # Now load all the remaining master data values
                self.load_data(row, linux)

                # Then load all the keycode names
                self.load_names(row, linux)

                # Finally calculate derived key maps
                self.derive_data(row, linux)

    def load_linux(self, row):
        col = self.CODE_COLUMNS[self.MAP_LINUX]
        linux = row[col]

        if linux.startswith("0x"):
            linux = int(linux, 16)
        else:
            linux = int(linux, 10)

        self.mapto[self.MAP_LINUX][linux] = linux
        self.mapfrom[self.MAP_LINUX][linux] = linux

        return linux


    def load_data(self, row, linux):
        for mapname in self.CODE_COLUMNS:
            if mapname == self.MAP_LINUX:
                continue

            col = self.CODE_COLUMNS[mapname]
            val = row[col]

            if val == "":
                continue

            if val.startswith("0x"):
                val = int(val, 16)
            else:
                val = int(val, 10)

            self.mapto[mapname][linux] = val
            self.mapfrom[mapname][val] = linux

    def load_names(self, row, linux):
        for mapname in self.NAME_COLUMNS:
            col = self.NAME_COLUMNS[mapname]
            val = row[col]

            if val == "":
                continue

            self.mapname[mapname][linux] = val


    def derive_data(self, row, linux):
        # Xorg KBD is XKBD XT offset by 8
        if linux in self.mapto[self.MAP_XKBDXT]:
            xorgkbd = self.mapto[self.MAP_XKBDXT][linux] + 8
            self.mapto[self.MAP_XORGXQUARTZ][linux] = xorgkbd
            self.mapfrom[self.MAP_XORGXQUARTZ][xorgkbd] = linux

        # Xorg evdev is Linux offset by 8
        self.mapto[self.MAP_XORGEVDEV][linux] = linux + 8
        self.mapfrom[self.MAP_XORGEVDEV][linux + 8] = linux

        # Xorg XQuartx is OS-X offset by 8
        if linux in self.mapto[self.MAP_OSX]:
            xorgxquartz = self.mapto[self.MAP_OSX][linux] + 8
            self.mapto[self.MAP_XORGXQUARTZ][linux] = xorgxquartz
            self.mapfrom[self.MAP_XORGXQUARTZ][xorgxquartz] = linux

        # Xorg Xwin (aka Cygwin) is XWin XT offset by 8
        if linux in self.mapto[self.MAP_XWINXT]:
            xorgxwin = self.mapto[self.MAP_XWINXT][linux] + 8
            self.mapto[self.MAP_XORGXWIN][linux] = xorgxwin
            self.mapfrom[self.MAP_XORGXWIN][xorgxwin] = linux


        # RFB keycodes are XT kbd keycodes with a slightly
        # different encoding of 0xe0 scan codes. RFB uses
        # the high bit of the first byte, instead of the low
        # bit of the second byte
        if linux in self.mapto[self.MAP_XTKBD]:
            xtkbd = self.mapto[self.MAP_XTKBD][linux]
            if xtkbd != 0:
                rfb = ((xtkbd & 0x100) >> 1) | (xtkbd & 0x7f)
            else:
                rfb = 0
            self.mapto[self.MAP_RFB][linux] = rfb
            self.mapfrom[self.MAP_RFB][rfb] = linux

class LanguageGenerator(object):

    def generate_header(self, database, args):
        today = time.strftime("%Y-%m-%d %H:%M")
        self._boilerplate([
            "This file is auto-generated from keymaps.csv on %s" % today,
            "Database checksum sha256(%s)" % database.mapchecksum,
            "To re-generate, run:",
            "  %s" % args,
        ])

    def generate_code_map(self, varname, database, frommapname, tomapname):
        if frommapname not in database.mapfrom:
            raise Exception("Unknown map %s, expected %s",
                            frommapname, ",".join(database.mapfrom.keys()))
        if tomapname not in database.mapto:
            raise Exception("Unknown map %s, expected %s",
                            tomapname, ",".join(database.mapto.keys()))

        tolinux = database.mapfrom[frommapname]
        fromlinux = database.mapto[tomapname]

        if varname is None:
            varname = "code_map_%s_to_%s" % (frommapname, tomapname)

        frommax = 0
        for key in tolinux.keys():
            if key > frommax:
                frommax = key

        self._array_start_code(varname, frommax + 1)
        for src in range(frommax + 1):
            linux = tolinux.get(src, None)
            if linux is None:
                dst = None
            else:
                dst = fromlinux.get(linux, None)

            comment = "%s -> %s -> %s" % (self._label(database, frommapname, src),
                                          self._label(database, Database.MAP_LINUX, linux),
                                          self._label(database, tomapname, dst))
            self._array_entry_code(src, dst, comment)
        self._array_end()                                        

    def generate_code_table(self, varname, database, mapname):
        if mapname not in database.mapto:
            raise Exception("Unknown map %s, expected %s",
                            mapname, ",".join(database.mapto.keys()))

        keys = database.mapto[Database.MAP_LINUX].keys()
        keys.sort()
        names = [database.mapname[Database.MAP_LINUX].get(key, "unnamed") for key in keys]

        if varname is None:
            varname = "code_table_%s" % mapname

        self._array_start_code(varname, len(keys))

        for i in range(len(keys)):
            key = keys[i]
            dst = database.mapto[mapname].get(key, None)
            self._array_entry_code(i, dst, names[i])

        self._array_end()

    def generate_name_map(self, varname, database, frommapname, tomapname):
        if frommapname not in database.mapfrom:
            raise Exception("Unknown map %s, expected %s",
                            frommapname, ",".join(database.mapfrom.keys()))
        if tomapname not in database.mapname:
            raise Exception("Unknown map %s, expected %s",
                            tomapname, ",".join(database.mapname.keys()))

        tolinux = database.mapfrom[frommapname]
        fromlinux = database.mapname[tomapname]

        if varname is None:
            varname = "name_map_%s_to_%s" % (frommapname, tomapname)

        frommax = 0
        for key in tolinux.keys():
            if key > frommax:
                frommax = key

        self._array_start_name(varname, frommax + 1)

        for src in range(frommax + 1):
            linux = tolinux.get(src, None)
            if linux is None:
                dst = None
            else:
                dst = fromlinux.get(linux, None)

            comment = "%s -> %s -> %s" % (self._label(database, frommapname, src),
                                          self._label(database, Database.MAP_LINUX, linux),
                                          self._label(database, tomapname, dst))
            self._array_entry_name(src, dst, comment)
        self._array_end()                                        

    def generate_name_table(self, varname, database, mapname):
        if mapname not in database.mapname:
            raise Exception("Unknown map %s, expected %s",
                            mapname, ",".join(database.mapname.keys()))

        keys = database.mapto[Database.MAP_LINUX].keys()
        keys.sort()
        names = [database.mapname[Database.MAP_LINUX].get(key, "unnamed") for key in keys]

        if varname is None:
            varname = "name_table_%s" % mapname

        self._array_start_name(varname, len(keys))

        for i in range(len(keys)):
            key = keys[i]
            dst = database.mapname[mapname].get(key, None)
            self._array_entry_name(i, dst, names[i])

        self._array_end()

    def _label(self, database, mapname, val):
        if mapname in database.mapname:
            return "%s:%s (%s)" % (mapname, val, database.mapname[mapname].get(val, "unnamed"))
        else:
            return "%s:%s" % (mapname, val)


class CLanguageGenerator(LanguageGenerator):

    def __init__(self, inttypename, strtypename):
        self.inttypename = inttypename
        self.strtypename = strtypename

    def _boilerplate(self, lines):
        print("/*")
        for line in lines:
            print(" * %s" % line)
        print("*/")

    def _array_start_code(self, varname, length):
        print("const %s %s[%d] = {" % (self.inttypename, varname, length))

    def _array_start_name(self, varname, length):
        print("const %s %s[%d] = {" % (self.strtypename, varname, length))

    def _array_end(self):
        print("};")

    def _array_entry_code(self, index, value, comment):
        if value is not None:
            print("  [0x%x] = 0x%x, /* %s */" % (index, value, comment))

    def _array_entry_name(self, index, value, comment):
        if value is not None:
            print("  [0x%x] = \"%s\", /* %s */" % (index, value, comment))

class StdCLanguageGenerator(CLanguageGenerator):

    def __init__(self):
        super(StdCLanguageGenerator, self).__init__("unsigned short", "char *")

class GLib2LanguageGenerator(CLanguageGenerator):

    def __init__(self):
        super(GLib2LanguageGenerator, self).__init__("guint16", "gchar *")

class PythonLanguageGenerator(LanguageGenerator):

    def _boilerplate(self, lines):
        print("#")
        for line in lines:
            print("# %s" % line)
        print("#")

    def _array_start_code(self, varname, length):
        print("%s = [" % varname)

    def _array_start_name(self, varname, length):
        print("%s = [" % varname)

    def _array_end(self):
        print("]")

    def _array_entry_code(self, index, value, comment):
        if value is None:
            print("  None, # %s" % (comment))
        else:
            print("  0x%x, # %s" % (value, comment))

    def _array_entry_name(self, index, value, comment):
        if value is None:
            print("  None, # %s" % (comment))
        else:
            print("  \"%s\", # %s" % (value, comment))

class PerlLanguageGenerator(LanguageGenerator):

    def _boilerplate(self, lines):
        print("#")
        for line in lines:
            print("# %s" % line)
        print("#")

    def _array_start_code(self, varname, length):
        print("my @%s = (" % varname)

    def _array_start_name(self, varname, length):
        print("my @%s = (" % varname)

    def _array_end(self):
        print(");")

    def _array_entry_code(self, index, value, comment):
        if value is None:
            print("  undef, # %s" % (comment))
        else:
            print("  0x%x, # %s" % (value, comment))

    def _array_entry_name(self, index, value, comment):
        if value is None:
            print("  undef, # %s" % (comment))
        else:
            print("  \"%s\", # %s" % (value, comment))

GENERATORS = {
    "stdc": StdCLanguageGenerator(),
    "glib2": GLib2LanguageGenerator(),
    "python2": PythonLanguageGenerator(),
    "python3": PythonLanguageGenerator(),
    "perl": PerlLanguageGenerator(),
}

def code_map(args):
    database = Database()
    database.load(args.keymaps)

    cliargs = ["keymap-gen", "--lang=%s" % args.lang]
    if args.varname is not None:
        cliargs.append("--varname=%s" % args.varname)
    cliargs.extend(["code-map", "keymaps.csv", args.frommapname, args.tomapname])
    GENERATORS[args.lang].generate_header(database, " ".join(cliargs))

    GENERATORS[args.lang].generate_code_map(args.varname, database, args.frommapname, args.tomapname)

def code_table(args):
    database = Database()
    database.load(args.keymaps)

    cliargs = ["keymap-gen", "--lang=%s" % args.lang]
    if args.varname is not None:
        cliargs.append("--varname=%s" % args.varname)
    cliargs.extend(["code-table", "keymaps.csv", args.mapname])
    GENERATORS[args.lang].generate_header(database, " ".join(cliargs))

    GENERATORS[args.lang].generate_code_table(args.varname, database, args.mapname)

def name_map(args):
    database = Database()
    database.load(args.keymaps)

    cliargs = ["keymap-gen", "--lang=%s" % args.lang]
    if args.varname is not None:
        cliargs.append("--varname=%s" % args.varname)
    cliargs.extend(["name-map", "keymaps.csv", args.frommapname, args.tomapname])
    GENERATORS[args.lang].generate_header(database, " ".join(cliargs))

    GENERATORS[args.lang].generate_name_map(args.varname, database, args.frommapname, args.tomapname)

def name_table(args):
    database = Database()
    database.load(args.keymaps)


    cliargs = ["keymap-gen", "--lang=%s" % args.lang]
    if args.varname is not None:
        cliargs.append("--varname=%s" % args.varname)
    cliargs.extend(["name-table", "keymaps.csv", args.mapname])
    GENERATORS[args.lang].generate_header(database, " ".join(cliargs))

    GENERATORS[args.lang].generate_name_table(args.varname, database, args.mapname)

def usage(args):
    print ("Please select a command:")
    print ("  'code-map', 'code-table', 'name-map', 'name-table'")
    sys.exit(1)

def main():
    parser = argparse.ArgumentParser()

    parser.add_argument("--lang", default="stdc",
                        help="Output language, %s" % ",".join(GENERATORS.keys()))
    parser.add_argument("--varname", default=None,
                        help="Data variable name")
    parser.set_defaults(func=usage)

    subparsers = parser.add_subparsers(help="sub-command help")

    codemapparser = subparsers.add_parser("code-map", help="Generate a mapping between code tables")
    codemapparser.add_argument("keymaps", help="Path to keymap CSV data file")
    codemapparser.add_argument("frommapname", help="Source code table name")
    codemapparser.add_argument("tomapname", help="Target code table name")
    codemapparser.set_defaults(func=code_map)

    codetableparser = subparsers.add_parser("code-table", help="Generate a flat code table")
    codetableparser.add_argument("keymaps", help="Path to keymap CSV data file")
    codetableparser.add_argument("mapname", help="Code table name")
    codetableparser.set_defaults(func=code_table)

    namemapparser = subparsers.add_parser("name-map", help="Generate a mapping to names")
    namemapparser.add_argument("keymaps", help="Path to keymap CSV data file")
    namemapparser.add_argument("frommapname", help="Source code table name")
    namemapparser.add_argument("tomapname", help="Target name table name")
    namemapparser.set_defaults(func=name_map)

    nametableparser = subparsers.add_parser("name-table", help="Generate a flat name table")
    nametableparser.add_argument("keymaps", help="Path to keymap CSV data file")
    nametableparser.add_argument("mapname", help="Name table name")
    nametableparser.set_defaults(func=name_table)

    args = parser.parse_args()
    args.func(args)


main()