#!/usr/bin/env python # coding: utf-8 """%(prog) [options] KLCFILE Read a .KLC file saved by Microsoft Keyboard Layout Creator, and print an equivalent xkb_symbol description. The resulting output can be included in the xkb_symbol section of a keymap description. This may work for you: $ %(prog) foo.klc > foo.xkbsym $ setxkbmap -print \ > | sed '/xkb_symbols/s/};/\n\t\t\tinclude "foo.xkbsym" };/' \ > | xkbcomp - "${DISPLAY}" $ setxkbmap -option lv3:ralt_switch # For AltGr To use the optional dead-key compose file, written out to say ~/.XCompose.dead_keys, create a file called ~/.XCompose containing include "%L" include "%H/.XCompose.dead_keys" See §4.19 “Xlib Compose file support and extensions” of the X11R7.1 release notes (http://xorg.freedesktop.org/releases/X11R7.1/doc/RELNOTES4.html#30) for more information. X applications read the compose file on startup, so you will need to restart applications to pick up changes in dead keys, whereas other changes will take effect immediately on running setxkbmap. There are fundamental difference in the way that X and Windows handle dead keys. For the key sequence 〈dead_key〉〈other_key〉, where 〈other_key〉 has no mapping for this particular dead key, windows treats both keypresses normally, while X ignores both. """ from __future__ import with_statement import codecs import re import sys import unicodedata import keysymdata from optparse import OptionParser class KeyMapping(object): def __init__(self, scancode, normal, shifted, altgr, shiftedaltgr): self.scancode = scancode self.normal = self._cleanup(normal) self.shifted = self._cleanup(shifted) self.altgr = self._cleanup(altgr) self.shiftedaltgr = self._cleanup(shiftedaltgr) @classmethod def _cleanup(cls, charstr): if charstr == '-1': return None noat, at, junk = charstr.partition('@') match = re.match('^[0-9A-Fa-f]{4}$', noat) if match: return unichr(int(match.group(), 16)) assert len(noat) == 1 return noat @classmethod def scancode_to_xkbname(cls, scancode): special = {0x29: "TLDE", 0x2b: "BKSL", 0x39: "SPCE", 0x53: "KPDL"} if scancode in special: return special[scancode] elif scancode <= 0x0d: return "AE%02d" % (scancode - 1) elif scancode <= 0x1b: return "AD%02d" % (scancode - 0xf) elif scancode <= 0x28: return "AC%02d" % (scancode - 0x1d) elif scancode <= 0x35: return "AB%02d" % (scancode - 0x2b) return None def to_xkb_def_str(self): xkbname = self.scancode_to_xkbname(self.scancode) if not xkbname: return "" return ("key <%s> { [ %s, %s, %s, %s ] };" % tuple([xkbname] + [keysymdata.name(sym) for sym in [self.normal, self.shifted, self.altgr, self.shiftedaltgr]])) def DDDD_to_name(DDDD): "'0046' -> 'FULL STOP'" return unicodedata.name(unichr(int(DDDD, 16))) class DeadKey(object): def __init__(self, deadkey): self.deadkey = deadkey self.maps = {} def add_entry(self, key, value): self.maps[key] = value def to_xcompose_str(self): return '# %s' % DDDD_to_name(self.deadkey) + "\n" \ + "\n".join("<%s> <%s>:\t%s\t# '%s' -> %s" % (keysymdata.name(self.deadkey), keysymdata.name(k), keysymdata.name(v), unichr(int(k, 16)), DDDD_to_name(v)) for k, v in self.maps.items()) + "\n" class MSKLCFile(object): def __init__(self, filename): self.filename = filename self.mappings = [] self.deadkeys = [] self.metadata = {} self.shiftmap = [] self.parse() def parse(self): with codecs.open(self.filename, 'r', encoding='utf-16') as file: state = None for line in file: line, sep, comment = line.partition('//') line = line.strip() comment = comment.strip() if line == '': continue if state == 'shiftstate': try: shift = int(line) self.shiftmap.append([shift, shift in [0, 1, 6, 7]]) continue except ValueError: state = None elif state == 'layout': pieces = line.split() if len(pieces) == len(self.shiftmap)+3: self.parse_layout_line(pieces) continue else: state = None elif state in ['keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']: try: hex, key = line.split("\t", 1) hex = int(hex, 16) continue except ValueError: state = None elif state == 'deadkey': match = re.match(r"""^([0-9a-fA-F]{4})\s+ ([0-9a-fA-F]{4})$""", line, re.VERBOSE) if not match: state = None else: if match.group(1): self.deadkeys[-1].add_entry(match.group(1), match.group(2)) continue elif state != None: print line raise KeyError key, sep, arg = line.partition("\t") if not key.isupper(): print line raise KeyError key = key.lower() if key == 'kbd': self.metadata['layout'], sep, quoted = arg.partition("\t") self.metadata['name'] = quoted[1:-1] elif key in ['copyright', 'company', 'localename', 'localeid']: self.metadata[key] = arg[1:-1] elif key == 'version': self.metadata[key] = arg elif key in ['shiftstate', 'layout', 'keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']: state = key elif key == 'deadkey': state = key self.deadkeys.append(DeadKey(arg)) elif key == 'endkbd': break else: print line raise KeyError def parse_layout_line(self, pieces): scancode = int(pieces[0], 16) vk = pieces[1] cap = pieces[2] valid = ['-1','-1','-1','-1'] for i in range(3, len(pieces)): shift, good = self.shiftmap[i-3] if good: valid[shift % 4] = pieces[i] elif pieces[i] != '-1': print >> sys.stderr, "skipping %(pc)s at position %(pos)u (shiftstate %(ss)u) for %(key)s" % { 'pc': pieces[i], 'pos': i, 'ss': shift, 'key': pieces[0] } mapping = KeyMapping(scancode, *valid) self.mappings.append(mapping) def main(args=None): if args is None: args = sys.argv[1:] optp = OptionParser(usage=__doc__) optp.add_option('--compose', dest='compose_file', metavar="FILE", help="Write dead key definitions to compose file FILE") options, args = optp.parse_args(args) if len(args) != 1: optp.print_help() return 1 filename = args[0] layout = MSKLCFile(filename) if layout.metadata['name']: print "// %(name)s" % layout.metadata print "// Converted from %s" % filename for key in ['copyright', 'company', 'version']: if key in layout.metadata: print "// %s: %s" % tuple([ key.title(), layout.metadata[key] ]) if layout.metadata['layout']: print 'xkb_symbols "%s" {' % layout.metadata['layout'] else: print "xkb_symbols {" for mapping in layout.mappings: print mapping.to_xkb_def_str() print "};" if options.compose_file is not None: with open(options.compose_file, 'w') as compose_file: for key in layout.deadkeys: print >> compose_file, key.to_xcompose_str().encode('utf-8') if __name__ == '__main__': sys.exit(main())