4 """%(prog) [options] KLCFILE
6 Read a .KLC file saved by Microsoft Keyboard Layout Creator, and print
7 an equivalent xkb_symbol description.
9 The resulting output can be included in the xkb_symbol section of a keymap
10 description. This may work for you:
12 $ %(prog) foo.klc > foo.xkbsym
14 > | sed '/xkb_symbols/s/};/\n\t\t\tinclude "foo.xkbsym" };/' \
15 > | xkbcomp - "${DISPLAY}"
16 $ setxkbmap -option lv3:ralt_switch # For AltGr
18 To use the optional dead-key compose file, written out to say
19 ~/.XCompose.dead_keys, create a file called ~/.XCompose containing
22 include "%H/.XCompose.dead_keys"
24 See §4.19 “Xlib Compose file support and extensions” of the X11R7.1 release
25 notes (http://xorg.freedesktop.org/releases/X11R7.1/doc/RELNOTES4.html#30) for
28 X applications read the compose file on startup, so you will need to restart
29 applications to pick up changes in dead keys, whereas other changes will take
30 effect immediately on running setxkbmap.
32 There are fundamental difference in the way that X and Windows handle dead
33 keys. For the key sequence 〈dead_key〉〈other_key〉, where 〈other_key〉 has no
34 mapping for this particular dead key, windows treats both keypresses normally,
38 from __future__ import with_statement
44 from optparse import OptionParser
46 class KeyMapping(object):
47 def __init__(self, scancode, normal, shifted, altgr, shiftedaltgr):
48 self.scancode = scancode
49 self.normal = self._cleanup(normal)
50 self.shifted = self._cleanup(shifted)
51 self.altgr = self._cleanup(altgr)
52 self.shiftedaltgr = self._cleanup(shiftedaltgr)
55 def _cleanup(cls, charstr):
58 noat, at, junk = charstr.partition('@')
59 match = re.match('^[0-9A-Fa-f]{4}$', noat)
61 return unichr(int(match.group(), 16))
66 def scancode_to_xkbname(cls, scancode):
67 special = {0x29: "TLDE", 0x2b: "BKSL", 0x39: "SPCE", 0x53: "KPDL"}
68 if scancode in special:
69 return special[scancode]
71 elif scancode <= 0x0d:
72 return "AE%02d" % (scancode - 1)
73 elif scancode <= 0x1b:
74 return "AD%02d" % (scancode - 0xf)
75 elif scancode <= 0x28:
76 return "AC%02d" % (scancode - 0x1d)
77 elif scancode <= 0x35:
78 return "AB%02d" % (scancode - 0x2b)
81 def to_xkb_def_str(self):
82 xkbname = self.scancode_to_xkbname(self.scancode)
85 def format_symbol(sym):
88 return "U%04x" % ord(sym)
90 return ("key <%s> { [ %s, %s, %s, %s ] };"
93 for sym in [self.normal, self.shifted,
94 self.altgr, self.shiftedaltgr]]))
96 def DDDD_to_name(DDDD):
97 "'0046' -> 'FULL STOP'"
98 return unicodedata.name(unichr(int(DDDD, 16)))
100 class DeadKey(object):
101 def __init__(self, deadkey):
102 self.deadkey = deadkey
105 def add_entry(self, key, value):
106 self.maps[key] = value
108 def to_xcompose_str(self):
109 return '# %s' % DDDD_to_name(self.deadkey) + "\n" \
110 + "\n".join("<U%s> <U%s>: U%s # '%s' -> %s" % (self.deadkey,
114 for k, v in self.maps.items()) + "\n"
116 class MSKLCFile(object):
117 def __init__(self, filename):
118 self.filename = filename
126 with codecs.open(self.filename, 'r', encoding='utf-16') as file:
130 line, sep, comment = line.partition('//')
132 comment = comment.strip()
137 if state == 'shiftstate':
140 self.shiftmap.append([shift, shift in [0, 1, 6, 7]])
144 elif state == 'layout':
145 pieces = line.split()
146 if len(pieces) == len(self.shiftmap)+3:
147 self.parse_layout_line(pieces)
151 elif state in ['keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
153 hex, key = line.split("\t", 1)
158 elif state == 'deadkey':
159 match = re.match(r"""^([0-9a-fA-F]{4})\s+
160 ([0-9a-fA-F]{4})$""",
167 self.deadkeys[-1].add_entry(match.group(1),
174 key, sep, arg = line.partition("\t")
175 if not key.isupper():
180 self.metadata['layout'], sep, quoted = arg.partition("\t")
181 self.metadata['name'] = quoted[1:-1]
182 elif key in ['copyright', 'company', 'version']:
183 self.metadata['key'] = arg[1:-1]
184 elif key in ['localename', 'localeid']:
186 elif key in ['shiftstate', 'layout', 'keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
188 elif key == 'deadkey':
190 self.deadkeys.append(DeadKey(arg))
191 elif key == 'endkbd':
197 def parse_layout_line(self, pieces):
198 scancode = int(pieces[0], 16)
201 valid = ['-1','-1','-1','-1']
202 for i in range(3, len(pieces)):
203 shift, good = self.shiftmap[i-3]
205 valid[shift % 4] = pieces[i]
206 elif pieces[i] != '-1':
207 print >> sys.stderr, "skipping %(pc)s at position %(pos)u (shiftstate %(ss)u) for %(key)s" % {
213 mapping = KeyMapping(scancode, *valid)
214 self.mappings.append(mapping)
220 optp = OptionParser(usage=__doc__)
221 optp.add_option('--compose', dest='compose_file', metavar="FILE",
222 help="Write dead key definitions to compose file FILE")
223 options, args = optp.parse_args(args)
231 layout = MSKLCFile(filename)
233 print "xkb_symbols {"
234 for mapping in layout.mappings:
235 print mapping.to_xkb_def_str()
238 if options.compose_file is not None:
239 with open(options.compose_file, 'w') as compose_file:
240 for key in layout.deadkeys:
241 print >> compose_file, key.to_xcompose_str().encode('utf-8')
243 if __name__ == '__main__':