More metadata output
[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     # 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?
72     @classmethod
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]
77
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)
86         return None
87
88     def to_xkb_def_str(self):
89         xkbname = self.scancode_to_xkbname(self.scancode)
90         if not xkbname:
91             warn("unknown scancode %(v)u/0x%(v)02x" % {'v': self.scancode})
92
93         return ("key <%s> { [ %s, %s, %s, %s ] };"
94                 % tuple([xkbname]
95                     + [keysymdata.name(sym)
96                         for sym in [self.normal, self.shifted,
97                             self.altgr, self.shiftedaltgr]]))
98
99 def DDDD_to_name(DDDD):
100     "'0046' -> 'FULL STOP'"
101     return unicodedata.name(unichr(int(DDDD, 16)))
102
103 class DeadKey(object):
104     def __init__(self, deadkey):
105         self.deadkey = deadkey
106         self.maps = {}
107
108     def add_entry(self, key, value):
109         self.maps[key] = value
110
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),
115                                                          unichr(int(k, 16)),
116                                                          DDDD_to_name(v))
117                            for k, v in self.maps.items()) + "\n"
118
119 class MSKLCFile(object):
120     def __init__(self, filename):
121         self.filename = filename
122         self.mappings = []
123         self.deadkeys = []
124         self.metadata = {}
125         self.shiftmap = []
126         self.parse()
127
128     def parse(self):
129         with codecs.open(self.filename, 'r', encoding='utf-16') as file:
130             state = None
131
132             for line in file:
133                 line, sep, comment = line.partition('//')
134                 line = line.strip()
135                 comment = comment.strip()
136
137                 if line == '':
138                     continue
139
140                 if state == 'shiftstate':
141                     try:
142                         shift = int(line)
143                         self.shiftmap.append([shift, shift in [0, 1, 6, 7]])
144                         continue
145                     except ValueError:
146                         state = None
147                 elif state == 'layout':
148                     pieces = line.split()
149                     if len(pieces) == len(self.shiftmap)+3:
150                         self.parse_layout_line(pieces)
151                         continue
152                     else:
153                         state = None
154                 elif state in ['keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
155                     try:
156                         hex, key = line.split("\t", 1)
157                         hex = int(hex, 16)
158                         continue
159                     except ValueError:
160                         state = None
161                 elif state == 'deadkey':
162                     match = re.match(r"""^([0-9a-fA-F]{4})\s+
163                                           ([0-9a-fA-F]{4})$""",
164                                      line,
165                                      re.VERBOSE)
166                     if not match:
167                         state = None
168                     else:
169                         if match.group(1):
170                             self.deadkeys[-1].add_entry(match.group(1),
171                                                         match.group(2))
172                         continue
173                 elif state is not None:
174                     print line
175                     raise KeyError
176
177                 key, sep, arg = line.partition("\t")
178                 if not key.isupper():
179                     print line
180                     raise KeyError
181                 key = key.lower()
182                 if key == 'kbd':
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']:
190                     state = key
191                 elif key == 'deadkey':
192                     state = key
193                     self.deadkeys.append(DeadKey(arg))
194                 elif key == 'endkbd':
195                     break
196                 else:
197                     print line
198                     raise KeyError
199
200     def parse_layout_line(self, pieces):
201         scancode = int(pieces[0], 16)
202         vk = pieces[1]
203         cap = pieces[2]
204         valid = ['-1','-1','-1','-1']
205         for i in range(3, len(pieces)):
206             shift, good = self.shiftmap[i-3]
207             if good:
208                 valid[shift % 4] = pieces[i]
209             elif pieces[i] != '-1':
210                 xkbname = KeyMapping.scancode_to_xkbname(scancode)
211                 if xkbname:
212                     key = "%(xkb)s (%(v)u/0x%(v)02x)"
213                 else:
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" % {
217                     'pc': pieces[i],
218                     'pos': i,
219                     'ss': shift,
220                     'key': key
221                     })
222         mapping = KeyMapping(scancode, *valid)
223         self.mappings.append(mapping)
224
225 def main(args=None):
226     if args is None:
227         args = sys.argv[1:]
228
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)
233
234     if len(args) != 1:
235         optp.print_help()
236         return 1
237
238     filename = args[0]
239
240     layout = MSKLCFile(filename)
241
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([
248                 key.title(),
249                 layout.metadata[key]
250                 ])
251
252     print "default partial alphanumeric_keys"
253     if layout.metadata['layout']:
254         print 'xkb_symbols "%s" {' % layout.metadata['layout']
255     else:
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()
261     print "};"
262
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()
267
268 if __name__ == '__main__':
269     sys.exit(main())