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))
70 # These scancode conversion is only correct for PC keyboards, i.e. it uses
71 # the values from keycodes/xfree86. Do we want to allow other archs too?
73 def scancode_to_xkbname(cls, scancode):
74 special = {0x29: "TLDE", 0x2b: "BKSL", 0x39: "SPCE", 0x53: "KPDL", 0x56: "LSGT"}
75 if scancode in special:
76 return special[scancode]
78 elif scancode <= 0x0d:
79 return "AE%02d" % (scancode - 1)
80 elif scancode <= 0x1b:
81 return "AD%02d" % (scancode - 0xf)
82 elif scancode <= 0x28:
83 return "AC%02d" % (scancode - 0x1d)
84 elif scancode <= 0x35:
85 return "AB%02d" % (scancode - 0x2b)
88 def to_xkb_def_str(self):
89 xkbname = self.scancode_to_xkbname(self.scancode)
91 warn("unknown scancode %(v)u/0x%(v)02x" % {'v': self.scancode})
93 return ("key <%s> { [ %s, %s, %s, %s ] };"
95 + [keysymdata.name(sym)
96 for sym in [self.normal, self.shifted,
97 self.altgr, self.shiftedaltgr]]))
99 def DDDD_to_name(DDDD):
100 "'0046' -> 'FULL STOP'"
101 return unicodedata.name(unichr(int(DDDD, 16)))
103 class DeadKey(object):
104 def __init__(self, deadkey):
105 self.deadkey = deadkey
108 def add_entry(self, key, value):
109 self.maps[key] = value
111 def to_xcompose_str(self):
112 return '# %s' % DDDD_to_name(self.deadkey) + "\n" \
113 + "\n".join("<%s> <%s>:\t%s\t# '%s' -> %s" % (keysymdata.name(self.deadkey),
114 keysymdata.name(k), keysymdata.name(v),
117 for k, v in self.maps.items()) + "\n"
119 class MSKLCFile(object):
120 def __init__(self, filename):
121 self.filename = filename
129 with codecs.open(self.filename, 'r', encoding='utf-16') as file:
133 line, sep, comment = line.partition('//')
135 comment = comment.strip()
140 if state == 'shiftstate':
143 self.shiftmap.append([shift, shift in [0, 1, 6, 7]])
147 elif state == 'layout':
148 pieces = line.split()
149 if len(pieces) == len(self.shiftmap)+3:
150 self.parse_layout_line(pieces)
154 elif state in ['keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
156 hex, key = line.split("\t", 1)
161 elif state == 'deadkey':
162 match = re.match(r"""^([0-9a-fA-F]{4})\s+
163 ([0-9a-fA-F]{4})$""",
170 self.deadkeys[-1].add_entry(match.group(1),
173 elif state is not None:
177 key, sep, arg = line.partition("\t")
178 if not key.isupper():
183 self.metadata['layout'], sep, quoted = arg.partition("\t")
184 self.metadata['name'] = quoted[1:-1]
185 elif key in ['copyright', 'company', 'localename', 'localeid']:
186 self.metadata[key] = arg[1:-1]
187 elif key == 'version':
188 self.metadata[key] = arg
189 elif key in ['shiftstate', 'layout', 'keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
191 elif key == 'deadkey':
193 self.deadkeys.append(DeadKey(arg))
194 elif key == 'endkbd':
200 def parse_layout_line(self, pieces):
201 scancode = int(pieces[0], 16)
204 valid = ['-1','-1','-1','-1']
205 for i in range(3, len(pieces)):
206 shift, good = self.shiftmap[i-3]
208 valid[shift % 4] = pieces[i]
209 elif pieces[i] != '-1':
210 xkbname = KeyMapping.scancode_to_xkbname(scancode)
212 key = "%(xkb)s (%(v)u/0x%(v)02x)"
214 key = "(%(v)u/0x%(v)02x)"
215 key = key % { 'xkb': xkbname, 'v': scancode }
216 warn("skipping %(pc)s at position %(pos)u (shiftstate %(ss)u) for %(key)s" % {
222 mapping = KeyMapping(scancode, *valid)
223 self.mappings.append(mapping)
229 optp = OptionParser(usage=__doc__)
230 optp.add_option('--compose', dest='compose_file', metavar="FILE",
231 help="Write dead key definitions to compose file FILE")
232 options, args = optp.parse_args(args)
240 layout = MSKLCFile(filename)
242 if layout.metadata['name']:
243 print "// %(name)s" % layout.metadata
244 print "// Converted from %s" % filename
245 for key in ['copyright', 'company', 'version']:
246 if key in layout.metadata:
247 print "// %s: %s" % tuple([
252 print "default partial alphanumeric_keys"
253 if layout.metadata['layout']:
254 print 'xkb_symbols "%s" {' % layout.metadata['layout']
256 print "xkb_symbols {"
257 if layout.metadata['name']:
258 print 'name[Group1] = "%(name)s";' % layout.metadata
259 for mapping in layout.mappings:
260 print mapping.to_xkb_def_str()
263 if options.compose_file is not None:
264 with codecs.open(options.compose_file, 'w', encoding='utf-8') as compose_file:
265 for key in layout.deadkeys:
266 print >> compose_file, key.to_xcompose_str()
268 if __name__ == '__main__':