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
45 from optparse import OptionParser
48 print >> sys.stderr, msg
51 class KeyMapping(object):
52 def __init__(self, scancode, normal, shifted, altgr, shiftedaltgr):
53 self.scancode = scancode
54 self.normal = self._cleanup(normal)
55 self.shifted = self._cleanup(shifted)
56 self.altgr = self._cleanup(altgr)
57 self.shiftedaltgr = self._cleanup(shiftedaltgr)
60 def _cleanup(cls, charstr):
63 noat, at, junk = charstr.partition('@')
64 match = re.match('^[0-9A-Fa-f]{4}$', noat)
66 return unichr(int(match.group(), 16))
71 def scancode_to_xkbname(cls, scancode):
72 special = {0x29: "TLDE", 0x2b: "BKSL", 0x39: "SPCE", 0x53: "KPDL"}
73 if scancode in special:
74 return special[scancode]
76 elif scancode <= 0x0d:
77 return "AE%02d" % (scancode - 1)
78 elif scancode <= 0x1b:
79 return "AD%02d" % (scancode - 0xf)
80 elif scancode <= 0x28:
81 return "AC%02d" % (scancode - 0x1d)
82 elif scancode <= 0x35:
83 return "AB%02d" % (scancode - 0x2b)
86 def to_xkb_def_str(self):
87 xkbname = self.scancode_to_xkbname(self.scancode)
89 warn("unknown scan code %(v)u/0x%(v)02x" % {'v': self.scancode})
91 return ("key <%s> { [ %s, %s, %s, %s ] };"
93 + [keysymdata.name(sym)
94 for sym in [self.normal, self.shifted,
95 self.altgr, self.shiftedaltgr]]))
97 def DDDD_to_name(DDDD):
98 "'0046' -> 'FULL STOP'"
99 return unicodedata.name(unichr(int(DDDD, 16)))
101 class DeadKey(object):
102 def __init__(self, deadkey):
103 self.deadkey = deadkey
106 def add_entry(self, key, value):
107 self.maps[key] = value
109 def to_xcompose_str(self):
110 return '# %s' % DDDD_to_name(self.deadkey) + "\n" \
111 + "\n".join("<%s> <%s>:\t%s\t# '%s' -> %s" % (keysymdata.name(self.deadkey),
112 keysymdata.name(k), keysymdata.name(v),
115 for k, v in self.maps.items()) + "\n"
117 class MSKLCFile(object):
118 def __init__(self, filename):
119 self.filename = filename
127 with codecs.open(self.filename, 'r', encoding='utf-16') as file:
131 line, sep, comment = line.partition('//')
133 comment = comment.strip()
138 if state == 'shiftstate':
141 self.shiftmap.append([shift, shift in [0, 1, 6, 7]])
145 elif state == 'layout':
146 pieces = line.split()
147 if len(pieces) == len(self.shiftmap)+3:
148 self.parse_layout_line(pieces)
152 elif state in ['keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
154 hex, key = line.split("\t", 1)
159 elif state == 'deadkey':
160 match = re.match(r"""^([0-9a-fA-F]{4})\s+
161 ([0-9a-fA-F]{4})$""",
168 self.deadkeys[-1].add_entry(match.group(1),
171 elif state is not None:
175 key, sep, arg = line.partition("\t")
176 if not key.isupper():
181 self.metadata['layout'], sep, quoted = arg.partition("\t")
182 self.metadata['name'] = quoted[1:-1]
183 elif key in ['copyright', 'company', 'localename', 'localeid']:
184 self.metadata[key] = arg[1:-1]
185 elif key == 'version':
186 self.metadata[key] = arg
187 elif key in ['shiftstate', 'layout', 'keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
189 elif key == 'deadkey':
191 self.deadkeys.append(DeadKey(arg))
192 elif key == 'endkbd':
198 def parse_layout_line(self, pieces):
199 scancode = int(pieces[0], 16)
202 valid = ['-1','-1','-1','-1']
203 for i in range(3, len(pieces)):
204 shift, good = self.shiftmap[i-3]
206 valid[shift % 4] = pieces[i]
207 elif pieces[i] != '-1':
208 warn("skipping %(pc)s at position %(pos)u (shiftstate %(ss)u) for %(key)s" % {
214 mapping = KeyMapping(scancode, *valid)
215 self.mappings.append(mapping)
221 optp = OptionParser(usage=__doc__)
222 optp.add_option('--compose', dest='compose_file', metavar="FILE",
223 help="Write dead key definitions to compose file FILE")
224 options, args = optp.parse_args(args)
232 layout = MSKLCFile(filename)
234 if layout.metadata['name']:
235 print "// %(name)s" % layout.metadata
236 print "// Converted from %s" % filename
237 for key in ['copyright', 'company', 'version']:
238 if key in layout.metadata:
239 print "// %s: %s" % tuple([
244 if layout.metadata['layout']:
245 print 'xkb_symbols "%s" {' % layout.metadata['layout']
247 print "xkb_symbols {"
248 for mapping in layout.mappings:
249 print mapping.to_xkb_def_str()
252 if options.compose_file is not None:
253 with codecs.open(options.compose_file, 'w', encoding='utf-8') as compose_file:
254 for key in layout.deadkeys:
255 print >> compose_file, key.to_xcompose_str()
257 if __name__ == '__main__':