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