Any character can be dead in KLC
[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         noat, at, junk = charstr.partition('@')
59         match = re.match('^[0-9A-Fa-f]{4}$', noat)
60         if match:
61             return unichr(int(match.group(), 16))
62         assert len(noat) == 1
63         return noat
64
65     @classmethod
66     def scancode_to_xkbname(cls, scancode):
67         special = {0x29: "TLDE", 0x2b: "BKSL", 0x39: "SPCE", 0x53: "KPDL"}
68         if scancode in special:
69             return special[scancode]
70
71         elif scancode <= 0x0d:
72             return "AE%02d" % (scancode - 1)
73         elif scancode <= 0x1b:
74             return "AD%02d" % (scancode - 0xf)
75         elif scancode <= 0x28:
76             return "AC%02d" % (scancode - 0x1d)
77         elif scancode <= 0x35:
78             return "AB%02d" % (scancode - 0x2b)
79         return None
80
81     def to_xkb_def_str(self):
82         xkbname = self.scancode_to_xkbname(self.scancode)
83         if not xkbname:
84             return ""
85         def format_symbol(sym):
86             if sym is None:
87                 return "NoSymbol"
88             return "U%04x" % ord(sym)
89
90         return ("key <%s> { [ %s, %s, %s, %s ] };"
91                 % tuple([xkbname]
92                     + [format_symbol(sym)
93                         for sym in [self.normal, self.shifted,
94                             self.altgr, self.shiftedaltgr]]))
95
96 def DDDD_to_name(DDDD):
97     "'0046' -> 'FULL STOP'"
98     return unicodedata.name(unichr(int(DDDD, 16)))
99
100 class DeadKey(object):
101     def __init__(self, deadkey):
102         self.deadkey = deadkey
103         self.maps = {}
104
105     def add_entry(self, key, value):
106         self.maps[key] = value
107
108     def to_xcompose_str(self):
109         return '# %s' % DDDD_to_name(self.deadkey) + "\n" \
110                + "\n".join("<U%s> <U%s>: U%s # '%s' -> %s" % (self.deadkey,
111                                                          k, v,
112                                                          unichr(int(k, 16)),
113                                                          DDDD_to_name(v))
114                            for k, v in self.maps.items()) + "\n"
115
116 class MSKLCFile(object):
117     def __init__(self, filename):
118         self.filename = filename
119         self.mappings = []
120         self.deadkeys = []
121         self.metadata = {}
122         self.shiftmap = []
123         self.parse()
124
125     def parse(self):
126         with codecs.open(self.filename, 'r', encoding='utf-16') as file:
127             state = None
128
129             for line in file:
130                 line, sep, comment = line.partition('//')
131                 line = line.strip()
132                 comment = comment.strip()
133
134                 if line == '':
135                     continue
136
137                 if state == 'shiftstate':
138                     try:
139                         shift = int(line)
140                         self.shiftmap.append([shift, shift in [0, 1, 6, 7]])
141                         continue
142                     except ValueError:
143                         state = None
144                 elif state == 'layout':
145                     pieces = line.split()
146                     if len(pieces) == len(self.shiftmap)+3:
147                         self.parse_layout_line(pieces)
148                         continue
149                     else:
150                         state = None
151                 elif state in ['keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
152                     try:
153                         hex, key = line.split("\t", 1)
154                         hex = int(hex, 16)
155                         continue
156                     except ValueError:
157                         state = None
158                 elif state == 'deadkey':
159                     match = re.match(r"""^([0-9a-fA-F]{4})\s+
160                                           ([0-9a-fA-F]{4})$""",
161                                      line,
162                                      re.VERBOSE)
163                     if not match:
164                         state = None
165                     else:
166                         if match.group(1):
167                             self.deadkeys[-1].add_entry(match.group(1),
168                                                         match.group(2))
169                         continue
170                 elif state != None:
171                     print line
172                     raise KeyError
173
174                 key, sep, arg = line.partition("\t")
175                 if not key.isupper():
176                     print line
177                     raise KeyError
178                 key = key.lower()
179                 if key == 'kbd':
180                     self.metadata['layout'], sep, quoted = arg.partition("\t")
181                     self.metadata['name'] = quoted[1:-1]
182                 elif key in ['copyright', 'company', 'version']:
183                     self.metadata['key'] = arg[1:-1]
184                 elif key in ['localename', 'localeid']:
185                     continue
186                 elif key in ['shiftstate', 'layout', 'keyname', 'keyname_ext', 'keyname_dead', 'descriptions', 'languagenames']:
187                     state = key
188                 elif key == 'deadkey':
189                     state = key
190                     self.deadkeys.append(DeadKey(arg))
191                 elif key == 'endkbd':
192                     break
193                 else:
194                     print line
195                     raise KeyError
196
197     def parse_layout_line(self, pieces):
198         scancode = int(pieces[0], 16)
199         vk = pieces[1]
200         cap = pieces[2]
201         valid = ['-1','-1','-1','-1']
202         for i in range(3, len(pieces)):
203             shift, good = self.shiftmap[i-3]
204             if good:
205                 valid[shift % 4] = pieces[i]
206             elif pieces[i] != '-1':
207                 print >> sys.stderr, "skipping %(pc)s at position %(pos)u (shiftstate %(ss)u) for %(key)s" % {
208                     'pc': pieces[i],
209                     'pos': i,
210                     'ss': shift,
211                     'key': pieces[0]
212                     }
213         mapping = KeyMapping(scancode, *valid)
214         self.mappings.append(mapping)
215
216 def main(args=None):
217     if args is None:
218         args = sys.argv[1:]
219
220     optp = OptionParser(usage=__doc__)
221     optp.add_option('--compose', dest='compose_file', metavar="FILE",
222             help="Write dead key definitions to compose file FILE")
223     options, args = optp.parse_args(args)
224
225     if len(args) != 1:
226         optp.print_help()
227         return 1
228
229     filename = args[0]
230
231     layout = MSKLCFile(filename)
232
233     print "xkb_symbols {"
234     for mapping in layout.mappings:
235         print mapping.to_xkb_def_str()
236     print "};"
237
238     if options.compose_file is not None:
239         with open(options.compose_file, 'w') as compose_file:
240             for key in layout.deadkeys:
241                 print >> compose_file, key.to_xcompose_str()
242
243 if __name__ == '__main__':
244     sys.exit(main())