Started edit globs box.
[mime-editor.git] / type.py
blob22a6c805b3532a62fe2a75ac478c3d26990dadcc
1 import os
2 import rox
3 from rox import g
4 from xml.parsers import expat
5 from xml.dom import XML_NAMESPACE
7 types = {}
9 home_mime = os.path.join(os.environ['HOME'], '.mime')
10 user_override = os.path.join(home_mime, 'packages', 'Override.xml')
12 FREE_NS='http://www.freedesktop.org/standards/shared-mime-info'
14 class Invalid(Exception):
15 pass
17 def get_type(name):
18 if name not in types:
19 types[name] = MIME_Type(name)
20 return types[name]
22 class MIME_Type:
23 def __init__(self, name):
24 assert name not in types
25 self.media, self.subtype = name.split('/')
27 self.comments = []
28 self.globs = []
29 self.magic = []
30 self.xml = []
31 self.others = []
33 def add_from_node(self, file):
34 self.sources.append(Source(file))
36 def add_comment(self, lang, comment):
37 self.comments.append((lang, comment))
39 def add_xml(self, uri, name):
40 self.xml.append((uri, name))
42 def add_magic(self, prio, root):
43 self.magic.append((prio, root))
45 def add_other(self, element):
46 self.others.append(element)
48 def add_glob(self, pattern):
49 self.globs.append(pattern)
51 def get_comment(self):
52 best = None
53 for lang, comment in self.comments:
54 if not lang:
55 return comment
56 best = comment
57 return best or self.get_name()
59 def get_name(self):
60 return self.media + '/' + self.subtype
62 def get_comments(self): return map(Comment, self.comments)
63 def get_globs(self): return map(Glob, self.globs)
64 def get_magic(self): return map(Magic, self.magic)
65 def get_xml(self): return map(XML, self.xml)
66 def get_others(self): return map(Other, self.others)
68 class Field:
69 "MIME_Type.get_* functions return a list of these."
70 def __init__(self, item):
71 self.item = item
73 def __str__(self):
74 return "<%s>" % self.item
76 def add_subtree(self, model, iter):
77 return
79 def __cmp__(self, b):
80 return cmp(str(self), str(b))
82 def delete(self):
83 rox.alert('TODO')
85 def edit(self):
86 if not hasattr(self, 'add_edit_widgets'):
87 rox.alert('Sorry, MIME-Editor does not support editing fields of this type')
88 return
89 box = rox.Dialog()
90 self.box = box
91 box.set_has_separator(False)
92 vbox = g.VBox(False, 4)
93 vbox.set_border_width(4)
94 box.vbox.pack_start(vbox, True, True, 0)
95 self.add_edit_widgets(vbox)
96 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
97 box.add_button(g.STOCK_OK, g.RESPONSE_OK)
98 box.set_default_response(g.RESPONSE_OK)
99 box.vbox.show_all()
100 while 1:
101 resp = box.run()
102 if resp == g.RESPONSE_OK:
103 try:
104 self.commit_edit(box)
105 except Invalid, e:
106 rox.alert(str(e))
107 continue
108 box.destroy()
109 break
111 def commit_edit(self, box):
112 raise Invalid('TODO: Not implemented')
114 class Comment(Field):
115 def __str__(self):
116 lang, data = self.item
117 if lang:
118 lang = '[' + lang + '] '
119 else:
120 lang = '(default) '
121 return lang + data
123 class Glob(Field):
124 def __str__(self):
125 return "Match '%s'" % self.item
127 def add_edit_widgets(self, vbox):
128 vbox.pack_start(
129 g.Label("Enter a glob pattern which matches files of this type.\n"
130 "Special characters are:\n"
131 "? - any one character\n"
132 "* - zero or more characters\n"
133 "[abc] - any character between the brackets\n"
134 "Example: '*.html' matches all files ending in '.html'"),
135 False, True, 0)
136 self.entry = g.Entry()
137 self.entry.set_text(self.item)
138 vbox.pack_start(self.entry, False, True, 0)
139 self.entry.set_activates_default(True)
141 def commit_edit(self, box):
142 new = self.entry.get_text()
143 if not new:
144 raise Invalid("Pattern can't be empty")
145 raise Invalid('TODO')
147 class Magic(Field):
148 def __str__(self):
149 prio, match = self.item
150 return "Match with priority %s" % prio
152 def add_subtree(self, model, parent):
153 def build(match, parent):
154 for m in match.matches:
155 text = '%s at %s = %s' % (m.type, m.offset, m.value)
156 if m.mask:
157 text += ' masked with ' + m.mask
158 iter = model.append(parent)
159 model.set(iter, 0, text, 1, m)
160 build(m, iter)
161 build(self.item[1], parent)
163 def __cmp__(self, b):
164 ret = cmp(str(self), str(b))
165 if ret: return ret
166 return cmp(self.item[1], b.item[1])
168 class XML(Field):
169 def __str__(self):
170 return "<%s> with namespace '%s'" % (self.item[1], self.item[0])
172 class Other(Field): pass
174 class FieldParser:
175 def __init__(self, type, attrs):
176 self.type = type
178 def start(self, element, attrs): pass
179 def data(self, data): pass
180 def end(self): pass
182 class CommentParser(FieldParser):
183 def __init__(self, type, attrs):
184 FieldParser.__init__(self, type, attrs)
185 self.lang = attrs.get(XML_NAMESPACE + ' lang', None)
186 self.comment = ''
188 def data(self, data):
189 self.comment += data
191 def end(self):
192 self.type.add_comment(self.lang, self.comment)
194 class Match:
195 def __init__(self, parent):
196 self.parent = parent
197 self.matches = []
199 def __cmp__(self, b):
200 def child_cmp():
201 for x, y in zip(self.matches, b.matches):
202 c = cmp(x, y)
203 if c: return c
204 return 0
206 if not self.parent:
207 return child_cmp()
209 return cmp(self.type, b.type) or cmp(self.offset, b.offset) or \
210 cmp(self.value, b.value) or cmp(self.mask, b.mask) or \
211 child_cmp()
213 def edit(self):
214 rox.alert("Editing of matches isn't currently supported")
216 class MagicParser(FieldParser):
217 def __init__(self, type, attrs):
218 FieldParser.__init__(self, type, attrs)
219 self.prio = attrs.get('priority', 50)
220 self.match = Match(None)
222 def start(self, element, attrs):
223 new = Match(self.match)
224 new.offset = attrs.get('offset', '?')
225 new.type = attrs.get('type', '?')
226 new.value = attrs.get('value', '?')
227 new.mask = attrs.get('mask', None)
228 self.match.matches.append(new)
230 def end(self):
231 if self.match.parent:
232 self.match = self.match.parent
233 else:
234 self.type.add_magic(self.prio, self.match)
236 class Scanner:
237 def __init__(self):
238 self.level = 0
239 self.type = None
240 self.handler = None
242 def parse(self, path):
243 parser = expat.ParserCreate(namespace_separator = ' ')
244 parser.StartElementHandler = self.start
245 parser.EndElementHandler = self.end
246 parser.CharacterDataHandler = self.data
247 parser.ParseFile(file(path))
249 def start(self, element, attrs):
250 self.level += 1
251 if self.level == 1:
252 assert element == FREE_NS + ' mime-info'
253 elif self.level == 2:
254 assert element == FREE_NS + ' mime-type'
255 self.type = get_type(attrs['type'])
256 elif self.level == 3:
257 if element == FREE_NS + ' comment':
258 self.handler = CommentParser(self.type, attrs)
259 elif element == FREE_NS + ' glob':
260 self.type.add_glob(attrs['pattern'])
261 elif element == FREE_NS + ' magic':
262 self.handler = MagicParser(self.type, attrs)
263 elif element == FREE_NS + ' root-XML':
264 self.type.add_xml(attrs['namespaceURI'], attrs['localName'])
265 else:
266 self.type.add_other(element)
267 else:
268 assert self.handler
269 self.handler.start(element, attrs)
271 def end(self, element):
272 if self.handler:
273 self.handler.end()
274 self.level -=1
275 if self.level == 1:
276 self.type = None
277 elif self.level == 2:
278 self.handler = None
280 def data(self, data):
281 if self.handler:
282 self.handler.data(data)
284 def scan_file(path):
285 if not path.endswith('.xml'): return
286 if not os.path.exists(path):
287 return
288 scanner = Scanner()
289 try:
290 scanner.parse(path)
291 except:
292 rox.report_exception()
294 def get_override():
295 from xml.dom import minidom
296 if os.path.exists(user_override):
297 doc = minidom.parse(user_override)
298 else:
299 doc = minidom.Document()
300 node = doc.createElementNS(FREE_NS, 'mime-info')
301 doc.appendChild(node)
302 node.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', FREE_NS)
303 return doc
305 def write_override(doc):
306 home_packages = os.path.join(home_mime, 'packages')
307 if not os.path.isdir(home_packages):
308 os.makedirs(home_packages)
309 path = os.path.join(home_packages, 'Override.xml.new')
310 doc.writexml(file(path, 'w'))
311 os.rename(path, path[:-4])
312 r, w = os.pipe()
313 child = os.fork()
314 if child == 0:
315 # Child
316 try:
317 os.close(r)
318 os.dup2(w, 1)
319 os.dup2(w, 2)
320 os.execlp('update-mime-database', 'update-mime-database', home_mime)
321 finally:
322 os._exit(1)
323 os.close(w)
324 rox.info(os.fdopen(r, 'r').read())
325 os.waitpid(child, 0)
327 import __main__
328 __main__.box.update()
330 def add_type(name):
331 doc = get_override()
333 root = doc.documentElement
334 type_node = doc.createElementNS(FREE_NS, 'mime-type')
335 root.appendChild(type_node)
337 type_node.setAttributeNS(None, 'type', name)
339 write_override(doc)
340 import __main__
341 __main__.box.show_type(name)
343 def delete_type(name):
344 from xml.dom import Node
345 doc = get_override()
346 removed = False
347 for c in doc.documentElement.childNodes:
348 if c.nodeType != Node.ELEMENT_NODE: continue
349 if c.nodeName != 'mime-type': continue
350 if c.namespaceURI != FREE_NS: continue
351 if c.getAttributeNS(None, 'type') != name: continue
352 doc.documentElement.removeChild(c)
353 removed = True
354 if not removed:
355 rox.alert("No user-provided information about this type -- can't remove anything")
356 return
357 if not rox.confirm("Really remove all user-specified information about type %s?" % name,
358 g.STOCK_DELETE):
359 return
360 write_override(doc)
362 def init():
363 "(Re)read the database."
364 global types
365 types = {}
366 for mime_dir in ['/usr/share/mime', '/usr/local/share/mime', home_mime]:
367 packages_dir = os.path.join(mime_dir, 'packages')
368 if not os.path.isdir(packages_dir):
369 continue
370 packages = os.listdir(packages_dir)
371 packages.sort()
372 for package in packages:
373 if package == 'Override.xml' and mime_dir is home_mime: continue
374 scan_file(os.path.join(packages_dir, package))
375 scan_file(user_override)