Initial import from neitsch's repository
[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         match = re.match('[0-9A-Fa-f]{4}', charstr)
59         if match:
60             return unichr(int(match.group(), 16))
61         assert len(charstr) == 1
62         return charstr
63
64     @classmethod
65     def scancode_to_xkbname(cls, scancode):
66         special = {0x29: "TLDE", 0x2b: "BKSL", 0x39: "SPCE", 0x53: "KPDL"}
67         if scancode in special:
68             return special[scancode]
69
70         elif scancode <= 0x0d:
71             return "AE%02d" % (scancode - 1)
72         elif scancode <= 0x1b:
73             return "AD%02d" % (scancode - 0xf)
74         elif scancode <= 0x28:
75             return "AC%02d" % (scancode - 0x1d)
76         elif scancode <= 0x35:
77             return "AB%02d" % (scancode - 0x2b)
78         return None
79
80     def to_xkb_def_str(self):
81         xkbname = self.scancode_to_xkbname(self.scancode)
82         if not xkbname:
83             return ""
84         def format_symbol(sym):
85             if sym is None:
86                 return "NoSymbol"
87             return "U%04x" % ord(sym)
88
89         return ("key <%s> { [ %s, %s, %s, %s ] };"
90                 % tuple([xkbname]
91                     + [format_symbol(sym)
92                         for sym in [self.normal, self.shifted,
93                             self.altgr, self.shiftedaltgr]]))
94
95 def DDDD_to_name(DDDD):
96     "'0046' -> 'FULL STOP'"
97     return unicodedata.name(unichr(int(DDDD, 16)))
98
99 class DeadKey(object):
100     def __init__(self, deadkey):
101         self.deadkey = deadkey
102         self.maps = {}
103
104     def add_entry(self, key, value):
105         self.maps[key] = value
106
107     def to_xcompose_str(self):
108         return '# %s' % DDDD_to_name(self.deadkey) + "\n" \
109                + "\n".join("<U%s> <U%s>: U%s # '%s' -> %s" % (self.deadkey,
110                                                          k, v,
111                                                          unichr(int(k, 16)),
112                                                          DDDD_to_name(v))
113                            for k, v in self.maps.items()) + "\n"
114
115 class MSKLCFile(object):
116     def __init__(self, filename):
117         self.filename = filename
118         self.mappings = []
119         self.deadkeys = []
120         self.parse()
121
122     def parse(self):
123         with codecs.open(self.filename, 'r', encoding='utf-16') as file:
124             in_deadkey_def = False
125
126             for line in file:
127                 line = line.strip()
128
129                 if in_deadkey_def:
130                     match = re.match(r"""^([0-9a-fA-F]{4})\s+
131                                           ([0-9a-fA-F]{4})\s+
132                                           //
133                                           |\s?$""",
134                                      line,
135                                      re.VERBOSE)
136                     if not match:
137                         in_deadkey_def = False
138                     else:
139                         if match.group(1):
140                             self.deadkeys[-1].add_entry(match.group(1),
141                                                         match.group(2))
142                         continue
143
144                 match = re.match(r'^DEADKEY\s+([0-9a-fA-F]{4})$', line)
145                 if match:
146                     in_deadkey_def = True
147                     self.deadkeys.append(DeadKey(match.group(1)))
148
149                 match = re.match('(.*?)//', line)
150                 if match:
151                     pieces = match.group().split()
152                     if len(pieces) == 9:
153                         self.parse_layout_line(pieces)
154
155     def parse_layout_line(self, pieces):
156         scancode, vk, cap, normal, shift, ctrl, altgr, shiftaltgr, junk = \
157                   pieces
158         scancode = int(scancode, 16)
159         mapping = KeyMapping(scancode, normal, shift, altgr, shiftaltgr)
160         self.mappings.append(mapping)
161
162 def main(args=None):
163     if args is None:
164         args = sys.argv[1:]
165
166     optp = OptionParser(usage=__doc__)
167     optp.add_option('--compose', dest='compose_file', metavar="FILE",
168             help="Write dead key definitions to compose file FILE")
169     options, args = optp.parse_args(args)
170
171     if len(args) != 1:
172         optp.print_help()
173         return 1
174
175     filename = args[0]
176
177     layout = MSKLCFile(filename)
178
179     print "xkb_symbols {"
180     for mapping in layout.mappings:
181         print mapping.to_xkb_def_str()
182     print "};"
183
184     if options.compose_file is not None:
185         with open(options.compose_file, 'w') as compose_file:
186             for key in layout.deadkeys:
187                 print >> compose_file, key.to_xcompose_str()
188
189 if __name__ == '__main__':
190     sys.exit(main())