1706f2543Smrg#!/usr/bin/python
2706f2543Smrg#
3706f2543Smrg# Convert xorg keys from hal FDIs files to xorg.conf InputClass sections.
4706f2543Smrg# Modified from Martin Pitt's original fdi2mpi.py script:
5706f2543Smrg# http://cgit.freedesktop.org/media-player-info/tree/tools/fdi2mpi.py
6706f2543Smrg#
7706f2543Smrg# (C) 2010 Dan Nicholson
8706f2543Smrg# (C) 2009 Canonical Ltd.
9706f2543Smrg# Author: Dan Nicholson <dbn.lists@gmail.com>
10706f2543Smrg# Author: Martin Pitt <martin.pitt@ubuntu.com>
11706f2543Smrg#
12706f2543Smrg# Permission is hereby granted, free of charge, to any person obtaining a copy
13706f2543Smrg# of this software and associated documentation files (the "Software"), to
14706f2543Smrg# deal in the Software without restriction, including without limitation the
15706f2543Smrg# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
16706f2543Smrg# sell copies of the Software, and to permit persons to whom the Software is
17706f2543Smrg# fur- nished to do so, subject to the following conditions:
18706f2543Smrg#
19706f2543Smrg#  The above copyright notice and this permission notice shall be included in
20706f2543Smrg#  all copies or substantial portions of the Software.
21706f2543Smrg#
22706f2543Smrg#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23706f2543Smrg#  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24706f2543Smrg#  FIT- NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
25706f2543Smrg#  THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
26706f2543Smrg#  AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CON-
27706f2543Smrg#  NECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28706f2543Smrg
29706f2543Smrgimport sys, xml.dom.minidom
30706f2543Smrg
31706f2543Smrg# dict converting <match> tags to Match* entries
32706f2543Smrgmatch_table = {
33706f2543Smrg    'info.product': 'MatchProduct',
34706f2543Smrg    'input.product': 'MatchProduct',
35706f2543Smrg    'info.vendor': 'MatchVendor',
36706f2543Smrg    'input.vendor': 'MatchVendor',
37706f2543Smrg    'info.device': 'MatchDevicePath',
38706f2543Smrg    'linux.device_file': 'MatchDevicePath',
39706f2543Smrg    '/org/freedesktop/Hal/devices/computer:system.kernel.name': 'MatchOS',
40706f2543Smrg    '@info.parent:pnp.id': 'MatchPnPID',
41706f2543Smrg}
42706f2543Smrg
43706f2543Smrg# dict converting info.capabilities list to Match* entries
44706f2543Smrgcap_match_table = {
45706f2543Smrg    'input.keys': 'MatchIsKeyboard',
46706f2543Smrg    'input.keyboard': 'MatchIsKeyboard',
47706f2543Smrg    'input.keypad': 'MatchIsKeyboard',
48706f2543Smrg    'input.mouse': 'MatchIsPointer',
49706f2543Smrg    'input.joystick': 'MatchIsJoystick',
50706f2543Smrg    'input.tablet': 'MatchIsTablet',
51706f2543Smrg    'input.touchpad': 'MatchIsTouchpad',
52706f2543Smrg    'input.touchscreen': 'MatchIsTouchscreen',
53706f2543Smrg}
54706f2543Smrg
55706f2543Smrgdef device_glob(path):
56706f2543Smrg    '''Convert a contains device path to a glob entry'''
57706f2543Smrg    if path[0] != '/':
58706f2543Smrg        path = '*' + path
59706f2543Smrg    return path + '*'
60706f2543Smrg
61706f2543Smrgdef parse_match(node):
62706f2543Smrg    '''Parse a <match> tag to a tuple with InputClass values'''
63706f2543Smrg    match = None
64706f2543Smrg    value = None
65706f2543Smrg    booltype = False
66706f2543Smrg
67706f2543Smrg    # see what type of key we have
68706f2543Smrg    if node.attributes.has_key('key'):
69706f2543Smrg        key = node.attributes['key'].nodeValue
70706f2543Smrg        if key in match_table:
71706f2543Smrg            match = match_table[key]
72706f2543Smrg        elif key == 'info.capabilities':
73706f2543Smrg            booltype = True
74706f2543Smrg
75706f2543Smrg    # bail out now if it's unrecognized
76706f2543Smrg    if not match and not booltype:
77706f2543Smrg        return (match, value)
78706f2543Smrg
79706f2543Smrg    if node.attributes.has_key('string'):
80706f2543Smrg        value = node.attributes['string'].nodeValue
81706f2543Smrg    elif node.attributes.has_key('contains'):
82706f2543Smrg        value = node.attributes['contains'].nodeValue
83706f2543Smrg        if match == 'MatchDevicePath':
84706f2543Smrg            value = device_glob(value)
85706f2543Smrg        elif booltype and value in cap_match_table:
86706f2543Smrg            match = cap_match_table[value]
87706f2543Smrg            value = 'yes'
88706f2543Smrg    elif node.attributes.has_key('string_outof'):
89706f2543Smrg        value = node.attributes['string_outof'].nodeValue.replace(';','|')
90706f2543Smrg    elif node.attributes.has_key('contains_outof'):
91706f2543Smrg        all_values = node.attributes['contains_outof'].nodeValue.split(';')
92706f2543Smrg        for v in all_values:
93706f2543Smrg            if match == 'MatchDevicePath':
94706f2543Smrg                v = device_glob(v)
95706f2543Smrg            elif match == 'MatchPnPID' and len(v) < 7:
96706f2543Smrg                v += '*'
97706f2543Smrg            if value:
98706f2543Smrg                value += '|' + v
99706f2543Smrg            else:
100706f2543Smrg                value = v
101706f2543Smrg
102706f2543Smrg    return (match, value)
103706f2543Smrg
104706f2543Smrgdef parse_options(node):
105706f2543Smrg    '''Parse the x11_* options and return InputClass entries'''
106706f2543Smrg    driver = ''
107706f2543Smrg    ignore = False
108706f2543Smrg    options = []
109706f2543Smrg    for n in node.childNodes:
110706f2543Smrg        if n.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
111706f2543Smrg            continue
112706f2543Smrg
113706f2543Smrg        tag = n.tagName
114706f2543Smrg        key = n.attributes['key'].nodeValue
115706f2543Smrg        value = ''
116706f2543Smrg
117706f2543Smrg        if n.hasChildNodes():
118706f2543Smrg            content_node = n.childNodes[0]
119706f2543Smrg            assert content_node.nodeType == xml.dom.Node.TEXT_NODE
120706f2543Smrg            value = content_node.nodeValue
121706f2543Smrg
122706f2543Smrg        if tag == 'match':
123706f2543Smrg            continue
124706f2543Smrg        assert tag in ('addset', 'merge', 'append', 'remove')
125706f2543Smrg
126706f2543Smrg        if tag == 'remove' and key == 'input.x11_driver':
127706f2543Smrg            ignore = True
128706f2543Smrg        elif key == 'input.x11_driver':
129706f2543Smrg            driver = value
130706f2543Smrg        elif key.startswith('input.x11_options.'):
131706f2543Smrg            option = key.split('.', 2)[2]
132706f2543Smrg            options.append((option, value))
133706f2543Smrg
134706f2543Smrg    return (driver, ignore, options)
135706f2543Smrg
136706f2543Smrgdef is_match_node(node):
137706f2543Smrg    '''Check if a node is a <match> element'''
138706f2543Smrg    return node.nodeType == xml.dom.minidom.Node.ELEMENT_NODE and \
139706f2543Smrg        node.tagName == 'match'
140706f2543Smrg
141706f2543Smrgdef parse_all_matches(node):
142706f2543Smrg    '''Parse a x11 match tag and any parents that don't supply their
143706f2543Smrg    own options'''
144706f2543Smrg    matches = []
145706f2543Smrg
146706f2543Smrg    while True:
147706f2543Smrg        (key, value) = parse_match(node)
148706f2543Smrg        if key and value:
149706f2543Smrg            matches.append((key, value))
150706f2543Smrg
151706f2543Smrg        # walk up to a parent match node
152706f2543Smrg        node = node.parentNode
153706f2543Smrg        if node == None or not is_match_node(node):
154706f2543Smrg            break
155706f2543Smrg
156706f2543Smrg        # leave if there other options at this level
157706f2543Smrg        children = set([n.tagName for n in node.childNodes
158706f2543Smrg                        if n.nodeType == xml.dom.minidom.Node.ELEMENT_NODE])
159706f2543Smrg        if children & set(['addset', 'merge', 'append']):
160706f2543Smrg            break
161706f2543Smrg
162706f2543Smrg    return matches
163706f2543Smrg
164706f2543Smrg# stupid counter to give "unique" rule names
165706f2543Smrgnum_sections = 1
166706f2543Smrgdef print_section(matches, driver, ignore, options):
167706f2543Smrg    '''Print a valid InputClass section to stdout'''
168706f2543Smrg    global num_sections
169706f2543Smrg    print 'Section "InputClass"'
170706f2543Smrg    print '\tIdentifier "Converted Class %d"' % num_sections
171706f2543Smrg    num_sections += 1
172706f2543Smrg    for m, v in matches:
173706f2543Smrg        print '\t%s "%s"' % (m, v)
174706f2543Smrg    if driver:
175706f2543Smrg        print '\tDriver "%s"' % driver
176706f2543Smrg    if ignore:
177706f2543Smrg        print '\tOption "Ignore" "yes"'
178706f2543Smrg    for o, v in options:
179706f2543Smrg        print '\tOption "%s" "%s"' % (o, v)
180706f2543Smrg    print 'EndSection'
181706f2543Smrg
182706f2543Smrgdef parse_fdi(fdi):
183706f2543Smrg    '''Parse x11 matches from fdi'''
184706f2543Smrg    # find all <match> leaf nodes
185706f2543Smrg    num = 0
186706f2543Smrg    for match_node in fdi.getElementsByTagName('match'):
187706f2543Smrg        children = set([n.tagName for n in match_node.childNodes
188706f2543Smrg                if n.nodeType == xml.dom.minidom.Node.ELEMENT_NODE])
189706f2543Smrg
190706f2543Smrg        # see if there are any options at this level
191706f2543Smrg        (driver, ignore, options) = parse_options(match_node)
192706f2543Smrg        if not driver and not ignore and not options:
193706f2543Smrg            continue
194706f2543Smrg
195706f2543Smrg        matches = parse_all_matches(match_node)
196706f2543Smrg        if num > 0:
197706f2543Smrg            print
198706f2543Smrg        print_section(matches, driver, ignore, options)
199706f2543Smrg        num += 1
200706f2543Smrg
201706f2543Smrgfor f in sys.argv[1:]:
202706f2543Smrg    parse_fdi(xml.dom.minidom.parse(f))
203