Fully parse KLC files
[xorg/xkbtools] / klc2xkb
1 #!/usr/bin/env python
2 # coding: utf-8
3
4 """%(prog) [options] KLCFILE
5
6 Read a .KLC file saved by Microsoft Keyboard Layout Creator, and print
7 an equivalent xkb_symbol description.
8
9 The resulting output can be included in the xkb_symbol section of a keymap
10 description. This may work for you:
11
12  $ %(prog) foo.klc > foo.xkbsym
13  $ setxkbmap -print \
14  >       | sed '/xkb_symbols/s/};/\n\t\t\tinclude "foo.xkbsym" };/' \
15  >       | xkbcomp - "${DISPLAY}"
16  $ setxkbmap -option lv3:ralt_switch # For AltGr
17
18 To use the optional dead-key compose file, written out to say
19 ~/.XCompose.dead_keys, create a file called ~/.XCompose containing
20
21  include "%L"
22  include "%H/.XCompose.dead_keys"
23
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
26 more information.
27
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.
31
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,
35 while X ignores both.
36 """
37
38 from __future__ import with_statement
39
40 import codecs
41 import re
42 import sys
43 import unicodedata
44 from optparse import OptionParser
45
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)
53
54     @classmethod
55     def _cleanup(cls, charstr):
56         if charstr == '-1':
57             return None
58         match = re.match('[0-9A-Fa-f]{4}', charstr)
59         if match:
60             return unichr(int(match.group(), 16))
61         assert len(charstr) == 1
62         return charstr
63
64     @classmethod
65     def scancode_to_xkbname(cls, scancode):
66         special = {0x29: "TLDE", 0x2b: "BKSL", 0x39: "SPCE", 0x53: "KPDL"}
67         if scancode in special:
68             return special[scancode]
69
70         elif scancode <= 0x0d:
71             return "AE%02d" % (scancode - 1)
72         elif scancode <= 0x1b:
73             return "AD%02d" % (scancode - 0xf)
74         elif scancode <= 0x28:
75             return "AC%02d" % (scancode - 0x1d)
76         elif scancode <= 0x35:
77             return "AB%02d" % (scancode - 0x2b)
78         return None
79
80     def to_xkb_def_str(self):
81         xkbname = self.scancode_to_xkbname(self.scancode)
82         if not xkbname:
83             return ""
84         def format_symbol(sym):
85             if sym is None:
86                 return "NoSymbol"
87             return "U%04x" % ord(sym)
88
89         return ("key <%s> { [ %s, %s, %s, %s ] };"
90                 % tuple([xkbname]
91                     + [format_symbol(sym)
92                         for sym in [self.normal, self.shifted,
93                             self.altgr, self.shiftedaltgr]]))
94
95 def DDDD_to_name(DDDD):
96     "'0046' -> 'FULL STOP'"
97     return unicodedata.name(unichr(int(DDDD, 16)))
98
99 class DeadKey(object):
100     def __init__(self, deadkey):
101         self.deadkey = deadkey
102         self.maps = {}
103
104     def add_entry(self, key, value):
105         self.maps[key] = value
106
107     def to_xcompose_str(self):
108         return '# %s' % DDDD_to_name(self.deadkey) + "\n" \
109                + "\n".join("<U%s> <U%s>: U%s # '%s' -> %s" % (self.deadkey,
110                                                          k, v,
111                                                          unichr(int(k, 16)),
112                                                          DDDD_to_name(v))
113                            for k, v in self.maps.items()) + "\n"
114
115 class MSKLCFile(object):
116     def __init__(self, filename):
117         self.filename = filename
118         self.mappings = []
119         self.deadkeys = []
120         self.metadata = {}
121         self.shiftmap = []
122         self.parse()
123
124     def parse(self):
125         with codecs.open(self.filename, 'r', encoding='utf-16') as file:
126             state = None
127
128             for line in file:
129                 line, sep, comment = line.partition('//')
130                 line = line.strip()
131                 comment = comment.strip()
132
133                 if line == '':
134                     continue
135
136                 if state == 'shiftstate':
137                     try:
138                         shift = int(line)
139                         self.shiftmap.append([shift, shift in [0, 1, 6, 7]])
140                         continue
141                     except ValueError:
142                         state = None
143                 elif state == 'layout':
144                     pieces = line.split()
145                     if len(pieces) == len(self.shiftmap)+3:
146                         self.parse_layout_line(pieces)
147                         continue
148                     else:
149                         state = None
150                 elif state in ['keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
151                     try:
152                         hex, key = line.split("\t", 1)
153                         hex = int(hex, 16)
154                         continue
155                     except ValueError:
156                         state = None
157                 elif state == 'deadkey':
158                     match = re.match(r"""^([0-9a-fA-F]{4})\s+
159                                           ([0-9a-fA-F]{4})$""",
160                                      line,
161                                      re.VERBOSE)
162                     if not match:
163                         state = None
164                     else:
165                         if match.group(1):
166                             self.deadkeys[-1].add_entry(match.group(1),
167                                                         match.group(2))
168                         continue
169                 elif state != None:
170                     print line
171                     raise KeyError
172
173                 key, sep, arg = line.partition("\t")
174                 if not key.isupper():
175                     print line
176                     raise KeyError
177                 key = key.lower()
178                 if key == 'kbd':
179                     self.metadata['layout'], sep, quoted = arg.partition("\t")
180                     self.metadata['name'] = quoted[1:-1]
181                 elif key in ['copyright', 'company', 'version']:
182                     self.metadata['key'] = arg[1:-1]
183                 elif key in ['localename', 'localeid']:
184                     continue
185                 elif key in ['shiftstate', 'layout', 'keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
186                     state = key
187                 elif key == 'deadkey':
188                     state = key
189                     self.deadkeys.append(DeadKey(arg))
190                 elif key == 'endkbd':
191                     break
192                 else:
193                     print line
194                     raise KeyError
195
196     def parse_layout_line(self, pieces):
197         scancode = int(pieces[0], 16)
198         vk = pieces[1]
199         cap = pieces[2]
200         valid = ['-1','-1','-1','-1']
201         for i in range(3, len(pieces)):
202             shift, good = self.shiftmap[i-3]
203             if good:
204                 valid[shift % 4] = pieces[i]
205             elif pieces[i] != '-1':
206                 print >> sys.stderr, "skipping %(pc)s at position %(pos)u (shiftstate %(ss)u) for %(key)s" % {
207                     'pc': pieces[i],
208                     'pos': i,
209                     'ss': shift,
210                     'key': pieces[0]
211                     }
212         mapping = KeyMapping(scancode, *valid)
213         self.mappings.append(mapping)
214
215 def main(args=None):
216     if args is None:
217         args = sys.argv[1:]
218
219     optp = OptionParser(usage=__doc__)
220     optp.add_option('--compose', dest='compose_file', metavar="FILE",
221             help="Write dead key definitions to compose file FILE")
222     options, args = optp.parse_args(args)
223
224     if len(args) != 1:
225         optp.print_help()
226         return 1
227
228     filename = args[0]
229
230     layout = MSKLCFile(filename)
231
232     print "xkb_symbols {"
233     for mapping in layout.mappings:
234         print mapping.to_xkb_def_str()
235     print "};"
236
237     if options.compose_file is not None:
238         with open(options.compose_file, 'w') as compose_file:
239             for key in layout.deadkeys:
240                 print >> compose_file, key.to_xcompose_str()
241
242 if __name__ == '__main__':
243     sys.exit(main())