Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/xdg/Menu.py: 66%
791 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:37 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:37 +0000
1"""
2Implementation of the XDG Menu Specification
3http://standards.freedesktop.org/menu-spec/
5Example code:
7from xdg.Menu import parse, Menu, MenuEntry
9def print_menu(menu, tab=0):
10 for submenu in menu.Entries:
11 if isinstance(submenu, Menu):
12 print ("\t" * tab) + unicode(submenu)
13 print_menu(submenu, tab+1)
14 elif isinstance(submenu, MenuEntry):
15 print ("\t" * tab) + unicode(submenu.DesktopEntry)
17print_menu(parse())
18"""
20import os
21import locale
22import subprocess
23import ast
24import sys
25try:
26 import xml.etree.cElementTree as etree
27except ImportError:
28 import xml.etree.ElementTree as etree
30from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs
31from xdg.DesktopEntry import DesktopEntry
32from xdg.Exceptions import ParsingError
33from xdg.util import PY3
35import xdg.Locale
36import xdg.Config
39def _ast_const(name):
40 if sys.version_info >= (3, 4):
41 name = ast.literal_eval(name)
42 if sys.version_info >= (3, 8):
43 return ast.Constant(name)
44 else:
45 return ast.NameConstant(name)
46 else:
47 return ast.Name(id=name, ctx=ast.Load())
50def _strxfrm(s):
51 """Wrapper around locale.strxfrm that accepts unicode strings on Python 2.
53 See Python bug #2481.
54 """
55 if (not PY3) and isinstance(s, unicode):
56 s = s.encode('utf-8')
57 return locale.strxfrm(s)
60DELETED = "Deleted"
61NO_DISPLAY = "NoDisplay"
62HIDDEN = "Hidden"
63EMPTY = "Empty"
64NOT_SHOW_IN = "NotShowIn"
65NO_EXEC = "NoExec"
68class Menu:
69 """Menu containing sub menus under menu.Entries
71 Contains both Menu and MenuEntry items.
72 """
73 def __init__(self):
74 # Public stuff
75 self.Name = ""
76 self.Directory = None
77 self.Entries = []
78 self.Doc = ""
79 self.Filename = ""
80 self.Depth = 0
81 self.Parent = None
82 self.NotInXml = False
84 # Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN
85 self.Show = True
86 self.Visible = 0
88 # Private stuff, only needed for parsing
89 self.AppDirs = []
90 self.DefaultLayout = None
91 self.Deleted = None
92 self.Directories = []
93 self.DirectoryDirs = []
94 self.Layout = None
95 self.MenuEntries = []
96 self.Moves = []
97 self.OnlyUnallocated = None
98 self.Rules = []
99 self.Submenus = []
101 def __str__(self):
102 return self.Name
104 def __add__(self, other):
105 for dir in other.AppDirs:
106 self.AppDirs.append(dir)
108 for dir in other.DirectoryDirs:
109 self.DirectoryDirs.append(dir)
111 for directory in other.Directories:
112 self.Directories.append(directory)
114 if other.Deleted is not None:
115 self.Deleted = other.Deleted
117 if other.OnlyUnallocated is not None:
118 self.OnlyUnallocated = other.OnlyUnallocated
120 if other.Layout:
121 self.Layout = other.Layout
123 if other.DefaultLayout:
124 self.DefaultLayout = other.DefaultLayout
126 for rule in other.Rules:
127 self.Rules.append(rule)
129 for move in other.Moves:
130 self.Moves.append(move)
132 for submenu in other.Submenus:
133 self.addSubmenu(submenu)
135 return self
137 # FIXME: Performance: cache getName()
138 def __cmp__(self, other):
139 return locale.strcoll(self.getName(), other.getName())
141 def _key(self):
142 """Key function for locale-aware sorting."""
143 return _strxfrm(self.getName())
145 def __lt__(self, other):
146 try:
147 other = other._key()
148 except AttributeError:
149 pass
150 return self._key() < other
152 def __eq__(self, other):
153 try:
154 return self.Name == unicode(other)
155 except NameError: # unicode() becomes str() in Python 3
156 return self.Name == str(other)
158 """ PUBLIC STUFF """
159 def getEntries(self, show_hidden=False):
160 """Interator for a list of Entries visible to the user."""
161 for entry in self.Entries:
162 if show_hidden:
163 yield entry
164 elif entry.Show is True:
165 yield entry
167 # FIXME: Add searchEntry/seaqrchMenu function
168 # search for name/comment/genericname/desktopfileid
169 # return multiple items
171 def getMenuEntry(self, desktopfileid, deep=False):
172 """Searches for a MenuEntry with a given DesktopFileID."""
173 for menuentry in self.MenuEntries:
174 if menuentry.DesktopFileID == desktopfileid:
175 return menuentry
176 if deep:
177 for submenu in self.Submenus:
178 submenu.getMenuEntry(desktopfileid, deep)
180 def getMenu(self, path):
181 """Searches for a Menu with a given path."""
182 array = path.split("/", 1)
183 for submenu in self.Submenus:
184 if submenu.Name == array[0]:
185 if len(array) > 1:
186 return submenu.getMenu(array[1])
187 else:
188 return submenu
190 def getPath(self, org=False, toplevel=False):
191 """Returns this menu's path in the menu structure."""
192 parent = self
193 names = []
194 while 1:
195 if org:
196 names.append(parent.Name)
197 else:
198 names.append(parent.getName())
199 if parent.Depth > 0:
200 parent = parent.Parent
201 else:
202 break
203 names.reverse()
204 path = ""
205 if not toplevel:
206 names.pop(0)
207 for name in names:
208 path = os.path.join(path, name)
209 return path
211 def getName(self):
212 """Returns the menu's localised name."""
213 try:
214 return self.Directory.DesktopEntry.getName()
215 except AttributeError:
216 return self.Name
218 def getGenericName(self):
219 """Returns the menu's generic name."""
220 try:
221 return self.Directory.DesktopEntry.getGenericName()
222 except AttributeError:
223 return ""
225 def getComment(self):
226 """Returns the menu's comment text."""
227 try:
228 return self.Directory.DesktopEntry.getComment()
229 except AttributeError:
230 return ""
232 def getIcon(self):
233 """Returns the menu's icon, filename or simple name"""
234 try:
235 return self.Directory.DesktopEntry.getIcon()
236 except AttributeError:
237 return ""
239 def sort(self):
240 self.Entries = []
241 self.Visible = 0
243 for submenu in self.Submenus:
244 submenu.sort()
246 _submenus = set()
247 _entries = set()
249 for order in self.Layout.order:
250 if order[0] == "Filename":
251 _entries.add(order[1])
252 elif order[0] == "Menuname":
253 _submenus.add(order[1])
255 for order in self.Layout.order:
256 if order[0] == "Separator":
257 separator = Separator(self)
258 if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator):
259 separator.Show = False
260 self.Entries.append(separator)
261 elif order[0] == "Filename":
262 menuentry = self.getMenuEntry(order[1])
263 if menuentry:
264 self.Entries.append(menuentry)
265 elif order[0] == "Menuname":
266 submenu = self.getMenu(order[1])
267 if submenu:
268 if submenu.Layout.inline:
269 self.merge_inline(submenu)
270 else:
271 self.Entries.append(submenu)
272 elif order[0] == "Merge":
273 if order[1] == "files" or order[1] == "all":
274 self.MenuEntries.sort()
275 for menuentry in self.MenuEntries:
276 if menuentry.DesktopFileID not in _entries:
277 self.Entries.append(menuentry)
278 elif order[1] == "menus" or order[1] == "all":
279 self.Submenus.sort()
280 for submenu in self.Submenus:
281 if submenu.Name not in _submenus:
282 if submenu.Layout.inline:
283 self.merge_inline(submenu)
284 else:
285 self.Entries.append(submenu)
287 # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec
288 for entry in self.Entries:
289 entry.Show = True
290 self.Visible += 1
291 if isinstance(entry, Menu):
292 if entry.Deleted is True:
293 entry.Show = DELETED
294 self.Visible -= 1
295 elif isinstance(entry.Directory, MenuEntry):
296 if entry.Directory.DesktopEntry.getNoDisplay():
297 entry.Show = NO_DISPLAY
298 self.Visible -= 1
299 elif entry.Directory.DesktopEntry.getHidden():
300 entry.Show = HIDDEN
301 self.Visible -= 1
302 elif isinstance(entry, MenuEntry):
303 if entry.DesktopEntry.getNoDisplay():
304 entry.Show = NO_DISPLAY
305 self.Visible -= 1
306 elif entry.DesktopEntry.getHidden():
307 entry.Show = HIDDEN
308 self.Visible -= 1
309 elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec():
310 entry.Show = NO_EXEC
311 self.Visible -= 1
312 elif xdg.Config.windowmanager:
313 if (entry.DesktopEntry.getOnlyShowIn() != [] and (
314 xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn()
315 )
316 ) or (
317 xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn()
318 ):
319 entry.Show = NOT_SHOW_IN
320 self.Visible -= 1
321 elif isinstance(entry, Separator):
322 self.Visible -= 1
323 # remove separators at the beginning and at the end
324 if len(self.Entries) > 0:
325 if isinstance(self.Entries[0], Separator):
326 self.Entries[0].Show = False
327 if len(self.Entries) > 1:
328 if isinstance(self.Entries[-1], Separator):
329 self.Entries[-1].Show = False
331 # show_empty tag
332 for entry in self.Entries[:]:
333 if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0:
334 entry.Show = EMPTY
335 self.Visible -= 1
336 if entry.NotInXml is True:
337 self.Entries.remove(entry)
339 """ PRIVATE STUFF """
340 def addSubmenu(self, newmenu):
341 for submenu in self.Submenus:
342 if submenu == newmenu:
343 submenu += newmenu
344 break
345 else:
346 self.Submenus.append(newmenu)
347 newmenu.Parent = self
348 newmenu.Depth = self.Depth + 1
350 # inline tags
351 def merge_inline(self, submenu):
352 """Appends a submenu's entries to this menu
353 See the <Menuname> section of the spec about the "inline" attribute
354 """
355 if len(submenu.Entries) == 1 and submenu.Layout.inline_alias:
356 menuentry = submenu.Entries[0]
357 menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True)
358 menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True)
359 menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True)
360 self.Entries.append(menuentry)
361 elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0:
362 if submenu.Layout.inline_header:
363 header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment())
364 self.Entries.append(header)
365 for entry in submenu.Entries:
366 self.Entries.append(entry)
367 else:
368 self.Entries.append(submenu)
371class Move:
372 "A move operation"
373 def __init__(self, old="", new=""):
374 self.Old = old
375 self.New = new
377 def __cmp__(self, other):
378 return cmp(self.Old, other.Old)
381class Layout:
382 "Menu Layout class"
383 def __init__(self, show_empty=False, inline=False, inline_limit=4,
384 inline_header=True, inline_alias=False):
385 self.show_empty = show_empty
386 self.inline = inline
387 self.inline_limit = inline_limit
388 self.inline_header = inline_header
389 self.inline_alias = inline_alias
390 self._order = []
391 self._default_order = [
392 ['Merge', 'menus'],
393 ['Merge', 'files']
394 ]
396 @property
397 def order(self):
398 return self._order if self._order else self._default_order
400 @order.setter
401 def order(self, order):
402 self._order = order
405class Rule:
406 """Include / Exclude Rules Class"""
408 TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1
410 @classmethod
411 def fromFilename(cls, type, filename):
412 tree = ast.Expression(
413 body=ast.Compare(
414 left=ast.Str(filename),
415 ops=[ast.Eq()],
416 comparators=[ast.Attribute(
417 value=ast.Name(id='menuentry', ctx=ast.Load()),
418 attr='DesktopFileID',
419 ctx=ast.Load()
420 )]
421 ),
422 lineno=1, col_offset=0
423 )
424 ast.fix_missing_locations(tree)
425 rule = Rule(type, tree)
426 return rule
428 def __init__(self, type, expression):
429 # Type is TYPE_INCLUDE or TYPE_EXCLUDE
430 self.Type = type
431 # expression is ast.Expression
432 self.expression = expression
433 self.code = compile(self.expression, '<compiled-menu-rule>', 'eval')
435 def __str__(self):
436 return ast.dump(self.expression)
438 def apply(self, menuentries, run):
439 for menuentry in menuentries:
440 if run == 2 and (menuentry.MatchedInclude is True or
441 menuentry.Allocated is True):
442 continue
443 if eval(self.code):
444 if self.Type is Rule.TYPE_INCLUDE:
445 menuentry.Add = True
446 menuentry.MatchedInclude = True
447 else:
448 menuentry.Add = False
449 return menuentries
452class MenuEntry:
453 "Wrapper for 'Menu Style' Desktop Entries"
455 TYPE_USER = "User"
456 TYPE_SYSTEM = "System"
457 TYPE_BOTH = "Both"
459 def __init__(self, filename, dir="", prefix=""):
460 # Create entry
461 self.DesktopEntry = DesktopEntry(os.path.join(dir, filename))
462 self.setAttributes(filename, dir, prefix)
464 # Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC
465 self.Show = True
467 # Semi-Private
468 self.Original = None
469 self.Parents = []
471 # Private Stuff
472 self.Allocated = False
473 self.Add = False
474 self.MatchedInclude = False
476 # Caching
477 self.Categories = self.DesktopEntry.getCategories()
479 def save(self):
480 """Save any changes to the desktop entry."""
481 if self.DesktopEntry.tainted:
482 self.DesktopEntry.write()
484 def getDir(self):
485 """Return the directory containing the desktop entry file."""
486 return self.DesktopEntry.filename.replace(self.Filename, '')
488 def getType(self):
489 """Return the type of MenuEntry, System/User/Both"""
490 if not xdg.Config.root_mode:
491 if self.Original:
492 return self.TYPE_BOTH
493 elif xdg_data_dirs[0] in self.DesktopEntry.filename:
494 return self.TYPE_USER
495 else:
496 return self.TYPE_SYSTEM
497 else:
498 return self.TYPE_USER
500 def setAttributes(self, filename, dir="", prefix=""):
501 self.Filename = filename
502 self.Prefix = prefix
503 self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-")
505 if not os.path.isabs(self.DesktopEntry.filename):
506 self.__setFilename()
508 def updateAttributes(self):
509 if self.getType() == self.TYPE_SYSTEM:
510 self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix)
511 self.__setFilename()
513 def __setFilename(self):
514 if not xdg.Config.root_mode:
515 path = xdg_data_dirs[0]
516 else:
517 path = xdg_data_dirs[1]
519 if self.DesktopEntry.getType() == "Application":
520 dir_ = os.path.join(path, "applications")
521 else:
522 dir_ = os.path.join(path, "desktop-directories")
524 self.DesktopEntry.filename = os.path.join(dir_, self.Filename)
526 def __cmp__(self, other):
527 return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName())
529 def _key(self):
530 """Key function for locale-aware sorting."""
531 return _strxfrm(self.DesktopEntry.getName())
533 def __lt__(self, other):
534 try:
535 other = other._key()
536 except AttributeError:
537 pass
538 return self._key() < other
540 def __eq__(self, other):
541 if self.DesktopFileID == str(other):
542 return True
543 else:
544 return False
546 def __repr__(self):
547 return self.DesktopFileID
550class Separator:
551 "Just a dummy class for Separators"
552 def __init__(self, parent):
553 self.Parent = parent
554 self.Show = True
557class Header:
558 "Class for Inline Headers"
559 def __init__(self, name, generic_name, comment):
560 self.Name = name
561 self.GenericName = generic_name
562 self.Comment = comment
564 def __str__(self):
565 return self.Name
568TYPE_DIR, TYPE_FILE = 0, 1
571def _check_file_path(value, filename, type):
572 path = os.path.dirname(filename)
573 if not os.path.isabs(value):
574 value = os.path.join(path, value)
575 value = os.path.abspath(value)
576 if not os.path.exists(value):
577 return False
578 if type == TYPE_DIR and os.path.isdir(value):
579 return value
580 if type == TYPE_FILE and os.path.isfile(value):
581 return value
582 return False
585def _get_menu_file_path(filename):
586 dirs = list(xdg_config_dirs)
587 if xdg.Config.root_mode is True:
588 dirs.pop(0)
589 for d in dirs:
590 menuname = os.path.join(d, "menus", filename)
591 if os.path.isfile(menuname):
592 return menuname
595def _to_bool(value):
596 if isinstance(value, bool):
597 return value
598 return value.lower() == "true"
601# remove duplicate entries from a list
602def _dedupe(_list):
603 _set = {}
604 _list.reverse()
605 _list = [_set.setdefault(e, e) for e in _list if e not in _set]
606 _list.reverse()
607 return _list
610class XMLMenuBuilder(object):
612 def __init__(self, debug=False):
613 self.debug = debug
615 def parse(self, filename=None):
616 """Load an applications.menu file.
618 filename : str, optional
619 The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``.
620 """
621 # convert to absolute path
622 if filename and not os.path.isabs(filename):
623 filename = _get_menu_file_path(filename)
624 # use default if no filename given
625 if not filename:
626 candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu"
627 filename = _get_menu_file_path(candidate)
628 if not filename:
629 raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate)
630 # check if it is a .menu file
631 if not filename.endswith(".menu"):
632 raise ParsingError('Not a .menu file', filename)
633 # create xml parser
634 try:
635 tree = etree.parse(filename)
636 except:
637 raise ParsingError('Not a valid .menu file', filename)
639 # parse menufile
640 self._merged_files = set()
641 self._directory_dirs = set()
642 self.cache = MenuEntryCache()
644 menu = self.parse_menu(tree.getroot(), filename)
645 menu.tree = tree
646 menu.filename = filename
648 self.handle_moves(menu)
649 self.post_parse(menu)
651 # generate the menu
652 self.generate_not_only_allocated(menu)
653 self.generate_only_allocated(menu)
655 # and finally sort
656 menu.sort()
658 return menu
660 def parse_menu(self, node, filename):
661 menu = Menu()
662 self.parse_node(node, filename, menu)
663 return menu
665 def parse_node(self, node, filename, parent=None):
666 num_children = len(node)
667 for child in node:
668 tag, text = child.tag, child.text
669 text = text.strip() if text else None
670 if tag == 'Menu':
671 menu = self.parse_menu(child, filename)
672 parent.addSubmenu(menu)
673 elif tag == 'AppDir' and text:
674 self.parse_app_dir(text, filename, parent)
675 elif tag == 'DefaultAppDirs':
676 self.parse_default_app_dir(filename, parent)
677 elif tag == 'DirectoryDir' and text:
678 self.parse_directory_dir(text, filename, parent)
679 elif tag == 'DefaultDirectoryDirs':
680 self.parse_default_directory_dir(filename, parent)
681 elif tag == 'Name' and text:
682 parent.Name = text
683 elif tag == 'Directory' and text:
684 parent.Directories.append(text)
685 elif tag == 'OnlyUnallocated':
686 parent.OnlyUnallocated = True
687 elif tag == 'NotOnlyUnallocated':
688 parent.OnlyUnallocated = False
689 elif tag == 'Deleted':
690 parent.Deleted = True
691 elif tag == 'NotDeleted':
692 parent.Deleted = False
693 elif tag == 'Include' or tag == 'Exclude':
694 parent.Rules.append(self.parse_rule(child))
695 elif tag == 'MergeFile':
696 if child.attrib.get("type", None) == "parent":
697 self.parse_merge_file("applications.menu", child, filename, parent)
698 elif text:
699 self.parse_merge_file(text, child, filename, parent)
700 elif tag == 'MergeDir' and text:
701 self.parse_merge_dir(text, child, filename, parent)
702 elif tag == 'DefaultMergeDirs':
703 self.parse_default_merge_dirs(child, filename, parent)
704 elif tag == 'Move':
705 parent.Moves.append(self.parse_move(child))
706 elif tag == 'Layout':
707 if num_children > 1:
708 parent.Layout = self.parse_layout(child)
709 elif tag == 'DefaultLayout':
710 if num_children > 1:
711 parent.DefaultLayout = self.parse_layout(child)
712 elif tag == 'LegacyDir' and text:
713 self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent)
714 elif tag == 'KDELegacyDirs':
715 self.parse_kde_legacy_dirs(filename, parent)
717 def parse_layout(self, node):
718 layout = Layout(
719 show_empty=_to_bool(node.attrib.get("show_empty", False)),
720 inline=_to_bool(node.attrib.get("inline", False)),
721 inline_limit=int(node.attrib.get("inline_limit", 4)),
722 inline_header=_to_bool(node.attrib.get("inline_header", True)),
723 inline_alias=_to_bool(node.attrib.get("inline_alias", False))
724 )
725 order = []
726 for child in node:
727 tag, text = child.tag, child.text
728 text = text.strip() if text else None
729 if tag == "Menuname" and text:
730 order.append([
731 "Menuname",
732 text,
733 _to_bool(child.attrib.get("show_empty", False)),
734 _to_bool(child.attrib.get("inline", False)),
735 int(child.attrib.get("inline_limit", 4)),
736 _to_bool(child.attrib.get("inline_header", True)),
737 _to_bool(child.attrib.get("inline_alias", False))
738 ])
739 elif tag == "Separator":
740 order.append(['Separator'])
741 elif tag == "Filename" and text:
742 order.append(["Filename", text])
743 elif tag == "Merge":
744 order.append([
745 "Merge",
746 child.attrib.get("type", "all")
747 ])
748 layout.order = order
749 return layout
751 def parse_move(self, node):
752 old, new = "", ""
753 for child in node:
754 tag, text = child.tag, child.text
755 text = text.strip() if text else None
756 if tag == "Old" and text:
757 old = text
758 elif tag == "New" and text:
759 new = text
760 return Move(old, new)
762 # ---------- <Rule> parsing
764 def parse_rule(self, node):
765 type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE
766 tree = ast.Expression(lineno=1, col_offset=0)
767 expr = self.parse_bool_op(node, ast.Or())
768 if expr:
769 tree.body = expr
770 else:
771 tree.body = _ast_const('False')
772 ast.fix_missing_locations(tree)
773 return Rule(type, tree)
775 def parse_bool_op(self, node, operator):
776 values = []
777 for child in node:
778 rule = self.parse_rule_node(child)
779 if rule:
780 values.append(rule)
781 num_values = len(values)
782 if num_values > 1:
783 return ast.BoolOp(operator, values)
784 elif num_values == 1:
785 return values[0]
786 return None
788 def parse_rule_node(self, node):
789 tag = node.tag
790 if tag == 'Or':
791 return self.parse_bool_op(node, ast.Or())
792 elif tag == 'And':
793 return self.parse_bool_op(node, ast.And())
794 elif tag == 'Not':
795 expr = self.parse_bool_op(node, ast.Or())
796 return ast.UnaryOp(ast.Not(), expr) if expr else None
797 elif tag == 'All':
798 return _ast_const('True')
799 elif tag == 'Category':
800 category = node.text
801 return ast.Compare(
802 left=ast.Str(category),
803 ops=[ast.In()],
804 comparators=[ast.Attribute(
805 value=ast.Name(id='menuentry', ctx=ast.Load()),
806 attr='Categories',
807 ctx=ast.Load()
808 )]
809 )
810 elif tag == 'Filename':
811 filename = node.text
812 return ast.Compare(
813 left=ast.Str(filename),
814 ops=[ast.Eq()],
815 comparators=[ast.Attribute(
816 value=ast.Name(id='menuentry', ctx=ast.Load()),
817 attr='DesktopFileID',
818 ctx=ast.Load()
819 )]
820 )
822 # ---------- App/Directory Dir Stuff
824 def parse_app_dir(self, value, filename, parent):
825 value = _check_file_path(value, filename, TYPE_DIR)
826 if value:
827 parent.AppDirs.append(value)
829 def parse_default_app_dir(self, filename, parent):
830 for d in reversed(xdg_data_dirs):
831 self.parse_app_dir(os.path.join(d, "applications"), filename, parent)
833 def parse_directory_dir(self, value, filename, parent):
834 value = _check_file_path(value, filename, TYPE_DIR)
835 if value:
836 parent.DirectoryDirs.append(value)
838 def parse_default_directory_dir(self, filename, parent):
839 for d in reversed(xdg_data_dirs):
840 self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent)
842 # ---------- Merge Stuff
844 def parse_merge_file(self, value, child, filename, parent):
845 if child.attrib.get("type", None) == "parent":
846 for d in xdg_config_dirs:
847 rel_file = filename.replace(d, "").strip("/")
848 if rel_file != filename:
849 for p in xdg_config_dirs:
850 if d == p:
851 continue
852 if os.path.isfile(os.path.join(p, rel_file)):
853 self.merge_file(os.path.join(p, rel_file), child, parent)
854 break
855 else:
856 value = _check_file_path(value, filename, TYPE_FILE)
857 if value:
858 self.merge_file(value, child, parent)
860 def parse_merge_dir(self, value, child, filename, parent):
861 value = _check_file_path(value, filename, TYPE_DIR)
862 if value:
863 for item in os.listdir(value):
864 try:
865 if item.endswith(".menu"):
866 self.merge_file(os.path.join(value, item), child, parent)
867 except UnicodeDecodeError:
868 continue
870 def parse_default_merge_dirs(self, child, filename, parent):
871 basename = os.path.splitext(os.path.basename(filename))[0]
872 for d in reversed(xdg_config_dirs):
873 self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent)
875 def merge_file(self, filename, child, parent):
876 # check for infinite loops
877 if filename in self._merged_files:
878 if self.debug:
879 raise ParsingError('Infinite MergeFile loop detected', filename)
880 else:
881 return
882 self._merged_files.add(filename)
883 # load file
884 try:
885 tree = etree.parse(filename)
886 except IOError:
887 if self.debug:
888 raise ParsingError('File not found', filename)
889 else:
890 return
891 except:
892 if self.debug:
893 raise ParsingError('Not a valid .menu file', filename)
894 else:
895 return
896 root = tree.getroot()
897 self.parse_node(root, filename, parent)
899 # ---------- Legacy Dir Stuff
901 def parse_legacy_dir(self, dir_, prefix, filename, parent):
902 m = self.merge_legacy_dir(dir_, prefix, filename, parent)
903 if m:
904 parent += m
906 def merge_legacy_dir(self, dir_, prefix, filename, parent):
907 dir_ = _check_file_path(dir_, filename, TYPE_DIR)
908 if dir_ and dir_ not in self._directory_dirs:
909 self._directory_dirs.add(dir_)
910 m = Menu()
911 m.AppDirs.append(dir_)
912 m.DirectoryDirs.append(dir_)
913 m.Name = os.path.basename(dir_)
914 m.NotInXml = True
916 for item in os.listdir(dir_):
917 try:
918 if item == ".directory":
919 m.Directories.append(item)
920 elif os.path.isdir(os.path.join(dir_, item)):
921 m.addSubmenu(self.merge_legacy_dir(
922 os.path.join(dir_, item),
923 prefix,
924 filename,
925 parent
926 ))
927 except UnicodeDecodeError:
928 continue
930 self.cache.add_menu_entries([dir_], prefix, True)
931 menuentries = self.cache.get_menu_entries([dir_], False)
933 for menuentry in menuentries:
934 categories = menuentry.Categories
935 if len(categories) == 0:
936 r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID)
937 m.Rules.append(r)
938 if not dir_ in parent.AppDirs:
939 categories.append("Legacy")
940 menuentry.Categories = categories
942 return m
944 def parse_kde_legacy_dirs(self, filename, parent):
945 try:
946 proc = subprocess.Popen(
947 ['kde-config', '--path', 'apps'],
948 stdout=subprocess.PIPE,
949 universal_newlines=True
950 )
951 output = proc.communicate()[0].splitlines()
952 except OSError:
953 # If kde-config doesn't exist, ignore this.
954 return
955 try:
956 for dir_ in output[0].split(":"):
957 self.parse_legacy_dir(dir_, "kde", filename, parent)
958 except IndexError:
959 pass
961 def post_parse(self, menu):
962 # unallocated / deleted
963 if menu.Deleted is None:
964 menu.Deleted = False
965 if menu.OnlyUnallocated is None:
966 menu.OnlyUnallocated = False
968 # Layout Tags
969 if not menu.Layout or not menu.DefaultLayout:
970 if menu.DefaultLayout:
971 menu.Layout = menu.DefaultLayout
972 elif menu.Layout:
973 if menu.Depth > 0:
974 menu.DefaultLayout = menu.Parent.DefaultLayout
975 else:
976 menu.DefaultLayout = Layout()
977 else:
978 if menu.Depth > 0:
979 menu.Layout = menu.Parent.DefaultLayout
980 menu.DefaultLayout = menu.Parent.DefaultLayout
981 else:
982 menu.Layout = Layout()
983 menu.DefaultLayout = Layout()
985 # add parent's app/directory dirs
986 if menu.Depth > 0:
987 menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs
988 menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs
990 # remove duplicates
991 menu.Directories = _dedupe(menu.Directories)
992 menu.DirectoryDirs = _dedupe(menu.DirectoryDirs)
993 menu.AppDirs = _dedupe(menu.AppDirs)
995 # go recursive through all menus
996 for submenu in menu.Submenus:
997 self.post_parse(submenu)
999 # reverse so handling is easier
1000 menu.Directories.reverse()
1001 menu.DirectoryDirs.reverse()
1002 menu.AppDirs.reverse()
1004 # get the valid .directory file out of the list
1005 for directory in menu.Directories:
1006 for dir in menu.DirectoryDirs:
1007 if os.path.isfile(os.path.join(dir, directory)):
1008 menuentry = MenuEntry(directory, dir)
1009 if not menu.Directory:
1010 menu.Directory = menuentry
1011 elif menuentry.getType() == MenuEntry.TYPE_SYSTEM:
1012 if menu.Directory.getType() == MenuEntry.TYPE_USER:
1013 menu.Directory.Original = menuentry
1014 if menu.Directory:
1015 break
1017 # Finally generate the menu
1018 def generate_not_only_allocated(self, menu):
1019 for submenu in menu.Submenus:
1020 self.generate_not_only_allocated(submenu)
1022 if menu.OnlyUnallocated is False:
1023 self.cache.add_menu_entries(menu.AppDirs)
1024 menuentries = []
1025 for rule in menu.Rules:
1026 menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1)
1028 for menuentry in menuentries:
1029 if menuentry.Add is True:
1030 menuentry.Parents.append(menu)
1031 menuentry.Add = False
1032 menuentry.Allocated = True
1033 menu.MenuEntries.append(menuentry)
1035 def generate_only_allocated(self, menu):
1036 for submenu in menu.Submenus:
1037 self.generate_only_allocated(submenu)
1039 if menu.OnlyUnallocated is True:
1040 self.cache.add_menu_entries(menu.AppDirs)
1041 menuentries = []
1042 for rule in menu.Rules:
1043 menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2)
1044 for menuentry in menuentries:
1045 if menuentry.Add is True:
1046 menuentry.Parents.append(menu)
1047 # menuentry.Add = False
1048 # menuentry.Allocated = True
1049 menu.MenuEntries.append(menuentry)
1051 def handle_moves(self, menu):
1052 for submenu in menu.Submenus:
1053 self.handle_moves(submenu)
1054 # parse move operations
1055 for move in menu.Moves:
1056 move_from_menu = menu.getMenu(move.Old)
1057 if move_from_menu:
1058 # FIXME: this is assigned, but never used...
1059 move_to_menu = menu.getMenu(move.New)
1061 menus = move.New.split("/")
1062 oldparent = None
1063 while len(menus) > 0:
1064 if not oldparent:
1065 oldparent = menu
1066 newmenu = oldparent.getMenu(menus[0])
1067 if not newmenu:
1068 newmenu = Menu()
1069 newmenu.Name = menus[0]
1070 if len(menus) > 1:
1071 newmenu.NotInXml = True
1072 oldparent.addSubmenu(newmenu)
1073 oldparent = newmenu
1074 menus.pop(0)
1076 newmenu += move_from_menu
1077 move_from_menu.Parent.Submenus.remove(move_from_menu)
1080class MenuEntryCache:
1081 "Class to cache Desktop Entries"
1082 def __init__(self):
1083 self.cacheEntries = {}
1084 self.cacheEntries['legacy'] = []
1085 self.cache = {}
1087 def add_menu_entries(self, dirs, prefix="", legacy=False):
1088 for dir_ in dirs:
1089 if not dir_ in self.cacheEntries:
1090 self.cacheEntries[dir_] = []
1091 self.__addFiles(dir_, "", prefix, legacy)
1093 def __addFiles(self, dir_, subdir, prefix, legacy):
1094 for item in os.listdir(os.path.join(dir_, subdir)):
1095 if item.endswith(".desktop"):
1096 try:
1097 menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix)
1098 except ParsingError:
1099 continue
1101 self.cacheEntries[dir_].append(menuentry)
1102 if legacy:
1103 self.cacheEntries['legacy'].append(menuentry)
1104 elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy:
1105 self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy)
1107 def get_menu_entries(self, dirs, legacy=True):
1108 entries = []
1109 ids = set()
1110 # handle legacy items
1111 appdirs = dirs[:]
1112 if legacy:
1113 appdirs.append("legacy")
1114 # cache the results again
1115 key = "".join(appdirs)
1116 try:
1117 return self.cache[key]
1118 except KeyError:
1119 pass
1120 for dir_ in appdirs:
1121 for menuentry in self.cacheEntries[dir_]:
1122 try:
1123 if menuentry.DesktopFileID not in ids:
1124 ids.add(menuentry.DesktopFileID)
1125 entries.append(menuentry)
1126 elif menuentry.getType() == MenuEntry.TYPE_SYSTEM:
1127 # FIXME: This is only 99% correct, but still...
1128 idx = entries.index(menuentry)
1129 entry = entries[idx]
1130 if entry.getType() == MenuEntry.TYPE_USER:
1131 entry.Original = menuentry
1132 except UnicodeDecodeError:
1133 continue
1134 self.cache[key] = entries
1135 return entries
1138def parse(filename=None, debug=False):
1139 """Helper function.
1140 Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename)
1141 """
1142 return XMLMenuBuilder(debug).parse(filename)