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 scancode %(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 xkbname = KeyMapping.scancode_to_xkbname(scancode)
210 key = "%(xkb)s (%(v)u/0x%(v)02x)"
212 key = "(%(v)u/0x%(v)02x)"
213 key = key % { 'xkb': xkbname, 'v': scancode }
214 warn("skipping %(pc)s at position %(pos)u (shiftstate %(ss)u) for %(key)s" % {
220 mapping = KeyMapping(scancode, *valid)
221 self.mappings.append(mapping)
227 optp = OptionParser(usage=__doc__)
228 optp.add_option('--compose', dest='compose_file', metavar="FILE",
229 help="Write dead key definitions to compose file FILE")
230 options, args = optp.parse_args(args)
238 layout = MSKLCFile(filename)
240 if layout.metadata['name']:
241 print "// %(name)s" % layout.metadata
242 print "// Converted from %s" % filename
243 for key in ['copyright', 'company', 'version']:
244 if key in layout.metadata:
245 print "// %s: %s" % tuple([
250 if layout.metadata['layout']:
251 print 'xkb_symbols "%s" {' % layout.metadata['layout']
253 print "xkb_symbols {"
254 for mapping in layout.mappings:
255 print mapping.to_xkb_def_str()
258 if options.compose_file is not None:
259 with codecs.open(options.compose_file, 'w', encoding='utf-8') as compose_file:
260 for key in layout.deadkeys:
261 print >> compose_file, key.to_xcompose_str()
263 if __name__ == '__main__':