Warning message beautifying
[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 scancode %(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                 xkbname = KeyMapping.scancode_to_xkbname(scancode)
209                 if xkbname:
210                     key = "%(xkb)s (%(v)u/0x%(v)02x)"
211                 else:
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" % {
215                     'pc': pieces[i],
216                     'pos': i,
217                     'ss': shift,
218                     'key': key
219                     })
220         mapping = KeyMapping(scancode, *valid)
221         self.mappings.append(mapping)
222
223 def main(args=None):
224     if args is None:
225         args = sys.argv[1:]
226
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)
231
232     if len(args) != 1:
233         optp.print_help()
234         return 1
235
236     filename = args[0]
237
238     layout = MSKLCFile(filename)
239
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([
246                 key.title(),
247                 layout.metadata[key]
248                 ])
249
250     if layout.metadata['layout']:
251         print 'xkb_symbols "%s" {' % layout.metadata['layout']
252     else:
253         print "xkb_symbols {"
254     for mapping in layout.mappings:
255         print mapping.to_xkb_def_str()
256     print "};"
257
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()
262
263 if __name__ == '__main__':
264     sys.exit(main())