Refactor warnings
[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 import keysymdata
45 from optparse import OptionParser
46
47 def warn(msg):
48     print >> sys.stderr, msg
49     print "//", msg
50
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)
58
59     @classmethod
60     def _cleanup(cls, charstr):
61         if charstr == '-1':
62             return None
63         noat, at, junk = charstr.partition('@')
64         match = re.match('^[0-9A-Fa-f]{4}$', noat)
65         if match:
66             return unichr(int(match.group(), 16))
67         assert len(noat) == 1
68         return noat
69
70     @classmethod
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]
75
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)
84         return None
85
86     def to_xkb_def_str(self):
87         xkbname = self.scancode_to_xkbname(self.scancode)
88         if not xkbname:
89             warn("unknown scan code %(v)u/0x%(v)02x" % {'v': self.scancode})
90
91         return ("key <%s> { [ %s, %s, %s, %s ] };"
92                 % tuple([xkbname]
93                     + [keysymdata.name(sym)
94                         for sym in [self.normal, self.shifted,
95                             self.altgr, self.shiftedaltgr]]))
96
97 def DDDD_to_name(DDDD):
98     "'0046' -> 'FULL STOP'"
99     return unicodedata.name(unichr(int(DDDD, 16)))
100
101 class DeadKey(object):
102     def __init__(self, deadkey):
103         self.deadkey = deadkey
104         self.maps = {}
105
106     def add_entry(self, key, value):
107         self.maps[key] = value
108
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),
113                                                          unichr(int(k, 16)),
114                                                          DDDD_to_name(v))
115                            for k, v in self.maps.items()) + "\n"
116
117 class MSKLCFile(object):
118     def __init__(self, filename):
119         self.filename = filename
120         self.mappings = []
121         self.deadkeys = []
122         self.metadata = {}
123         self.shiftmap = []
124         self.parse()
125
126     def parse(self):
127         with codecs.open(self.filename, 'r', encoding='utf-16') as file:
128             state = None
129
130             for line in file:
131                 line, sep, comment = line.partition('//')
132                 line = line.strip()
133                 comment = comment.strip()
134
135                 if line == '':
136                     continue
137
138                 if state == 'shiftstate':
139                     try:
140                         shift = int(line)
141                         self.shiftmap.append([shift, shift in [0, 1, 6, 7]])
142                         continue
143                     except ValueError:
144                         state = None
145                 elif state == 'layout':
146                     pieces = line.split()
147                     if len(pieces) == len(self.shiftmap)+3:
148                         self.parse_layout_line(pieces)
149                         continue
150                     else:
151                         state = None
152                 elif state in ['keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
153                     try:
154                         hex, key = line.split("\t", 1)
155                         hex = int(hex, 16)
156                         continue
157                     except ValueError:
158                         state = None
159                 elif state == 'deadkey':
160                     match = re.match(r"""^([0-9a-fA-F]{4})\s+
161                                           ([0-9a-fA-F]{4})$""",
162                                      line,
163                                      re.VERBOSE)
164                     if not match:
165                         state = None
166                     else:
167                         if match.group(1):
168                             self.deadkeys[-1].add_entry(match.group(1),
169                                                         match.group(2))
170                         continue
171                 elif state is not None:
172                     print line
173                     raise KeyError
174
175                 key, sep, arg = line.partition("\t")
176                 if not key.isupper():
177                     print line
178                     raise KeyError
179                 key = key.lower()
180                 if key == 'kbd':
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']:
188                     state = key
189                 elif key == 'deadkey':
190                     state = key
191                     self.deadkeys.append(DeadKey(arg))
192                 elif key == 'endkbd':
193                     break
194                 else:
195                     print line
196                     raise KeyError
197
198     def parse_layout_line(self, pieces):
199         scancode = int(pieces[0], 16)
200         vk = pieces[1]
201         cap = pieces[2]
202         valid = ['-1','-1','-1','-1']
203         for i in range(3, len(pieces)):
204             shift, good = self.shiftmap[i-3]
205             if good:
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" % {
209                     'pc': pieces[i],
210                     'pos': i,
211                     'ss': shift,
212                     'key': pieces[0]
213                     })
214         mapping = KeyMapping(scancode, *valid)
215         self.mappings.append(mapping)
216
217 def main(args=None):
218     if args is None:
219         args = sys.argv[1:]
220
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)
225
226     if len(args) != 1:
227         optp.print_help()
228         return 1
229
230     filename = args[0]
231
232     layout = MSKLCFile(filename)
233
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([
240                 key.title(),
241                 layout.metadata[key]
242                 ])
243
244     if layout.metadata['layout']:
245         print 'xkb_symbols "%s" {' % layout.metadata['layout']
246     else:
247         print "xkb_symbols {"
248     for mapping in layout.mappings:
249         print mapping.to_xkb_def_str()
250     print "};"
251
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()
256
257 if __name__ == '__main__':
258     sys.exit(main())