From dcaeeeda485971f3c4a9ea7ed2eb8f44b1697f36 Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Sat, 24 Apr 2021 21:06:18 -0500 Subject: [PATCH] Removed other xdg --- .../icons/mixins/xdg_0.27/BaseDirectory.py | 160 --- .../view/icons/mixins/xdg_0.27/Config.py | 39 - .../icons/mixins/xdg_0.27/DesktopEntry.py | 433 ------- .../view/icons/mixins/xdg_0.27/Exceptions.py | 84 -- .../view/icons/mixins/xdg_0.27/IconTheme.py | 445 ------- .../view/icons/mixins/xdg_0.27/IniFile.py | 417 ------ .../view/icons/mixins/xdg_0.27/Locale.py | 79 -- .../view/icons/mixins/xdg_0.27/Menu.py | 1142 ----------------- .../view/icons/mixins/xdg_0.27/MenuEditor.py | 541 -------- .../view/icons/mixins/xdg_0.27/Mime.py | 784 ----------- .../view/icons/mixins/xdg_0.27/RecentFiles.py | 180 --- .../view/icons/mixins/xdg_0.27/__init__.py | 3 - .../view/icons/mixins/xdg_0.27/util.py | 75 -- 13 files changed, 4382 deletions(-) delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/BaseDirectory.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/Config.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/DesktopEntry.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/Exceptions.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/IconTheme.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/IniFile.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/Locale.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/Menu.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/MenuEditor.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/Mime.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/RecentFiles.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/__init__.py delete mode 100644 src/shellfm/windows/view/icons/mixins/xdg_0.27/util.py diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/BaseDirectory.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/BaseDirectory.py deleted file mode 100644 index a7c31b1..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/BaseDirectory.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -This module is based on a rox module (LGPL): - -http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log - -The freedesktop.org Base Directory specification provides a way for -applications to locate shared data and configuration: - - http://standards.freedesktop.org/basedir-spec/ - -(based on version 0.6) - -This module can be used to load and save from and to these directories. - -Typical usage: - - from rox import basedir - - for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'): - print "Load settings from", dir - - dir = basedir.save_config_path('mydomain.org', 'MyProg') - print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2" - -Note: see the rox.Options module for a higher-level API for managing options. -""" - -import os, stat - -_home = os.path.expanduser('~') -xdg_data_home = os.environ.get('XDG_DATA_HOME') or \ - os.path.join(_home, '.local', 'share') - -xdg_data_dirs = [xdg_data_home] + \ - (os.environ.get('XDG_DATA_DIRS') or '/usr/local/share:/usr/share').split(':') - -xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \ - os.path.join(_home, '.config') - -xdg_config_dirs = [xdg_config_home] + \ - (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':') - -xdg_cache_home = os.environ.get('XDG_CACHE_HOME') or \ - os.path.join(_home, '.cache') - -xdg_data_dirs = [x for x in xdg_data_dirs if x] -xdg_config_dirs = [x for x in xdg_config_dirs if x] - -def save_config_path(*resource): - """Ensure ``$XDG_CONFIG_HOME//`` exists, and return its path. - 'resource' should normally be the name of your application. Use this - when saving configuration settings. - """ - resource = os.path.join(*resource) - assert not resource.startswith('/') - path = os.path.join(xdg_config_home, resource) - if not os.path.isdir(path): - os.makedirs(path, 0o700) - return path - -def save_data_path(*resource): - """Ensure ``$XDG_DATA_HOME//`` exists, and return its path. - 'resource' should normally be the name of your application or a shared - resource. Use this when saving or updating application data. - """ - resource = os.path.join(*resource) - assert not resource.startswith('/') - path = os.path.join(xdg_data_home, resource) - if not os.path.isdir(path): - os.makedirs(path) - return path - -def save_cache_path(*resource): - """Ensure ``$XDG_CACHE_HOME//`` exists, and return its path. - 'resource' should normally be the name of your application or a shared - resource.""" - resource = os.path.join(*resource) - assert not resource.startswith('/') - path = os.path.join(xdg_cache_home, resource) - if not os.path.isdir(path): - os.makedirs(path) - return path - -def load_config_paths(*resource): - """Returns an iterator which gives each directory named 'resource' in the - configuration search path. Information provided by earlier directories should - take precedence over later ones, and the user-specific config dir comes - first.""" - resource = os.path.join(*resource) - for config_dir in xdg_config_dirs: - path = os.path.join(config_dir, resource) - if os.path.exists(path): yield path - -def load_first_config(*resource): - """Returns the first result from load_config_paths, or None if there is nothing - to load.""" - for x in load_config_paths(*resource): - return x - return None - -def load_data_paths(*resource): - """Returns an iterator which gives each directory named 'resource' in the - application data search path. Information provided by earlier directories - should take precedence over later ones.""" - resource = os.path.join(*resource) - for data_dir in xdg_data_dirs: - path = os.path.join(data_dir, resource) - if os.path.exists(path): yield path - -def get_runtime_dir(strict=True): - """Returns the value of $XDG_RUNTIME_DIR, a directory path. - - This directory is intended for 'user-specific non-essential runtime files - and other file objects (such as sockets, named pipes, ...)', and - 'communication and synchronization purposes'. - - As of late 2012, only quite new systems set $XDG_RUNTIME_DIR. If it is not - set, with ``strict=True`` (the default), a KeyError is raised. With - ``strict=False``, PyXDG will create a fallback under /tmp for the current - user. This fallback does *not* provide the same guarantees as the - specification requires for the runtime directory. - - The strict default is deliberately conservative, so that application - developers can make a conscious decision to allow the fallback. - """ - try: - return os.environ['XDG_RUNTIME_DIR'] - except KeyError: - if strict: - raise - - import getpass - fallback = '/tmp/pyxdg-runtime-dir-fallback-' + getpass.getuser() - create = False - - try: - # This must be a real directory, not a symlink, so attackers can't - # point it elsewhere. So we use lstat to check it. - st = os.lstat(fallback) - except OSError as e: - import errno - if e.errno == errno.ENOENT: - create = True - else: - raise - else: - # The fallback must be a directory - if not stat.S_ISDIR(st.st_mode): - os.unlink(fallback) - create = True - # Must be owned by the user and not accessible by anyone else - elif (st.st_uid != os.getuid()) \ - or (st.st_mode & (stat.S_IRWXG | stat.S_IRWXO)): - os.rmdir(fallback) - create = True - - if create: - os.mkdir(fallback, 0o700) - - return fallback diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Config.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/Config.py deleted file mode 100644 index 3f5d654..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Config.py +++ /dev/null @@ -1,39 +0,0 @@ -""" -Functions to configure Basic Settings -""" - -language = "C" -windowmanager = None -icon_theme = "hicolor" -icon_size = 48 -cache_time = 5 -root_mode = False - -def setWindowManager(wm): - global windowmanager - windowmanager = wm - -def setIconTheme(theme): - global icon_theme - icon_theme = theme - import xdg.IconTheme - xdg.IconTheme.themes = [] - -def setIconSize(size): - global icon_size - icon_size = size - -def setCacheTime(time): - global cache_time - cache_time = time - -def setLocale(lang): - import locale - lang = locale.normalize(lang) - locale.setlocale(locale.LC_ALL, lang) - import xdg.Locale - xdg.Locale.update(lang) - -def setRootMode(boolean): - global root_mode - root_mode = boolean diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/DesktopEntry.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/DesktopEntry.py deleted file mode 100644 index 84e6dd9..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/DesktopEntry.py +++ /dev/null @@ -1,433 +0,0 @@ -""" -Complete implementation of the XDG Desktop Entry Specification -http://standards.freedesktop.org/desktop-entry-spec/ - -Not supported: -- Encoding: Legacy Mixed -- Does not check exec parameters -- Does not check URL's -- Does not completly validate deprecated/kde items -- Does not completly check categories -""" - -from xdg.IniFile import IniFile, is_ascii -import xdg.Locale -from xdg.Exceptions import ParsingError -from xdg.util import which -import os.path -import re -import warnings - -class DesktopEntry(IniFile): - "Class to parse and validate Desktop Entries" - - defaultGroup = 'Desktop Entry' - - def __init__(self, filename=None): - """Create a new DesktopEntry. - - If filename exists, it will be parsed as a desktop entry file. If not, - or if filename is None, a blank DesktopEntry is created. - """ - self.content = dict() - if filename and os.path.exists(filename): - self.parse(filename) - elif filename: - self.new(filename) - - def __str__(self): - return self.getName() - - def parse(self, file): - """Parse a desktop entry file. - - This can raise :class:`~xdg.Exceptions.ParsingError`, - :class:`~xdg.Exceptions.DuplicateGroupError` or - :class:`~xdg.Exceptions.DuplicateKeyError`. - """ - IniFile.parse(self, file, ["Desktop Entry", "KDE Desktop Entry"]) - - def findTryExec(self): - """Looks in the PATH for the executable given in the TryExec field. - - Returns the full path to the executable if it is found, None if not. - Raises :class:`~xdg.Exceptions.NoKeyError` if TryExec is not present. - """ - tryexec = self.get('TryExec', strict=True) - return which(tryexec) - - # start standard keys - def getType(self): - return self.get('Type') - def getVersion(self): - """deprecated, use getVersionString instead """ - return self.get('Version', type="numeric") - def getVersionString(self): - return self.get('Version') - def getName(self): - return self.get('Name', locale=True) - def getGenericName(self): - return self.get('GenericName', locale=True) - def getNoDisplay(self): - return self.get('NoDisplay', type="boolean") - def getComment(self): - return self.get('Comment', locale=True) - def getIcon(self): - return self.get('Icon', locale=True) - def getHidden(self): - return self.get('Hidden', type="boolean") - def getOnlyShowIn(self): - return self.get('OnlyShowIn', list=True) - def getNotShowIn(self): - return self.get('NotShowIn', list=True) - def getTryExec(self): - return self.get('TryExec') - def getExec(self): - return self.get('Exec') - def getPath(self): - return self.get('Path') - def getTerminal(self): - return self.get('Terminal', type="boolean") - def getMimeType(self): - """deprecated, use getMimeTypes instead """ - return self.get('MimeType', list=True, type="regex") - def getMimeTypes(self): - return self.get('MimeType', list=True) - def getCategories(self): - return self.get('Categories', list=True) - def getStartupNotify(self): - return self.get('StartupNotify', type="boolean") - def getStartupWMClass(self): - return self.get('StartupWMClass') - def getURL(self): - return self.get('URL') - # end standard keys - - # start kde keys - def getServiceTypes(self): - return self.get('ServiceTypes', list=True) - def getDocPath(self): - return self.get('DocPath') - def getKeywords(self): - return self.get('Keywords', list=True, locale=True) - def getInitialPreference(self): - return self.get('InitialPreference') - def getDev(self): - return self.get('Dev') - def getFSType(self): - return self.get('FSType') - def getMountPoint(self): - return self.get('MountPoint') - def getReadonly(self): - return self.get('ReadOnly', type="boolean") - def getUnmountIcon(self): - return self.get('UnmountIcon', locale=True) - # end kde keys - - # start deprecated keys - def getMiniIcon(self): - return self.get('MiniIcon', locale=True) - def getTerminalOptions(self): - return self.get('TerminalOptions') - def getDefaultApp(self): - return self.get('DefaultApp') - def getProtocols(self): - return self.get('Protocols', list=True) - def getExtensions(self): - return self.get('Extensions', list=True) - def getBinaryPattern(self): - return self.get('BinaryPattern') - def getMapNotify(self): - return self.get('MapNotify') - def getEncoding(self): - return self.get('Encoding') - def getSwallowTitle(self): - return self.get('SwallowTitle', locale=True) - def getSwallowExec(self): - return self.get('SwallowExec') - def getSortOrder(self): - return self.get('SortOrder', list=True) - def getFilePattern(self): - return self.get('FilePattern', type="regex") - def getActions(self): - return self.get('Actions', list=True) - # end deprecated keys - - # desktop entry edit stuff - def new(self, filename): - """Make this instance into a new, blank desktop entry. - - If filename has a .desktop extension, Type is set to Application. If it - has a .directory extension, Type is Directory. Other extensions will - cause :class:`~xdg.Exceptions.ParsingError` to be raised. - """ - if os.path.splitext(filename)[1] == ".desktop": - type = "Application" - elif os.path.splitext(filename)[1] == ".directory": - type = "Directory" - else: - raise ParsingError("Unknown extension", filename) - - self.content = dict() - self.addGroup(self.defaultGroup) - self.set("Type", type) - self.filename = filename - # end desktop entry edit stuff - - # validation stuff - def checkExtras(self): - # header - if self.defaultGroup == "KDE Desktop Entry": - self.warnings.append('[KDE Desktop Entry]-Header is deprecated') - - # file extension - if self.fileExtension == ".kdelnk": - self.warnings.append("File extension .kdelnk is deprecated") - elif self.fileExtension != ".desktop" and self.fileExtension != ".directory": - self.warnings.append('Unknown File extension') - - # Type - try: - self.type = self.content[self.defaultGroup]["Type"] - except KeyError: - self.errors.append("Key 'Type' is missing") - - # Name - try: - self.name = self.content[self.defaultGroup]["Name"] - except KeyError: - self.errors.append("Key 'Name' is missing") - - def checkGroup(self, group): - # check if group header is valid - if not (group == self.defaultGroup \ - or re.match("^Desktop Action [a-zA-Z0-9-]+$", group) \ - or (re.match("^X-", group) and is_ascii(group))): - self.errors.append("Invalid Group name: %s" % group) - else: - #OnlyShowIn and NotShowIn - if ("OnlyShowIn" in self.content[group]) and ("NotShowIn" in self.content[group]): - self.errors.append("Group may either have OnlyShowIn or NotShowIn, but not both") - - def checkKey(self, key, value, group): - # standard keys - if key == "Type": - if value == "ServiceType" or value == "Service" or value == "FSDevice": - self.warnings.append("Type=%s is a KDE extension" % key) - elif value == "MimeType": - self.warnings.append("Type=MimeType is deprecated") - elif not (value == "Application" or value == "Link" or value == "Directory"): - self.errors.append("Value of key 'Type' must be Application, Link or Directory, but is '%s'" % value) - - if self.fileExtension == ".directory" and not value == "Directory": - self.warnings.append("File extension is .directory, but Type is '%s'" % value) - elif self.fileExtension == ".desktop" and value == "Directory": - self.warnings.append("Files with Type=Directory should have the extension .directory") - - if value == "Application": - if "Exec" not in self.content[group]: - self.warnings.append("Type=Application needs 'Exec' key") - if value == "Link": - if "URL" not in self.content[group]: - self.warnings.append("Type=Link needs 'URL' key") - - elif key == "Version": - self.checkValue(key, value) - - elif re.match("^Name"+xdg.Locale.regex+"$", key): - pass # locale string - - elif re.match("^GenericName"+xdg.Locale.regex+"$", key): - pass # locale string - - elif key == "NoDisplay": - self.checkValue(key, value, type="boolean") - - elif re.match("^Comment"+xdg.Locale.regex+"$", key): - pass # locale string - - elif re.match("^Icon"+xdg.Locale.regex+"$", key): - self.checkValue(key, value) - - elif key == "Hidden": - self.checkValue(key, value, type="boolean") - - elif key == "OnlyShowIn": - self.checkValue(key, value, list=True) - self.checkOnlyShowIn(value) - - elif key == "NotShowIn": - self.checkValue(key, value, list=True) - self.checkOnlyShowIn(value) - - elif key == "TryExec": - self.checkValue(key, value) - self.checkType(key, "Application") - - elif key == "Exec": - self.checkValue(key, value) - self.checkType(key, "Application") - - elif key == "Path": - self.checkValue(key, value) - self.checkType(key, "Application") - - elif key == "Terminal": - self.checkValue(key, value, type="boolean") - self.checkType(key, "Application") - - elif key == "Actions": - self.checkValue(key, value, list=True) - self.checkType(key, "Application") - - elif key == "MimeType": - self.checkValue(key, value, list=True) - self.checkType(key, "Application") - - elif key == "Categories": - self.checkValue(key, value) - self.checkType(key, "Application") - self.checkCategories(value) - - elif re.match("^Keywords"+xdg.Locale.regex+"$", key): - self.checkValue(key, value, type="localestring", list=True) - self.checkType(key, "Application") - - elif key == "StartupNotify": - self.checkValue(key, value, type="boolean") - self.checkType(key, "Application") - - elif key == "StartupWMClass": - self.checkType(key, "Application") - - elif key == "URL": - self.checkValue(key, value) - self.checkType(key, "URL") - - # kde extensions - elif key == "ServiceTypes": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "DocPath": - self.checkValue(key, value) - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "InitialPreference": - self.checkValue(key, value, type="numeric") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "Dev": - self.checkValue(key, value) - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "FSType": - self.checkValue(key, value) - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "MountPoint": - self.checkValue(key, value) - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif key == "ReadOnly": - self.checkValue(key, value, type="boolean") - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - elif re.match("^UnmountIcon"+xdg.Locale.regex+"$", key): - self.checkValue(key, value) - self.checkType(key, "FSDevice") - self.warnings.append("Key '%s' is a KDE extension" % key) - - # deprecated keys - elif key == "Encoding": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif re.match("^MiniIcon"+xdg.Locale.regex+"$", key): - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "TerminalOptions": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "DefaultApp": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "Protocols": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "Extensions": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "BinaryPattern": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "MapNotify": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif re.match("^SwallowTitle"+xdg.Locale.regex+"$", key): - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "SwallowExec": - self.checkValue(key, value) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "FilePattern": - self.checkValue(key, value, type="regex", list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - elif key == "SortOrder": - self.checkValue(key, value, list=True) - self.warnings.append("Key '%s' is deprecated" % key) - - # "X-" extensions - elif re.match("^X-[a-zA-Z0-9-]+", key): - pass - - else: - self.errors.append("Invalid key: %s" % key) - - def checkType(self, key, type): - if not self.getType() == type: - self.errors.append("Key '%s' only allowed in Type=%s" % (key, type)) - - def checkOnlyShowIn(self, value): - values = self.getList(value) - valid = ["GNOME", "KDE", "LXDE", "MATE", "Razor", "ROX", "TDE", "Unity", - "XFCE", "Old"] - for item in values: - if item not in valid and item[0:2] != "X-": - self.errors.append("'%s' is not a registered OnlyShowIn value" % item); - - def checkCategories(self, value): - values = self.getList(value) - - main = ["AudioVideo", "Audio", "Video", "Development", "Education", "Game", "Graphics", "Network", "Office", "Science", "Settings", "System", "Utility"] - if not any(item in main for item in values): - self.errors.append("Missing main category") - - additional = ['Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling', 'RevisionControl', 'Translation', 'Calendar', 'ContactManagement', 'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA', 'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor', '2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning', 'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools', 'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager', 'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer', 'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools', 'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer', 'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder', 'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame', 'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying', 'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art', 'Construction', 'Music', 'Languages', 'ArtificialIntelligence', 'Astronomy', 'Biology', 'Chemistry', 'ComputerScience', 'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology', 'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature', 'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics', 'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement', 'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering', 'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor', 'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor', 'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt', 'Motif', 'Java', 'ConsoleOnly'] - allcategories = additional + main - - for item in values: - if item not in allcategories and not item.startswith("X-"): - self.errors.append("'%s' is not a registered Category" % item); - - def checkCategorie(self, value): - """Deprecated alias for checkCategories - only exists for backwards - compatibility. - """ - warnings.warn("checkCategorie is deprecated, use checkCategories", - DeprecationWarning) - return self.checkCategories(value) - diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Exceptions.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/Exceptions.py deleted file mode 100644 index 7096b61..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Exceptions.py +++ /dev/null @@ -1,84 +0,0 @@ -""" -Exception Classes for the xdg package -""" - -debug = False - -class Error(Exception): - """Base class for exceptions defined here.""" - def __init__(self, msg): - self.msg = msg - Exception.__init__(self, msg) - def __str__(self): - return self.msg - -class ValidationError(Error): - """Raised when a file fails to validate. - - The filename is the .file attribute. - """ - def __init__(self, msg, file): - self.msg = msg - self.file = file - Error.__init__(self, "ValidationError in file '%s': %s " % (file, msg)) - -class ParsingError(Error): - """Raised when a file cannot be parsed. - - The filename is the .file attribute. - """ - def __init__(self, msg, file): - self.msg = msg - self.file = file - Error.__init__(self, "ParsingError in file '%s', %s" % (file, msg)) - -class NoKeyError(Error): - """Raised when trying to access a nonexistant key in an INI-style file. - - Attributes are .key, .group and .file. - """ - def __init__(self, key, group, file): - Error.__init__(self, "No key '%s' in group %s of file %s" % (key, group, file)) - self.key = key - self.group = group - self.file = file - -class DuplicateKeyError(Error): - """Raised when the same key occurs twice in an INI-style file. - - Attributes are .key, .group and .file. - """ - def __init__(self, key, group, file): - Error.__init__(self, "Duplicate key '%s' in group %s of file %s" % (key, group, file)) - self.key = key - self.group = group - self.file = file - -class NoGroupError(Error): - """Raised when trying to access a nonexistant group in an INI-style file. - - Attributes are .group and .file. - """ - def __init__(self, group, file): - Error.__init__(self, "No group: %s in file %s" % (group, file)) - self.group = group - self.file = file - -class DuplicateGroupError(Error): - """Raised when the same key occurs twice in an INI-style file. - - Attributes are .group and .file. - """ - def __init__(self, group, file): - Error.__init__(self, "Duplicate group: %s in file %s" % (group, file)) - self.group = group - self.file = file - -class NoThemeError(Error): - """Raised when trying to access a nonexistant icon theme. - - The name of the theme is the .theme attribute. - """ - def __init__(self, theme): - Error.__init__(self, "No such icon-theme: %s" % theme) - self.theme = theme diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/IconTheme.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/IconTheme.py deleted file mode 100644 index bda8b8f..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/IconTheme.py +++ /dev/null @@ -1,445 +0,0 @@ -""" -Complete implementation of the XDG Icon Spec -http://standards.freedesktop.org/icon-theme-spec/ -""" - -import os, time -import re - -from xdg.IniFile import IniFile, is_ascii -from xdg.BaseDirectory import xdg_data_dirs -from xdg.Exceptions import NoThemeError, debug - -import xdg.Config - -class IconTheme(IniFile): - "Class to parse and validate IconThemes" - def __init__(self): - IniFile.__init__(self) - - def __repr__(self): - return self.name - - def parse(self, file): - IniFile.parse(self, file, ["Icon Theme", "KDE Icon Theme"]) - self.dir = os.path.dirname(file) - (nil, self.name) = os.path.split(self.dir) - - def getDir(self): - return self.dir - - # Standard Keys - def getName(self): - return self.get('Name', locale=True) - def getComment(self): - return self.get('Comment', locale=True) - def getInherits(self): - return self.get('Inherits', list=True) - def getDirectories(self): - return self.get('Directories', list=True) - def getScaledDirectories(self): - return self.get('ScaledDirectories', list=True) - def getHidden(self): - return self.get('Hidden', type="boolean") - def getExample(self): - return self.get('Example') - - # Per Directory Keys - def getSize(self, directory): - return self.get('Size', type="integer", group=directory) - def getContext(self, directory): - return self.get('Context', group=directory) - def getType(self, directory): - value = self.get('Type', group=directory) - if value: - return value - else: - return "Threshold" - def getMaxSize(self, directory): - value = self.get('MaxSize', type="integer", group=directory) - if value or value == 0: - return value - else: - return self.getSize(directory) - def getMinSize(self, directory): - value = self.get('MinSize', type="integer", group=directory) - if value or value == 0: - return value - else: - return self.getSize(directory) - def getThreshold(self, directory): - value = self.get('Threshold', type="integer", group=directory) - if value or value == 0: - return value - else: - return 2 - - def getScale(self, directory): - value = self.get('Scale', type="integer", group=directory) - return value or 1 - - # validation stuff - def checkExtras(self): - # header - if self.defaultGroup == "KDE Icon Theme": - self.warnings.append('[KDE Icon Theme]-Header is deprecated') - - # file extension - if self.fileExtension == ".theme": - pass - elif self.fileExtension == ".desktop": - self.warnings.append('.desktop fileExtension is deprecated') - else: - self.warnings.append('Unknown File extension') - - # Check required keys - # Name - try: - self.name = self.content[self.defaultGroup]["Name"] - except KeyError: - self.errors.append("Key 'Name' is missing") - - # Comment - try: - self.comment = self.content[self.defaultGroup]["Comment"] - except KeyError: - self.errors.append("Key 'Comment' is missing") - - # Directories - try: - self.directories = self.content[self.defaultGroup]["Directories"] - except KeyError: - self.errors.append("Key 'Directories' is missing") - - def checkGroup(self, group): - # check if group header is valid - if group == self.defaultGroup: - try: - self.name = self.content[group]["Name"] - except KeyError: - self.errors.append("Key 'Name' in Group '%s' is missing" % group) - try: - self.name = self.content[group]["Comment"] - except KeyError: - self.errors.append("Key 'Comment' in Group '%s' is missing" % group) - elif group in self.getDirectories(): - try: - self.type = self.content[group]["Type"] - except KeyError: - self.type = "Threshold" - try: - self.name = self.content[group]["Size"] - except KeyError: - self.errors.append("Key 'Size' in Group '%s' is missing" % group) - elif not (re.match(r"^\[X-", group) and is_ascii(group)): - self.errors.append("Invalid Group name: %s" % group) - - def checkKey(self, key, value, group): - # standard keys - if group == self.defaultGroup: - if re.match("^Name"+xdg.Locale.regex+"$", key): - pass - elif re.match("^Comment"+xdg.Locale.regex+"$", key): - pass - elif key == "Inherits": - self.checkValue(key, value, list=True) - elif key == "Directories": - self.checkValue(key, value, list=True) - elif key == "ScaledDirectories": - self.checkValue(key, value, list=True) - elif key == "Hidden": - self.checkValue(key, value, type="boolean") - elif key == "Example": - self.checkValue(key, value) - elif re.match("^X-[a-zA-Z0-9-]+", key): - pass - else: - self.errors.append("Invalid key: %s" % key) - elif group in self.getDirectories(): - if key == "Size": - self.checkValue(key, value, type="integer") - elif key == "Context": - self.checkValue(key, value) - elif key == "Type": - self.checkValue(key, value) - if value not in ["Fixed", "Scalable", "Threshold"]: - self.errors.append("Key 'Type' must be one out of 'Fixed','Scalable','Threshold', but is %s" % value) - elif key == "MaxSize": - self.checkValue(key, value, type="integer") - if self.type != "Scalable": - self.errors.append("Key 'MaxSize' give, but Type is %s" % self.type) - elif key == "MinSize": - self.checkValue(key, value, type="integer") - if self.type != "Scalable": - self.errors.append("Key 'MinSize' give, but Type is %s" % self.type) - elif key == "Threshold": - self.checkValue(key, value, type="integer") - if self.type != "Threshold": - self.errors.append("Key 'Threshold' give, but Type is %s" % self.type) - elif key == "Scale": - self.checkValue(key, value, type="integer") - elif re.match("^X-[a-zA-Z0-9-]+", key): - pass - else: - self.errors.append("Invalid key: %s" % key) - - -class IconData(IniFile): - "Class to parse and validate IconData Files" - def __init__(self): - IniFile.__init__(self) - - def __repr__(self): - displayname = self.getDisplayName() - if displayname: - return "" % displayname - else: - return "" - - def parse(self, file): - IniFile.parse(self, file, ["Icon Data"]) - - # Standard Keys - def getDisplayName(self): - """Retrieve the display name from the icon data, if one is specified.""" - return self.get('DisplayName', locale=True) - def getEmbeddedTextRectangle(self): - """Retrieve the embedded text rectangle from the icon data as a list of - numbers (x0, y0, x1, y1), if it is specified.""" - return self.get('EmbeddedTextRectangle', type="integer", list=True) - def getAttachPoints(self): - """Retrieve the anchor points for overlays & emblems from the icon data, - as a list of co-ordinate pairs, if they are specified.""" - return self.get('AttachPoints', type="point", list=True) - - # validation stuff - def checkExtras(self): - # file extension - if self.fileExtension != ".icon": - self.warnings.append('Unknown File extension') - - def checkGroup(self, group): - # check if group header is valid - if not (group == self.defaultGroup \ - or (re.match(r"^\[X-", group) and is_ascii(group))): - self.errors.append("Invalid Group name: %s" % group.encode("ascii", "replace")) - - def checkKey(self, key, value, group): - # standard keys - if re.match("^DisplayName"+xdg.Locale.regex+"$", key): - pass - elif key == "EmbeddedTextRectangle": - self.checkValue(key, value, type="integer", list=True) - elif key == "AttachPoints": - self.checkValue(key, value, type="point", list=True) - elif re.match("^X-[a-zA-Z0-9-]+", key): - pass - else: - self.errors.append("Invalid key: %s" % key) - - - -icondirs = [] -for basedir in xdg_data_dirs: - icondirs.append(os.path.join(basedir, "icons")) - icondirs.append(os.path.join(basedir, "pixmaps")) -icondirs.append(os.path.expanduser("~/.icons")) - -# just cache variables, they give a 10x speed improvement -themes = [] -theme_cache = {} -dir_cache = {} -icon_cache = {} - -def getIconPath(iconname, size = None, theme = None, extensions = ["png", "svg", "xpm"]): - """Get the path to a specified icon. - - size : - Icon size in pixels. Defaults to ``xdg.Config.icon_size``. - theme : - Icon theme name. Defaults to ``xdg.Config.icon_theme``. If the icon isn't - found in the specified theme, it will be looked up in the basic 'hicolor' - theme. - extensions : - List of preferred file extensions. - - Example:: - - >>> getIconPath("inkscape", 32) - '/usr/share/icons/hicolor/32x32/apps/inkscape.png' - """ - - global themes - - if size == None: - size = xdg.Config.icon_size - if theme == None: - theme = xdg.Config.icon_theme - - # if we have an absolute path, just return it - if os.path.isabs(iconname): - return iconname - - # check if it has an extension and strip it - if os.path.splitext(iconname)[1][1:] in extensions: - iconname = os.path.splitext(iconname)[0] - - # parse theme files - if (themes == []) or (themes[0].name != theme): - themes = list(__get_themes(theme)) - - # more caching (icon looked up in the last 5 seconds?) - tmp = (iconname, size, theme, tuple(extensions)) - try: - timestamp, icon = icon_cache[tmp] - except KeyError: - pass - else: - if (time.time() - timestamp) >= xdg.Config.cache_time: - del icon_cache[tmp] - else: - return icon - - for thme in themes: - icon = LookupIcon(iconname, size, thme, extensions) - if icon: - icon_cache[tmp] = (time.time(), icon) - return icon - - # cache stuff again (directories looked up in the last 5 seconds?) - for directory in icondirs: - if (directory not in dir_cache \ - or (int(time.time() - dir_cache[directory][1]) >= xdg.Config.cache_time \ - and dir_cache[directory][2] < os.path.getmtime(directory))) \ - and os.path.isdir(directory): - dir_cache[directory] = (os.listdir(directory), time.time(), os.path.getmtime(directory)) - - for dir, values in dir_cache.items(): - for extension in extensions: - try: - if iconname + "." + extension in values[0]: - icon = os.path.join(dir, iconname + "." + extension) - icon_cache[tmp] = [time.time(), icon] - return icon - except UnicodeDecodeError as e: - if debug: - raise e - else: - pass - - # we haven't found anything? "hicolor" is our fallback - if theme != "hicolor": - icon = getIconPath(iconname, size, "hicolor") - icon_cache[tmp] = [time.time(), icon] - return icon - -def getIconData(path): - """Retrieve the data from the .icon file corresponding to the given file. If - there is no .icon file, it returns None. - - Example:: - - getIconData("/usr/share/icons/Tango/scalable/places/folder.svg") - """ - if os.path.isfile(path): - icon_file = os.path.splitext(path)[0] + ".icon" - if os.path.isfile(icon_file): - data = IconData() - data.parse(icon_file) - return data - -def __get_themes(themename): - """Generator yielding IconTheme objects for a specified theme and any themes - from which it inherits. - """ - for dir in icondirs: - theme_file = os.path.join(dir, themename, "index.theme") - if os.path.isfile(theme_file): - break - theme_file = os.path.join(dir, themename, "index.desktop") - if os.path.isfile(theme_file): - break - else: - if debug: - raise NoThemeError(themename) - return - - theme = IconTheme() - theme.parse(theme_file) - yield theme - for subtheme in theme.getInherits(): - for t in __get_themes(subtheme): - yield t - -def LookupIcon(iconname, size, theme, extensions): - # look for the cache - if theme.name not in theme_cache: - theme_cache[theme.name] = [] - theme_cache[theme.name].append(time.time() - (xdg.Config.cache_time + 1)) # [0] last time of lookup - theme_cache[theme.name].append(0) # [1] mtime - theme_cache[theme.name].append(dict()) # [2] dir: [subdir, [items]] - - # cache stuff (directory lookuped up the in the last 5 seconds?) - if int(time.time() - theme_cache[theme.name][0]) >= xdg.Config.cache_time: - theme_cache[theme.name][0] = time.time() - for subdir in theme.getDirectories(): - for directory in icondirs: - dir = os.path.join(directory,theme.name,subdir) - if (dir not in theme_cache[theme.name][2] \ - or theme_cache[theme.name][1] < os.path.getmtime(os.path.join(directory,theme.name))) \ - and subdir != "" \ - and os.path.isdir(dir): - theme_cache[theme.name][2][dir] = [subdir, os.listdir(dir)] - theme_cache[theme.name][1] = os.path.getmtime(os.path.join(directory,theme.name)) - - for dir, values in theme_cache[theme.name][2].items(): - if DirectoryMatchesSize(values[0], size, theme): - for extension in extensions: - if iconname + "." + extension in values[1]: - return os.path.join(dir, iconname + "." + extension) - - minimal_size = 2**31 - closest_filename = "" - for dir, values in theme_cache[theme.name][2].items(): - distance = DirectorySizeDistance(values[0], size, theme) - if distance < minimal_size: - for extension in extensions: - if iconname + "." + extension in values[1]: - closest_filename = os.path.join(dir, iconname + "." + extension) - minimal_size = distance - - return closest_filename - -def DirectoryMatchesSize(subdir, iconsize, theme): - Type = theme.getType(subdir) - Size = theme.getSize(subdir) - Threshold = theme.getThreshold(subdir) - MinSize = theme.getMinSize(subdir) - MaxSize = theme.getMaxSize(subdir) - if Type == "Fixed": - return Size == iconsize - elif Type == "Scaleable": - return MinSize <= iconsize <= MaxSize - elif Type == "Threshold": - return Size - Threshold <= iconsize <= Size + Threshold - -def DirectorySizeDistance(subdir, iconsize, theme): - Type = theme.getType(subdir) - Size = theme.getSize(subdir) - Threshold = theme.getThreshold(subdir) - MinSize = theme.getMinSize(subdir) - MaxSize = theme.getMaxSize(subdir) - if Type == "Fixed": - return abs(Size - iconsize) - elif Type == "Scalable": - if iconsize < MinSize: - return MinSize - iconsize - elif iconsize > MaxSize: - return MaxSize - iconsize - return 0 - elif Type == "Threshold": - if iconsize < Size - Threshold: - return MinSize - iconsize - elif iconsize > Size + Threshold: - return iconsize - MaxSize - return 0 diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/IniFile.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/IniFile.py deleted file mode 100644 index 84be614..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/IniFile.py +++ /dev/null @@ -1,417 +0,0 @@ -""" -Base Class for DesktopEntry, IconTheme and IconData -""" - -import re, os, stat, io -from xdg.Exceptions import (ParsingError, DuplicateGroupError, NoGroupError, - NoKeyError, DuplicateKeyError, ValidationError, - debug) -import xdg.Locale -from xdg.util import u - -def is_ascii(s): - """Return True if a string consists entirely of ASCII characters.""" - try: - s.encode('ascii', 'strict') - return True - except UnicodeError: - return False - -class IniFile: - defaultGroup = '' - fileExtension = '' - - filename = '' - - tainted = False - - def __init__(self, filename=None): - self.content = dict() - if filename: - self.parse(filename) - - def __cmp__(self, other): - return cmp(self.content, other.content) - - def parse(self, filename, headers=None): - '''Parse an INI file. - - headers -- list of headers the parser will try to select as a default header - ''' - # for performance reasons - content = self.content - - if not os.path.isfile(filename): - raise ParsingError("File not found", filename) - - try: - # The content should be UTF-8, but legacy files can have other - # encodings, including mixed encodings in one file. We don't attempt - # to decode them, but we silence the errors. - fd = io.open(filename, 'r', encoding='utf-8', errors='replace') - except IOError as e: - if debug: - raise e - else: - return - - # parse file - with fd: - for line in fd: - line = line.strip() - # empty line - if not line: - continue - # comment - elif line[0] == '#': - continue - # new group - elif line[0] == '[': - currentGroup = line.lstrip("[").rstrip("]") - if debug and self.hasGroup(currentGroup): - raise DuplicateGroupError(currentGroup, filename) - else: - content[currentGroup] = {} - # key - else: - try: - key, value = line.split("=", 1) - except ValueError: - raise ParsingError("Invalid line: " + line, filename) - - key = key.strip() # Spaces before/after '=' should be ignored - try: - if debug and self.hasKey(key, currentGroup): - raise DuplicateKeyError(key, currentGroup, filename) - else: - content[currentGroup][key] = value.strip() - except (IndexError, UnboundLocalError): - raise ParsingError("Parsing error on key, group missing", filename) - - self.filename = filename - self.tainted = False - - # check header - if headers: - for header in headers: - if header in content: - self.defaultGroup = header - break - else: - raise ParsingError("[%s]-Header missing" % headers[0], filename) - - # start stuff to access the keys - def get(self, key, group=None, locale=False, type="string", list=False, strict=False): - # set default group - if not group: - group = self.defaultGroup - - # return key (with locale) - if (group in self.content) and (key in self.content[group]): - if locale: - value = self.content[group][self.__addLocale(key, group)] - else: - value = self.content[group][key] - else: - if strict or debug: - if group not in self.content: - raise NoGroupError(group, self.filename) - elif key not in self.content[group]: - raise NoKeyError(key, group, self.filename) - else: - value = "" - - if list == True: - values = self.getList(value) - result = [] - else: - values = [value] - - for value in values: - if type == "boolean": - value = self.__getBoolean(value) - elif type == "integer": - try: - value = int(value) - except ValueError: - value = 0 - elif type == "numeric": - try: - value = float(value) - except ValueError: - value = 0.0 - elif type == "regex": - value = re.compile(value) - elif type == "point": - x, y = value.split(",") - value = int(x), int(y) - - if list == True: - result.append(value) - else: - result = value - - return result - # end stuff to access the keys - - # start subget - def getList(self, string): - if re.search(r"(? 0: - key = key + "[" + xdg.Locale.langs[0] + "]" - - try: - self.content[group][key] = value - except KeyError: - raise NoGroupError(group, self.filename) - - self.tainted = (value == self.get(key, group)) - - def addGroup(self, group): - if self.hasGroup(group): - if debug: - raise DuplicateGroupError(group, self.filename) - else: - self.content[group] = {} - self.tainted = True - - def removeGroup(self, group): - existed = group in self.content - if existed: - del self.content[group] - self.tainted = True - else: - if debug: - raise NoGroupError(group, self.filename) - return existed - - def removeKey(self, key, group=None, locales=True): - # set default group - if not group: - group = self.defaultGroup - - try: - if locales: - for name in list(self.content[group]): - if re.match("^" + key + xdg.Locale.regex + "$", name) and name != key: - del self.content[group][name] - value = self.content[group].pop(key) - self.tainted = True - return value - except KeyError as e: - if debug: - if e == group: - raise NoGroupError(group, self.filename) - else: - raise NoKeyError(key, group, self.filename) - else: - return "" - - # misc - def groups(self): - return self.content.keys() - - def hasGroup(self, group): - return group in self.content - - def hasKey(self, key, group=None): - # set default group - if not group: - group = self.defaultGroup - - return key in self.content[group] - - def getFileName(self): - return self.filename diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Locale.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/Locale.py deleted file mode 100644 index d0a70d2..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Locale.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -Helper Module for Locale settings - -This module is based on a ROX module (LGPL): - -http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/i18n.py?rev=1.3&view=log -""" - -import os -from locale import normalize - -regex = r"(\[([a-zA-Z]+)(_[a-zA-Z]+)?(\.[a-zA-Z0-9-]+)?(@[a-zA-Z]+)?\])?" - -def _expand_lang(locale): - locale = normalize(locale) - COMPONENT_CODESET = 1 << 0 - COMPONENT_MODIFIER = 1 << 1 - COMPONENT_TERRITORY = 1 << 2 - # split up the locale into its base components - mask = 0 - pos = locale.find('@') - if pos >= 0: - modifier = locale[pos:] - locale = locale[:pos] - mask |= COMPONENT_MODIFIER - else: - modifier = '' - pos = locale.find('.') - codeset = '' - if pos >= 0: - locale = locale[:pos] - pos = locale.find('_') - if pos >= 0: - territory = locale[pos:] - locale = locale[:pos] - mask |= COMPONENT_TERRITORY - else: - territory = '' - language = locale - ret = [] - for i in range(mask+1): - if not (i & ~mask): # if all components for this combo exist ... - val = language - if i & COMPONENT_TERRITORY: val += territory - if i & COMPONENT_CODESET: val += codeset - if i & COMPONENT_MODIFIER: val += modifier - ret.append(val) - ret.reverse() - return ret - -def expand_languages(languages=None): - # Get some reasonable defaults for arguments that were not supplied - if languages is None: - languages = [] - for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'): - val = os.environ.get(envar) - if val: - languages = val.split(':') - break - #if 'C' not in languages: - # languages.append('C') - - # now normalize and expand the languages - nelangs = [] - for lang in languages: - for nelang in _expand_lang(lang): - if nelang not in nelangs: - nelangs.append(nelang) - return nelangs - -def update(language=None): - global langs - if language: - langs = expand_languages([language]) - else: - langs = expand_languages() - -langs = [] -update() diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Menu.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/Menu.py deleted file mode 100644 index 1dd2af5..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Menu.py +++ /dev/null @@ -1,1142 +0,0 @@ -""" -Implementation of the XDG Menu Specification -http://standards.freedesktop.org/menu-spec/ - -Example code: - -from xdg.Menu import parse, Menu, MenuEntry - -def print_menu(menu, tab=0): - for submenu in menu.Entries: - if isinstance(submenu, Menu): - print ("\t" * tab) + unicode(submenu) - print_menu(submenu, tab+1) - elif isinstance(submenu, MenuEntry): - print ("\t" * tab) + unicode(submenu.DesktopEntry) - -print_menu(parse()) -""" - -import os -import locale -import subprocess -import ast -import sys -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree - -from xdg.BaseDirectory import xdg_data_dirs, xdg_config_dirs -from xdg.DesktopEntry import DesktopEntry -from xdg.Exceptions import ParsingError -from xdg.util import PY3 - -import xdg.Locale -import xdg.Config - - -def _ast_const(name): - if sys.version_info >= (3, 4): - name = ast.literal_eval(name) - if sys.version_info >= (3, 8): - return ast.Constant(name) - else: - return ast.NameConstant(name) - else: - return ast.Name(id=name, ctx=ast.Load()) - - -def _strxfrm(s): - """Wrapper around locale.strxfrm that accepts unicode strings on Python 2. - - See Python bug #2481. - """ - if (not PY3) and isinstance(s, unicode): - s = s.encode('utf-8') - return locale.strxfrm(s) - - -DELETED = "Deleted" -NO_DISPLAY = "NoDisplay" -HIDDEN = "Hidden" -EMPTY = "Empty" -NOT_SHOW_IN = "NotShowIn" -NO_EXEC = "NoExec" - - -class Menu: - """Menu containing sub menus under menu.Entries - - Contains both Menu and MenuEntry items. - """ - def __init__(self): - # Public stuff - self.Name = "" - self.Directory = None - self.Entries = [] - self.Doc = "" - self.Filename = "" - self.Depth = 0 - self.Parent = None - self.NotInXml = False - - # Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN - self.Show = True - self.Visible = 0 - - # Private stuff, only needed for parsing - self.AppDirs = [] - self.DefaultLayout = None - self.Deleted = None - self.Directories = [] - self.DirectoryDirs = [] - self.Layout = None - self.MenuEntries = [] - self.Moves = [] - self.OnlyUnallocated = None - self.Rules = [] - self.Submenus = [] - - def __str__(self): - return self.Name - - def __add__(self, other): - for dir in other.AppDirs: - self.AppDirs.append(dir) - - for dir in other.DirectoryDirs: - self.DirectoryDirs.append(dir) - - for directory in other.Directories: - self.Directories.append(directory) - - if other.Deleted is not None: - self.Deleted = other.Deleted - - if other.OnlyUnallocated is not None: - self.OnlyUnallocated = other.OnlyUnallocated - - if other.Layout: - self.Layout = other.Layout - - if other.DefaultLayout: - self.DefaultLayout = other.DefaultLayout - - for rule in other.Rules: - self.Rules.append(rule) - - for move in other.Moves: - self.Moves.append(move) - - for submenu in other.Submenus: - self.addSubmenu(submenu) - - return self - - # FIXME: Performance: cache getName() - def __cmp__(self, other): - return locale.strcoll(self.getName(), other.getName()) - - def _key(self): - """Key function for locale-aware sorting.""" - return _strxfrm(self.getName()) - - def __lt__(self, other): - try: - other = other._key() - except AttributeError: - pass - return self._key() < other - - def __eq__(self, other): - try: - return self.Name == unicode(other) - except NameError: # unicode() becomes str() in Python 3 - return self.Name == str(other) - - """ PUBLIC STUFF """ - def getEntries(self, show_hidden=False): - """Interator for a list of Entries visible to the user.""" - for entry in self.Entries: - if show_hidden: - yield entry - elif entry.Show is True: - yield entry - - # FIXME: Add searchEntry/seaqrchMenu function - # search for name/comment/genericname/desktopfileid - # return multiple items - - def getMenuEntry(self, desktopfileid, deep=False): - """Searches for a MenuEntry with a given DesktopFileID.""" - for menuentry in self.MenuEntries: - if menuentry.DesktopFileID == desktopfileid: - return menuentry - if deep: - for submenu in self.Submenus: - submenu.getMenuEntry(desktopfileid, deep) - - def getMenu(self, path): - """Searches for a Menu with a given path.""" - array = path.split("/", 1) - for submenu in self.Submenus: - if submenu.Name == array[0]: - if len(array) > 1: - return submenu.getMenu(array[1]) - else: - return submenu - - def getPath(self, org=False, toplevel=False): - """Returns this menu's path in the menu structure.""" - parent = self - names = [] - while 1: - if org: - names.append(parent.Name) - else: - names.append(parent.getName()) - if parent.Depth > 0: - parent = parent.Parent - else: - break - names.reverse() - path = "" - if not toplevel: - names.pop(0) - for name in names: - path = os.path.join(path, name) - return path - - def getName(self): - """Returns the menu's localised name.""" - try: - return self.Directory.DesktopEntry.getName() - except AttributeError: - return self.Name - - def getGenericName(self): - """Returns the menu's generic name.""" - try: - return self.Directory.DesktopEntry.getGenericName() - except AttributeError: - return "" - - def getComment(self): - """Returns the menu's comment text.""" - try: - return self.Directory.DesktopEntry.getComment() - except AttributeError: - return "" - - def getIcon(self): - """Returns the menu's icon, filename or simple name""" - try: - return self.Directory.DesktopEntry.getIcon() - except AttributeError: - return "" - - def sort(self): - self.Entries = [] - self.Visible = 0 - - for submenu in self.Submenus: - submenu.sort() - - _submenus = set() - _entries = set() - - for order in self.Layout.order: - if order[0] == "Filename": - _entries.add(order[1]) - elif order[0] == "Menuname": - _submenus.add(order[1]) - - for order in self.Layout.order: - if order[0] == "Separator": - separator = Separator(self) - if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator): - separator.Show = False - self.Entries.append(separator) - elif order[0] == "Filename": - menuentry = self.getMenuEntry(order[1]) - if menuentry: - self.Entries.append(menuentry) - elif order[0] == "Menuname": - submenu = self.getMenu(order[1]) - if submenu: - if submenu.Layout.inline: - self.merge_inline(submenu) - else: - self.Entries.append(submenu) - elif order[0] == "Merge": - if order[1] == "files" or order[1] == "all": - self.MenuEntries.sort() - for menuentry in self.MenuEntries: - if menuentry.DesktopFileID not in _entries: - self.Entries.append(menuentry) - elif order[1] == "menus" or order[1] == "all": - self.Submenus.sort() - for submenu in self.Submenus: - if submenu.Name not in _submenus: - if submenu.Layout.inline: - self.merge_inline(submenu) - else: - self.Entries.append(submenu) - - # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec - for entry in self.Entries: - entry.Show = True - self.Visible += 1 - if isinstance(entry, Menu): - if entry.Deleted is True: - entry.Show = DELETED - self.Visible -= 1 - elif isinstance(entry.Directory, MenuEntry): - if entry.Directory.DesktopEntry.getNoDisplay(): - entry.Show = NO_DISPLAY - self.Visible -= 1 - elif entry.Directory.DesktopEntry.getHidden(): - entry.Show = HIDDEN - self.Visible -= 1 - elif isinstance(entry, MenuEntry): - if entry.DesktopEntry.getNoDisplay(): - entry.Show = NO_DISPLAY - self.Visible -= 1 - elif entry.DesktopEntry.getHidden(): - entry.Show = HIDDEN - self.Visible -= 1 - elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec(): - entry.Show = NO_EXEC - self.Visible -= 1 - elif xdg.Config.windowmanager: - if (entry.DesktopEntry.getOnlyShowIn() != [] and ( - xdg.Config.windowmanager not in entry.DesktopEntry.getOnlyShowIn() - ) - ) or ( - xdg.Config.windowmanager in entry.DesktopEntry.getNotShowIn() - ): - entry.Show = NOT_SHOW_IN - self.Visible -= 1 - elif isinstance(entry, Separator): - self.Visible -= 1 - # remove separators at the beginning and at the end - if len(self.Entries) > 0: - if isinstance(self.Entries[0], Separator): - self.Entries[0].Show = False - if len(self.Entries) > 1: - if isinstance(self.Entries[-1], Separator): - self.Entries[-1].Show = False - - # show_empty tag - for entry in self.Entries[:]: - if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0: - entry.Show = EMPTY - self.Visible -= 1 - if entry.NotInXml is True: - self.Entries.remove(entry) - - """ PRIVATE STUFF """ - def addSubmenu(self, newmenu): - for submenu in self.Submenus: - if submenu == newmenu: - submenu += newmenu - break - else: - self.Submenus.append(newmenu) - newmenu.Parent = self - newmenu.Depth = self.Depth + 1 - - # inline tags - def merge_inline(self, submenu): - """Appends a submenu's entries to this menu - See the section of the spec about the "inline" attribute - """ - if len(submenu.Entries) == 1 and submenu.Layout.inline_alias: - menuentry = submenu.Entries[0] - menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True) - menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True) - menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True) - self.Entries.append(menuentry) - elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0: - if submenu.Layout.inline_header: - header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment()) - self.Entries.append(header) - for entry in submenu.Entries: - self.Entries.append(entry) - else: - self.Entries.append(submenu) - - -class Move: - "A move operation" - def __init__(self, old="", new=""): - self.Old = old - self.New = new - - def __cmp__(self, other): - return cmp(self.Old, other.Old) - - -class Layout: - "Menu Layout class" - def __init__(self, show_empty=False, inline=False, inline_limit=4, - inline_header=True, inline_alias=False): - self.show_empty = show_empty - self.inline = inline - self.inline_limit = inline_limit - self.inline_header = inline_header - self.inline_alias = inline_alias - self._order = [] - self._default_order = [ - ['Merge', 'menus'], - ['Merge', 'files'] - ] - - @property - def order(self): - return self._order if self._order else self._default_order - - @order.setter - def order(self, order): - self._order = order - - -class Rule: - """Include / Exclude Rules Class""" - - TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1 - - @classmethod - def fromFilename(cls, type, filename): - tree = ast.Expression( - body=ast.Compare( - left=ast.Str(filename), - ops=[ast.Eq()], - comparators=[ast.Attribute( - value=ast.Name(id='menuentry', ctx=ast.Load()), - attr='DesktopFileID', - ctx=ast.Load() - )] - ), - lineno=1, col_offset=0 - ) - ast.fix_missing_locations(tree) - rule = Rule(type, tree) - return rule - - def __init__(self, type, expression): - # Type is TYPE_INCLUDE or TYPE_EXCLUDE - self.Type = type - # expression is ast.Expression - self.expression = expression - self.code = compile(self.expression, '', 'eval') - - def __str__(self): - return ast.dump(self.expression) - - def apply(self, menuentries, run): - for menuentry in menuentries: - if run == 2 and (menuentry.MatchedInclude is True or - menuentry.Allocated is True): - continue - if eval(self.code): - if self.Type is Rule.TYPE_INCLUDE: - menuentry.Add = True - menuentry.MatchedInclude = True - else: - menuentry.Add = False - return menuentries - - -class MenuEntry: - "Wrapper for 'Menu Style' Desktop Entries" - - TYPE_USER = "User" - TYPE_SYSTEM = "System" - TYPE_BOTH = "Both" - - def __init__(self, filename, dir="", prefix=""): - # Create entry - self.DesktopEntry = DesktopEntry(os.path.join(dir, filename)) - self.setAttributes(filename, dir, prefix) - - # Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC - self.Show = True - - # Semi-Private - self.Original = None - self.Parents = [] - - # Private Stuff - self.Allocated = False - self.Add = False - self.MatchedInclude = False - - # Caching - self.Categories = self.DesktopEntry.getCategories() - - def save(self): - """Save any changes to the desktop entry.""" - if self.DesktopEntry.tainted: - self.DesktopEntry.write() - - def getDir(self): - """Return the directory containing the desktop entry file.""" - return self.DesktopEntry.filename.replace(self.Filename, '') - - def getType(self): - """Return the type of MenuEntry, System/User/Both""" - if not xdg.Config.root_mode: - if self.Original: - return self.TYPE_BOTH - elif xdg_data_dirs[0] in self.DesktopEntry.filename: - return self.TYPE_USER - else: - return self.TYPE_SYSTEM - else: - return self.TYPE_USER - - def setAttributes(self, filename, dir="", prefix=""): - self.Filename = filename - self.Prefix = prefix - self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-") - - if not os.path.isabs(self.DesktopEntry.filename): - self.__setFilename() - - def updateAttributes(self): - if self.getType() == self.TYPE_SYSTEM: - self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix) - self.__setFilename() - - def __setFilename(self): - if not xdg.Config.root_mode: - path = xdg_data_dirs[0] - else: - path = xdg_data_dirs[1] - - if self.DesktopEntry.getType() == "Application": - dir_ = os.path.join(path, "applications") - else: - dir_ = os.path.join(path, "desktop-directories") - - self.DesktopEntry.filename = os.path.join(dir_, self.Filename) - - def __cmp__(self, other): - return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName()) - - def _key(self): - """Key function for locale-aware sorting.""" - return _strxfrm(self.DesktopEntry.getName()) - - def __lt__(self, other): - try: - other = other._key() - except AttributeError: - pass - return self._key() < other - - def __eq__(self, other): - if self.DesktopFileID == str(other): - return True - else: - return False - - def __repr__(self): - return self.DesktopFileID - - -class Separator: - "Just a dummy class for Separators" - def __init__(self, parent): - self.Parent = parent - self.Show = True - - -class Header: - "Class for Inline Headers" - def __init__(self, name, generic_name, comment): - self.Name = name - self.GenericName = generic_name - self.Comment = comment - - def __str__(self): - return self.Name - - -TYPE_DIR, TYPE_FILE = 0, 1 - - -def _check_file_path(value, filename, type): - path = os.path.dirname(filename) - if not os.path.isabs(value): - value = os.path.join(path, value) - value = os.path.abspath(value) - if not os.path.exists(value): - return False - if type == TYPE_DIR and os.path.isdir(value): - return value - if type == TYPE_FILE and os.path.isfile(value): - return value - return False - - -def _get_menu_file_path(filename): - dirs = list(xdg_config_dirs) - if xdg.Config.root_mode is True: - dirs.pop(0) - for d in dirs: - menuname = os.path.join(d, "menus", filename) - if os.path.isfile(menuname): - return menuname - - -def _to_bool(value): - if isinstance(value, bool): - return value - return value.lower() == "true" - - -# remove duplicate entries from a list -def _dedupe(_list): - _set = {} - _list.reverse() - _list = [_set.setdefault(e, e) for e in _list if e not in _set] - _list.reverse() - return _list - - -class XMLMenuBuilder(object): - - def __init__(self, debug=False): - self.debug = debug - - def parse(self, filename=None): - """Load an applications.menu file. - - filename : str, optional - The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``. - """ - # convert to absolute path - if filename and not os.path.isabs(filename): - filename = _get_menu_file_path(filename) - # use default if no filename given - if not filename: - candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu" - filename = _get_menu_file_path(candidate) - if not filename: - raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate) - # check if it is a .menu file - if not filename.endswith(".menu"): - raise ParsingError('Not a .menu file', filename) - # create xml parser - try: - tree = etree.parse(filename) - except: - raise ParsingError('Not a valid .menu file', filename) - - # parse menufile - self._merged_files = set() - self._directory_dirs = set() - self.cache = MenuEntryCache() - - menu = self.parse_menu(tree.getroot(), filename) - menu.tree = tree - menu.filename = filename - - self.handle_moves(menu) - self.post_parse(menu) - - # generate the menu - self.generate_not_only_allocated(menu) - self.generate_only_allocated(menu) - - # and finally sort - menu.sort() - - return menu - - def parse_menu(self, node, filename): - menu = Menu() - self.parse_node(node, filename, menu) - return menu - - def parse_node(self, node, filename, parent=None): - num_children = len(node) - for child in node: - tag, text = child.tag, child.text - text = text.strip() if text else None - if tag == 'Menu': - menu = self.parse_menu(child, filename) - parent.addSubmenu(menu) - elif tag == 'AppDir' and text: - self.parse_app_dir(text, filename, parent) - elif tag == 'DefaultAppDirs': - self.parse_default_app_dir(filename, parent) - elif tag == 'DirectoryDir' and text: - self.parse_directory_dir(text, filename, parent) - elif tag == 'DefaultDirectoryDirs': - self.parse_default_directory_dir(filename, parent) - elif tag == 'Name' and text: - parent.Name = text - elif tag == 'Directory' and text: - parent.Directories.append(text) - elif tag == 'OnlyUnallocated': - parent.OnlyUnallocated = True - elif tag == 'NotOnlyUnallocated': - parent.OnlyUnallocated = False - elif tag == 'Deleted': - parent.Deleted = True - elif tag == 'NotDeleted': - parent.Deleted = False - elif tag == 'Include' or tag == 'Exclude': - parent.Rules.append(self.parse_rule(child)) - elif tag == 'MergeFile': - if child.attrib.get("type", None) == "parent": - self.parse_merge_file("applications.menu", child, filename, parent) - elif text: - self.parse_merge_file(text, child, filename, parent) - elif tag == 'MergeDir' and text: - self.parse_merge_dir(text, child, filename, parent) - elif tag == 'DefaultMergeDirs': - self.parse_default_merge_dirs(child, filename, parent) - elif tag == 'Move': - parent.Moves.append(self.parse_move(child)) - elif tag == 'Layout': - if num_children > 1: - parent.Layout = self.parse_layout(child) - elif tag == 'DefaultLayout': - if num_children > 1: - parent.DefaultLayout = self.parse_layout(child) - elif tag == 'LegacyDir' and text: - self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent) - elif tag == 'KDELegacyDirs': - self.parse_kde_legacy_dirs(filename, parent) - - def parse_layout(self, node): - layout = Layout( - show_empty=_to_bool(node.attrib.get("show_empty", False)), - inline=_to_bool(node.attrib.get("inline", False)), - inline_limit=int(node.attrib.get("inline_limit", 4)), - inline_header=_to_bool(node.attrib.get("inline_header", True)), - inline_alias=_to_bool(node.attrib.get("inline_alias", False)) - ) - order = [] - for child in node: - tag, text = child.tag, child.text - text = text.strip() if text else None - if tag == "Menuname" and text: - order.append([ - "Menuname", - text, - _to_bool(child.attrib.get("show_empty", False)), - _to_bool(child.attrib.get("inline", False)), - int(child.attrib.get("inline_limit", 4)), - _to_bool(child.attrib.get("inline_header", True)), - _to_bool(child.attrib.get("inline_alias", False)) - ]) - elif tag == "Separator": - order.append(['Separator']) - elif tag == "Filename" and text: - order.append(["Filename", text]) - elif tag == "Merge": - order.append([ - "Merge", - child.attrib.get("type", "all") - ]) - layout.order = order - return layout - - def parse_move(self, node): - old, new = "", "" - for child in node: - tag, text = child.tag, child.text - text = text.strip() if text else None - if tag == "Old" and text: - old = text - elif tag == "New" and text: - new = text - return Move(old, new) - - # ---------- parsing - - def parse_rule(self, node): - type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE - tree = ast.Expression(lineno=1, col_offset=0) - expr = self.parse_bool_op(node, ast.Or()) - if expr: - tree.body = expr - else: - tree.body = _ast_const('False') - ast.fix_missing_locations(tree) - return Rule(type, tree) - - def parse_bool_op(self, node, operator): - values = [] - for child in node: - rule = self.parse_rule_node(child) - if rule: - values.append(rule) - num_values = len(values) - if num_values > 1: - return ast.BoolOp(operator, values) - elif num_values == 1: - return values[0] - return None - - def parse_rule_node(self, node): - tag = node.tag - if tag == 'Or': - return self.parse_bool_op(node, ast.Or()) - elif tag == 'And': - return self.parse_bool_op(node, ast.And()) - elif tag == 'Not': - expr = self.parse_bool_op(node, ast.Or()) - return ast.UnaryOp(ast.Not(), expr) if expr else None - elif tag == 'All': - return _ast_const('True') - elif tag == 'Category': - category = node.text - return ast.Compare( - left=ast.Str(category), - ops=[ast.In()], - comparators=[ast.Attribute( - value=ast.Name(id='menuentry', ctx=ast.Load()), - attr='Categories', - ctx=ast.Load() - )] - ) - elif tag == 'Filename': - filename = node.text - return ast.Compare( - left=ast.Str(filename), - ops=[ast.Eq()], - comparators=[ast.Attribute( - value=ast.Name(id='menuentry', ctx=ast.Load()), - attr='DesktopFileID', - ctx=ast.Load() - )] - ) - - # ---------- App/Directory Dir Stuff - - def parse_app_dir(self, value, filename, parent): - value = _check_file_path(value, filename, TYPE_DIR) - if value: - parent.AppDirs.append(value) - - def parse_default_app_dir(self, filename, parent): - for d in reversed(xdg_data_dirs): - self.parse_app_dir(os.path.join(d, "applications"), filename, parent) - - def parse_directory_dir(self, value, filename, parent): - value = _check_file_path(value, filename, TYPE_DIR) - if value: - parent.DirectoryDirs.append(value) - - def parse_default_directory_dir(self, filename, parent): - for d in reversed(xdg_data_dirs): - self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent) - - # ---------- Merge Stuff - - def parse_merge_file(self, value, child, filename, parent): - if child.attrib.get("type", None) == "parent": - for d in xdg_config_dirs: - rel_file = filename.replace(d, "").strip("/") - if rel_file != filename: - for p in xdg_config_dirs: - if d == p: - continue - if os.path.isfile(os.path.join(p, rel_file)): - self.merge_file(os.path.join(p, rel_file), child, parent) - break - else: - value = _check_file_path(value, filename, TYPE_FILE) - if value: - self.merge_file(value, child, parent) - - def parse_merge_dir(self, value, child, filename, parent): - value = _check_file_path(value, filename, TYPE_DIR) - if value: - for item in os.listdir(value): - try: - if item.endswith(".menu"): - self.merge_file(os.path.join(value, item), child, parent) - except UnicodeDecodeError: - continue - - def parse_default_merge_dirs(self, child, filename, parent): - basename = os.path.splitext(os.path.basename(filename))[0] - for d in reversed(xdg_config_dirs): - self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent) - - def merge_file(self, filename, child, parent): - # check for infinite loops - if filename in self._merged_files: - if self.debug: - raise ParsingError('Infinite MergeFile loop detected', filename) - else: - return - self._merged_files.add(filename) - # load file - try: - tree = etree.parse(filename) - except IOError: - if self.debug: - raise ParsingError('File not found', filename) - else: - return - except: - if self.debug: - raise ParsingError('Not a valid .menu file', filename) - else: - return - root = tree.getroot() - self.parse_node(root, filename, parent) - - # ---------- Legacy Dir Stuff - - def parse_legacy_dir(self, dir_, prefix, filename, parent): - m = self.merge_legacy_dir(dir_, prefix, filename, parent) - if m: - parent += m - - def merge_legacy_dir(self, dir_, prefix, filename, parent): - dir_ = _check_file_path(dir_, filename, TYPE_DIR) - if dir_ and dir_ not in self._directory_dirs: - self._directory_dirs.add(dir_) - m = Menu() - m.AppDirs.append(dir_) - m.DirectoryDirs.append(dir_) - m.Name = os.path.basename(dir_) - m.NotInXml = True - - for item in os.listdir(dir_): - try: - if item == ".directory": - m.Directories.append(item) - elif os.path.isdir(os.path.join(dir_, item)): - m.addSubmenu(self.merge_legacy_dir( - os.path.join(dir_, item), - prefix, - filename, - parent - )) - except UnicodeDecodeError: - continue - - self.cache.add_menu_entries([dir_], prefix, True) - menuentries = self.cache.get_menu_entries([dir_], False) - - for menuentry in menuentries: - categories = menuentry.Categories - if len(categories) == 0: - r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID) - m.Rules.append(r) - if not dir_ in parent.AppDirs: - categories.append("Legacy") - menuentry.Categories = categories - - return m - - def parse_kde_legacy_dirs(self, filename, parent): - try: - proc = subprocess.Popen( - ['kde-config', '--path', 'apps'], - stdout=subprocess.PIPE, - universal_newlines=True - ) - output = proc.communicate()[0].splitlines() - except OSError: - # If kde-config doesn't exist, ignore this. - return - try: - for dir_ in output[0].split(":"): - self.parse_legacy_dir(dir_, "kde", filename, parent) - except IndexError: - pass - - def post_parse(self, menu): - # unallocated / deleted - if menu.Deleted is None: - menu.Deleted = False - if menu.OnlyUnallocated is None: - menu.OnlyUnallocated = False - - # Layout Tags - if not menu.Layout or not menu.DefaultLayout: - if menu.DefaultLayout: - menu.Layout = menu.DefaultLayout - elif menu.Layout: - if menu.Depth > 0: - menu.DefaultLayout = menu.Parent.DefaultLayout - else: - menu.DefaultLayout = Layout() - else: - if menu.Depth > 0: - menu.Layout = menu.Parent.DefaultLayout - menu.DefaultLayout = menu.Parent.DefaultLayout - else: - menu.Layout = Layout() - menu.DefaultLayout = Layout() - - # add parent's app/directory dirs - if menu.Depth > 0: - menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs - menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs - - # remove duplicates - menu.Directories = _dedupe(menu.Directories) - menu.DirectoryDirs = _dedupe(menu.DirectoryDirs) - menu.AppDirs = _dedupe(menu.AppDirs) - - # go recursive through all menus - for submenu in menu.Submenus: - self.post_parse(submenu) - - # reverse so handling is easier - menu.Directories.reverse() - menu.DirectoryDirs.reverse() - menu.AppDirs.reverse() - - # get the valid .directory file out of the list - for directory in menu.Directories: - for dir in menu.DirectoryDirs: - if os.path.isfile(os.path.join(dir, directory)): - menuentry = MenuEntry(directory, dir) - if not menu.Directory: - menu.Directory = menuentry - elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: - if menu.Directory.getType() == MenuEntry.TYPE_USER: - menu.Directory.Original = menuentry - if menu.Directory: - break - - # Finally generate the menu - def generate_not_only_allocated(self, menu): - for submenu in menu.Submenus: - self.generate_not_only_allocated(submenu) - - if menu.OnlyUnallocated is False: - self.cache.add_menu_entries(menu.AppDirs) - menuentries = [] - for rule in menu.Rules: - menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1) - - for menuentry in menuentries: - if menuentry.Add is True: - menuentry.Parents.append(menu) - menuentry.Add = False - menuentry.Allocated = True - menu.MenuEntries.append(menuentry) - - def generate_only_allocated(self, menu): - for submenu in menu.Submenus: - self.generate_only_allocated(submenu) - - if menu.OnlyUnallocated is True: - self.cache.add_menu_entries(menu.AppDirs) - menuentries = [] - for rule in menu.Rules: - menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2) - for menuentry in menuentries: - if menuentry.Add is True: - menuentry.Parents.append(menu) - # menuentry.Add = False - # menuentry.Allocated = True - menu.MenuEntries.append(menuentry) - - def handle_moves(self, menu): - for submenu in menu.Submenus: - self.handle_moves(submenu) - # parse move operations - for move in menu.Moves: - move_from_menu = menu.getMenu(move.Old) - if move_from_menu: - # FIXME: this is assigned, but never used... - move_to_menu = menu.getMenu(move.New) - - menus = move.New.split("/") - oldparent = None - while len(menus) > 0: - if not oldparent: - oldparent = menu - newmenu = oldparent.getMenu(menus[0]) - if not newmenu: - newmenu = Menu() - newmenu.Name = menus[0] - if len(menus) > 1: - newmenu.NotInXml = True - oldparent.addSubmenu(newmenu) - oldparent = newmenu - menus.pop(0) - - newmenu += move_from_menu - move_from_menu.Parent.Submenus.remove(move_from_menu) - - -class MenuEntryCache: - "Class to cache Desktop Entries" - def __init__(self): - self.cacheEntries = {} - self.cacheEntries['legacy'] = [] - self.cache = {} - - def add_menu_entries(self, dirs, prefix="", legacy=False): - for dir_ in dirs: - if not dir_ in self.cacheEntries: - self.cacheEntries[dir_] = [] - self.__addFiles(dir_, "", prefix, legacy) - - def __addFiles(self, dir_, subdir, prefix, legacy): - for item in os.listdir(os.path.join(dir_, subdir)): - if item.endswith(".desktop"): - try: - menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix) - except ParsingError: - continue - - self.cacheEntries[dir_].append(menuentry) - if legacy: - self.cacheEntries['legacy'].append(menuentry) - elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy: - self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy) - - def get_menu_entries(self, dirs, legacy=True): - entries = [] - ids = set() - # handle legacy items - appdirs = dirs[:] - if legacy: - appdirs.append("legacy") - # cache the results again - key = "".join(appdirs) - try: - return self.cache[key] - except KeyError: - pass - for dir_ in appdirs: - for menuentry in self.cacheEntries[dir_]: - try: - if menuentry.DesktopFileID not in ids: - ids.add(menuentry.DesktopFileID) - entries.append(menuentry) - elif menuentry.getType() == MenuEntry.TYPE_SYSTEM: - # FIXME: This is only 99% correct, but still... - idx = entries.index(menuentry) - entry = entries[idx] - if entry.getType() == MenuEntry.TYPE_USER: - entry.Original = menuentry - except UnicodeDecodeError: - continue - self.cache[key] = entries - return entries - - -def parse(filename=None, debug=False): - """Helper function. - Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename) - """ - return XMLMenuBuilder(debug).parse(filename) diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/MenuEditor.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/MenuEditor.py deleted file mode 100644 index ee880f4..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/MenuEditor.py +++ /dev/null @@ -1,541 +0,0 @@ -""" CLass to edit XDG Menus """ -import os -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree - -from xdg.Menu import Menu, MenuEntry, Layout, Separator, XMLMenuBuilder -from xdg.BaseDirectory import xdg_config_dirs, xdg_data_dirs -from xdg.Exceptions import ParsingError -from xdg.Config import setRootMode - -# XML-Cleanups: Move / Exclude -# FIXME: proper reverte/delete -# FIXME: pass AppDirs/DirectoryDirs around in the edit/move functions -# FIXME: catch Exceptions -# FIXME: copy functions -# FIXME: More Layout stuff -# FIXME: unod/redo function / remove menu... -# FIXME: Advanced MenuEditing Stuff: LegacyDir/MergeFile -# Complex Rules/Deleted/OnlyAllocated/AppDirs/DirectoryDirs - - -class MenuEditor(object): - - def __init__(self, menu=None, filename=None, root=False): - self.menu = None - self.filename = None - self.tree = None - self.parser = XMLMenuBuilder() - self.parse(menu, filename, root) - - # fix for creating two menus with the same name on the fly - self.filenames = [] - - def parse(self, menu=None, filename=None, root=False): - if root: - setRootMode(True) - - if isinstance(menu, Menu): - self.menu = menu - elif menu: - self.menu = self.parser.parse(menu) - else: - self.menu = self.parser.parse() - - if root: - self.filename = self.menu.Filename - elif filename: - self.filename = filename - else: - self.filename = os.path.join(xdg_config_dirs[0], "menus", os.path.split(self.menu.Filename)[1]) - - try: - self.tree = etree.parse(self.filename) - except IOError: - root = etree.fromstring(""" - - - Applications - %s - -""" % self.menu.Filename) - self.tree = etree.ElementTree(root) - except ParsingError: - raise ParsingError('Not a valid .menu file', self.filename) - - #FIXME: is this needed with etree ? - self.__remove_whitespace_nodes(self.tree) - - def save(self): - self.__saveEntries(self.menu) - self.__saveMenu() - - def createMenuEntry(self, parent, name, command=None, genericname=None, comment=None, icon=None, terminal=None, after=None, before=None): - menuentry = MenuEntry(self.__getFileName(name, ".desktop")) - menuentry = self.editMenuEntry(menuentry, name, genericname, comment, command, icon, terminal) - - self.__addEntry(parent, menuentry, after, before) - - self.menu.sort() - - return menuentry - - def createMenu(self, parent, name, genericname=None, comment=None, icon=None, after=None, before=None): - menu = Menu() - - menu.Parent = parent - menu.Depth = parent.Depth + 1 - menu.Layout = parent.DefaultLayout - menu.DefaultLayout = parent.DefaultLayout - - menu = self.editMenu(menu, name, genericname, comment, icon) - - self.__addEntry(parent, menu, after, before) - - self.menu.sort() - - return menu - - def createSeparator(self, parent, after=None, before=None): - separator = Separator(parent) - - self.__addEntry(parent, separator, after, before) - - self.menu.sort() - - return separator - - def moveMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): - self.__deleteEntry(oldparent, menuentry, after, before) - self.__addEntry(newparent, menuentry, after, before) - - self.menu.sort() - - return menuentry - - def moveMenu(self, menu, oldparent, newparent, after=None, before=None): - self.__deleteEntry(oldparent, menu, after, before) - self.__addEntry(newparent, menu, after, before) - - root_menu = self.__getXmlMenu(self.menu.Name) - if oldparent.getPath(True) != newparent.getPath(True): - self.__addXmlMove(root_menu, os.path.join(oldparent.getPath(True), menu.Name), os.path.join(newparent.getPath(True), menu.Name)) - - self.menu.sort() - - return menu - - def moveSeparator(self, separator, parent, after=None, before=None): - self.__deleteEntry(parent, separator, after, before) - self.__addEntry(parent, separator, after, before) - - self.menu.sort() - - return separator - - def copyMenuEntry(self, menuentry, oldparent, newparent, after=None, before=None): - self.__addEntry(newparent, menuentry, after, before) - - self.menu.sort() - - return menuentry - - def editMenuEntry(self, menuentry, name=None, genericname=None, comment=None, command=None, icon=None, terminal=None, nodisplay=None, hidden=None): - deskentry = menuentry.DesktopEntry - - if name: - if not deskentry.hasKey("Name"): - deskentry.set("Name", name) - deskentry.set("Name", name, locale=True) - if comment: - if not deskentry.hasKey("Comment"): - deskentry.set("Comment", comment) - deskentry.set("Comment", comment, locale=True) - if genericname: - if not deskentry.hasKey("GenericName"): - deskentry.set("GenericName", genericname) - deskentry.set("GenericName", genericname, locale=True) - if command: - deskentry.set("Exec", command) - if icon: - deskentry.set("Icon", icon) - - if terminal: - deskentry.set("Terminal", "true") - elif not terminal: - deskentry.set("Terminal", "false") - - if nodisplay is True: - deskentry.set("NoDisplay", "true") - elif nodisplay is False: - deskentry.set("NoDisplay", "false") - - if hidden is True: - deskentry.set("Hidden", "true") - elif hidden is False: - deskentry.set("Hidden", "false") - - menuentry.updateAttributes() - - if len(menuentry.Parents) > 0: - self.menu.sort() - - return menuentry - - def editMenu(self, menu, name=None, genericname=None, comment=None, icon=None, nodisplay=None, hidden=None): - # Hack for legacy dirs - if isinstance(menu.Directory, MenuEntry) and menu.Directory.Filename == ".directory": - xml_menu = self.__getXmlMenu(menu.getPath(True, True)) - self.__addXmlTextElement(xml_menu, 'Directory', menu.Name + ".directory") - menu.Directory.setAttributes(menu.Name + ".directory") - # Hack for New Entries - elif not isinstance(menu.Directory, MenuEntry): - if not name: - name = menu.Name - filename = self.__getFileName(name, ".directory").replace("/", "") - if not menu.Name: - menu.Name = filename.replace(".directory", "") - xml_menu = self.__getXmlMenu(menu.getPath(True, True)) - self.__addXmlTextElement(xml_menu, 'Directory', filename) - menu.Directory = MenuEntry(filename) - - deskentry = menu.Directory.DesktopEntry - - if name: - if not deskentry.hasKey("Name"): - deskentry.set("Name", name) - deskentry.set("Name", name, locale=True) - if genericname: - if not deskentry.hasKey("GenericName"): - deskentry.set("GenericName", genericname) - deskentry.set("GenericName", genericname, locale=True) - if comment: - if not deskentry.hasKey("Comment"): - deskentry.set("Comment", comment) - deskentry.set("Comment", comment, locale=True) - if icon: - deskentry.set("Icon", icon) - - if nodisplay is True: - deskentry.set("NoDisplay", "true") - elif nodisplay is False: - deskentry.set("NoDisplay", "false") - - if hidden is True: - deskentry.set("Hidden", "true") - elif hidden is False: - deskentry.set("Hidden", "false") - - menu.Directory.updateAttributes() - - if isinstance(menu.Parent, Menu): - self.menu.sort() - - return menu - - def hideMenuEntry(self, menuentry): - self.editMenuEntry(menuentry, nodisplay=True) - - def unhideMenuEntry(self, menuentry): - self.editMenuEntry(menuentry, nodisplay=False, hidden=False) - - def hideMenu(self, menu): - self.editMenu(menu, nodisplay=True) - - def unhideMenu(self, menu): - self.editMenu(menu, nodisplay=False, hidden=False) - xml_menu = self.__getXmlMenu(menu.getPath(True, True), False) - deleted = xml_menu.findall('Deleted') - not_deleted = xml_menu.findall('NotDeleted') - for node in deleted + not_deleted: - xml_menu.remove(node) - - def deleteMenuEntry(self, menuentry): - if self.getAction(menuentry) == "delete": - self.__deleteFile(menuentry.DesktopEntry.filename) - for parent in menuentry.Parents: - self.__deleteEntry(parent, menuentry) - self.menu.sort() - return menuentry - - def revertMenuEntry(self, menuentry): - if self.getAction(menuentry) == "revert": - self.__deleteFile(menuentry.DesktopEntry.filename) - menuentry.Original.Parents = [] - for parent in menuentry.Parents: - index = parent.Entries.index(menuentry) - parent.Entries[index] = menuentry.Original - index = parent.MenuEntries.index(menuentry) - parent.MenuEntries[index] = menuentry.Original - menuentry.Original.Parents.append(parent) - self.menu.sort() - return menuentry - - def deleteMenu(self, menu): - if self.getAction(menu) == "delete": - self.__deleteFile(menu.Directory.DesktopEntry.filename) - self.__deleteEntry(menu.Parent, menu) - xml_menu = self.__getXmlMenu(menu.getPath(True, True)) - parent = self.__get_parent_node(xml_menu) - parent.remove(xml_menu) - self.menu.sort() - return menu - - def revertMenu(self, menu): - if self.getAction(menu) == "revert": - self.__deleteFile(menu.Directory.DesktopEntry.filename) - menu.Directory = menu.Directory.Original - self.menu.sort() - return menu - - def deleteSeparator(self, separator): - self.__deleteEntry(separator.Parent, separator, after=True) - - self.menu.sort() - - return separator - - """ Private Stuff """ - def getAction(self, entry): - if isinstance(entry, Menu): - if not isinstance(entry.Directory, MenuEntry): - return "none" - elif entry.Directory.getType() == "Both": - return "revert" - elif entry.Directory.getType() == "User" and ( - len(entry.Submenus) + len(entry.MenuEntries) - ) == 0: - return "delete" - - elif isinstance(entry, MenuEntry): - if entry.getType() == "Both": - return "revert" - elif entry.getType() == "User": - return "delete" - else: - return "none" - - return "none" - - def __saveEntries(self, menu): - if not menu: - menu = self.menu - if isinstance(menu.Directory, MenuEntry): - menu.Directory.save() - for entry in menu.getEntries(hidden=True): - if isinstance(entry, MenuEntry): - entry.save() - elif isinstance(entry, Menu): - self.__saveEntries(entry) - - def __saveMenu(self): - if not os.path.isdir(os.path.dirname(self.filename)): - os.makedirs(os.path.dirname(self.filename)) - self.tree.write(self.filename, encoding='utf-8') - - def __getFileName(self, name, extension): - postfix = 0 - while 1: - if postfix == 0: - filename = name + extension - else: - filename = name + "-" + str(postfix) + extension - if extension == ".desktop": - dir = "applications" - elif extension == ".directory": - dir = "desktop-directories" - if not filename in self.filenames and not os.path.isfile( - os.path.join(xdg_data_dirs[0], dir, filename) - ): - self.filenames.append(filename) - break - else: - postfix += 1 - - return filename - - def __getXmlMenu(self, path, create=True, element=None): - # FIXME: we should also return the menu's parent, - # to avoid looking for it later on - # @see Element.getiterator() - if not element: - element = self.tree - - if "/" in path: - (name, path) = path.split("/", 1) - else: - name = path - path = "" - - found = None - for node in element.findall("Menu"): - name_node = node.find('Name') - if name_node.text == name: - if path: - found = self.__getXmlMenu(path, create, node) - else: - found = node - if found: - break - if not found and create: - node = self.__addXmlMenuElement(element, name) - if path: - found = self.__getXmlMenu(path, create, node) - else: - found = node - - return found - - def __addXmlMenuElement(self, element, name): - menu_node = etree.SubElement('Menu', element) - name_node = etree.SubElement('Name', menu_node) - name_node.text = name - return menu_node - - def __addXmlTextElement(self, element, name, text): - node = etree.SubElement(name, element) - node.text = text - return node - - def __addXmlFilename(self, element, filename, type_="Include"): - # remove old filenames - includes = element.findall('Include') - excludes = element.findall('Exclude') - rules = includes + excludes - for rule in rules: - #FIXME: this finds only Rules whose FIRST child is a Filename element - if rule[0].tag == "Filename" and rule[0].text == filename: - element.remove(rule) - # shouldn't it remove all occurences, like the following: - #filename_nodes = rule.findall('.//Filename'): - #for fn in filename_nodes: - #if fn.text == filename: - ##element.remove(rule) - #parent = self.__get_parent_node(fn) - #parent.remove(fn) - - # add new filename - node = etree.SubElement(type_, element) - self.__addXmlTextElement(node, 'Filename', filename) - return node - - def __addXmlMove(self, element, old, new): - node = etree.SubElement("Move", element) - self.__addXmlTextElement(node, 'Old', old) - self.__addXmlTextElement(node, 'New', new) - return node - - def __addXmlLayout(self, element, layout): - # remove old layout - for node in element.findall("Layout"): - element.remove(node) - - # add new layout - node = etree.SubElement("Layout", element) - for order in layout.order: - if order[0] == "Separator": - child = etree.SubElement("Separator", node) - elif order[0] == "Filename": - child = self.__addXmlTextElement(node, "Filename", order[1]) - elif order[0] == "Menuname": - child = self.__addXmlTextElement(node, "Menuname", order[1]) - elif order[0] == "Merge": - child = etree.SubElement("Merge", node) - child.attrib["type"] = order[1] - return node - - def __addLayout(self, parent): - layout = Layout() - layout.order = [] - layout.show_empty = parent.Layout.show_empty - layout.inline = parent.Layout.inline - layout.inline_header = parent.Layout.inline_header - layout.inline_alias = parent.Layout.inline_alias - layout.inline_limit = parent.Layout.inline_limit - - layout.order.append(["Merge", "menus"]) - for entry in parent.Entries: - if isinstance(entry, Menu): - layout.parseMenuname(entry.Name) - elif isinstance(entry, MenuEntry): - layout.parseFilename(entry.DesktopFileID) - elif isinstance(entry, Separator): - layout.parseSeparator() - layout.order.append(["Merge", "files"]) - - parent.Layout = layout - - return layout - - def __addEntry(self, parent, entry, after=None, before=None): - if after or before: - if after: - index = parent.Entries.index(after) + 1 - elif before: - index = parent.Entries.index(before) - parent.Entries.insert(index, entry) - else: - parent.Entries.append(entry) - - xml_parent = self.__getXmlMenu(parent.getPath(True, True)) - - if isinstance(entry, MenuEntry): - parent.MenuEntries.append(entry) - entry.Parents.append(parent) - self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Include") - elif isinstance(entry, Menu): - parent.addSubmenu(entry) - - if after or before: - self.__addLayout(parent) - self.__addXmlLayout(xml_parent, parent.Layout) - - def __deleteEntry(self, parent, entry, after=None, before=None): - parent.Entries.remove(entry) - - xml_parent = self.__getXmlMenu(parent.getPath(True, True)) - - if isinstance(entry, MenuEntry): - entry.Parents.remove(parent) - parent.MenuEntries.remove(entry) - self.__addXmlFilename(xml_parent, entry.DesktopFileID, "Exclude") - elif isinstance(entry, Menu): - parent.Submenus.remove(entry) - - if after or before: - self.__addLayout(parent) - self.__addXmlLayout(xml_parent, parent.Layout) - - def __deleteFile(self, filename): - try: - os.remove(filename) - except OSError: - pass - try: - self.filenames.remove(filename) - except ValueError: - pass - - def __remove_whitespace_nodes(self, node): - for child in node: - text = child.text.strip() - if not text: - child.text = '' - tail = child.tail.strip() - if not tail: - child.tail = '' - if len(child): - self.__remove_whilespace_nodes(child) - - def __get_parent_node(self, node): - # elements in ElementTree doesn't hold a reference to their parent - for parent, child in self.__iter_parent(): - if child is node: - return child - - def __iter_parent(self): - for parent in self.tree.getiterator(): - for child in parent: - yield parent, child diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Mime.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/Mime.py deleted file mode 100644 index 886cb42..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/Mime.py +++ /dev/null @@ -1,784 +0,0 @@ -""" -This module is based on a rox module (LGPL): - -http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log - -This module provides access to the shared MIME database. - -types is a dictionary of all known MIME types, indexed by the type name, e.g. -types['application/x-python'] - -Applications can install information about MIME types by storing an -XML file as /packages/.xml and running the -update-mime-database command, which is provided by the freedesktop.org -shared mime database package. - -See http://www.freedesktop.org/standards/shared-mime-info-spec/ for -information about the format of these files. - -(based on version 0.13) -""" - -import os -import re -import stat -import sys -import fnmatch - -from xdg import BaseDirectory -import xdg.Locale - -from xml.dom import minidom, XML_NAMESPACE -from collections import defaultdict - -FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info' - -types = {} # Maps MIME names to type objects - -exts = None # Maps extensions to types -globs = None # List of (glob, type) pairs -literals = None # Maps liternal names to types -magic = None - -PY3 = (sys.version_info[0] >= 3) - -def _get_node_data(node): - """Get text of XML node""" - return ''.join([n.nodeValue for n in node.childNodes]).strip() - -def lookup(media, subtype = None): - """Get the MIMEtype object for the given type. - - This remains for backwards compatibility; calling MIMEtype now does - the same thing. - - The name can either be passed as one part ('text/plain'), or as two - ('text', 'plain'). - """ - return MIMEtype(media, subtype) - -class MIMEtype(object): - """Class holding data about a MIME type. - - Calling the class will return a cached instance, so there is only one - instance for each MIME type. The name can either be passed as one part - ('text/plain'), or as two ('text', 'plain'). - """ - def __new__(cls, media, subtype=None): - if subtype is None and '/' in media: - media, subtype = media.split('/', 1) - assert '/' not in subtype - media = media.lower() - subtype = subtype.lower() - - try: - return types[(media, subtype)] - except KeyError: - mtype = super(MIMEtype, cls).__new__(cls) - mtype._init(media, subtype) - types[(media, subtype)] = mtype - return mtype - - # If this is done in __init__, it is automatically called again each time - # the MIMEtype is returned by __new__, which we don't want. So we call it - # explicitly only when we construct a new instance. - def _init(self, media, subtype): - self.media = media - self.subtype = subtype - self._comment = None - - def _load(self): - "Loads comment for current language. Use get_comment() instead." - resource = os.path.join('mime', self.media, self.subtype + '.xml') - for path in BaseDirectory.load_data_paths(resource): - doc = minidom.parse(path) - if doc is None: - continue - for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'): - lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en' - goodness = 1 + (lang in xdg.Locale.langs) - if goodness > self._comment[0]: - self._comment = (goodness, _get_node_data(comment)) - if goodness == 2: return - - # FIXME: add get_icon method - def get_comment(self): - """Returns comment for current language, loading it if needed.""" - # Should we ever reload? - if self._comment is None: - self._comment = (0, str(self)) - self._load() - return self._comment[1] - - def canonical(self): - """Returns the canonical MimeType object if this is an alias.""" - update_cache() - s = str(self) - if s in aliases: - return lookup(aliases[s]) - return self - - def inherits_from(self): - """Returns a set of Mime types which this inherits from.""" - update_cache() - return set(lookup(t) for t in inheritance[str(self)]) - - def __str__(self): - return self.media + '/' + self.subtype - - def __repr__(self): - return 'MIMEtype(%r, %r)' % (self.media, self.subtype) - - def __hash__(self): - return hash(self.media) ^ hash(self.subtype) - -class UnknownMagicRuleFormat(ValueError): - pass - -class DiscardMagicRules(Exception): - "Raised when __NOMAGIC__ is found, and caught to discard previous rules." - pass - -class MagicRule: - also = None - - def __init__(self, start, value, mask, word, range): - self.start = start - self.value = value - self.mask = mask - self.word = word - self.range = range - - rule_ending_re = re.compile(br'(?:~(\d+))?(?:\+(\d+))?\n$') - - @classmethod - def from_file(cls, f): - """Read a rule from the binary magics file. Returns a 2-tuple of - the nesting depth and the MagicRule.""" - line = f.readline() - #print line - - # [indent] '>' - nest_depth, line = line.split(b'>', 1) - nest_depth = int(nest_depth) if nest_depth else 0 - - # start-offset '=' - start, line = line.split(b'=', 1) - start = int(start) - - if line == b'__NOMAGIC__\n': - raise DiscardMagicRules - - # value length (2 bytes, big endian) - if sys.version_info[0] >= 3: - lenvalue = int.from_bytes(line[:2], byteorder='big') - else: - lenvalue = (ord(line[0])<<8)+ord(line[1]) - line = line[2:] - - # value - # This can contain newlines, so we may need to read more lines - while len(line) <= lenvalue: - line += f.readline() - value, line = line[:lenvalue], line[lenvalue:] - - # ['&' mask] - if line.startswith(b'&'): - # This can contain newlines, so we may need to read more lines - while len(line) <= lenvalue: - line += f.readline() - mask, line = line[1:lenvalue+1], line[lenvalue+1:] - else: - mask = None - - # ['~' word-size] ['+' range-length] - ending = cls.rule_ending_re.match(line) - if not ending: - # Per the spec, this will be caught and ignored, to allow - # for future extensions. - raise UnknownMagicRuleFormat(repr(line)) - - word, range = ending.groups() - word = int(word) if (word is not None) else 1 - range = int(range) if (range is not None) else 1 - - return nest_depth, cls(start, value, mask, word, range) - - def maxlen(self): - l = self.start + len(self.value) + self.range - if self.also: - return max(l, self.also.maxlen()) - return l - - def match(self, buffer): - if self.match0(buffer): - if self.also: - return self.also.match(buffer) - return True - - def match0(self, buffer): - l=len(buffer) - lenvalue = len(self.value) - for o in range(self.range): - s=self.start+o - e=s+lenvalue - if l [(priority, rule), ...] - - def merge_file(self, fname): - """Read a magic binary file, and add its rules to this MagicDB.""" - with open(fname, 'rb') as f: - line = f.readline() - if line != b'MIME-Magic\0\n': - raise IOError('Not a MIME magic file') - - while True: - shead = f.readline().decode('ascii') - #print(shead) - if not shead: - break - if shead[0] != '[' or shead[-2:] != ']\n': - raise ValueError('Malformed section heading', shead) - pri, tname = shead[1:-2].split(':') - #print shead[1:-2] - pri = int(pri) - mtype = lookup(tname) - try: - rule = MagicMatchAny.from_file(f) - except DiscardMagicRules: - self.bytype.pop(mtype, None) - rule = MagicMatchAny.from_file(f) - if rule is None: - continue - #print rule - - self.bytype[mtype].append((pri, rule)) - - def finalise(self): - """Prepare the MagicDB for matching. - - This should be called after all rules have been merged into it. - """ - maxlen = 0 - self.alltypes = [] # (priority, mimetype, rule) - - for mtype, rules in self.bytype.items(): - for pri, rule in rules: - self.alltypes.append((pri, mtype, rule)) - maxlen = max(maxlen, rule.maxlen()) - - self.maxlen = maxlen # Number of bytes to read from files - self.alltypes.sort(key=lambda x: x[0], reverse=True) - - def match_data(self, data, max_pri=100, min_pri=0, possible=None): - """Do magic sniffing on some bytes. - - max_pri & min_pri can be used to specify the maximum & minimum priority - rules to look for. possible can be a list of mimetypes to check, or None - (the default) to check all mimetypes until one matches. - - Returns the MIMEtype found, or None if no entries match. - """ - if possible is not None: - types = [] - for mt in possible: - for pri, rule in self.bytype[mt]: - types.append((pri, mt, rule)) - types.sort(key=lambda x: x[0]) - else: - types = self.alltypes - - for priority, mimetype, rule in types: - #print priority, max_pri, min_pri - if priority > max_pri: - continue - if priority < min_pri: - break - - if rule.match(data): - return mimetype - - def match(self, path, max_pri=100, min_pri=0, possible=None): - """Read data from the file and do magic sniffing on it. - - max_pri & min_pri can be used to specify the maximum & minimum priority - rules to look for. possible can be a list of mimetypes to check, or None - (the default) to check all mimetypes until one matches. - - Returns the MIMEtype found, or None if no entries match. Raises IOError - if the file can't be opened. - """ - with open(path, 'rb') as f: - buf = f.read(self.maxlen) - return self.match_data(buf, max_pri, min_pri, possible) - - def __repr__(self): - return '' % len(self.alltypes) - -class GlobDB(object): - def __init__(self): - """Prepare the GlobDB. It can't actually be used until .finalise() is - called, but merge_file() can be used to add data before that. - """ - # Maps mimetype to {(weight, glob, flags), ...} - self.allglobs = defaultdict(set) - - def merge_file(self, path): - """Loads name matching information from a globs2 file."""# - allglobs = self.allglobs - with open(path) as f: - for line in f: - if line.startswith('#'): continue # Comment - - fields = line[:-1].split(':') - weight, type_name, pattern = fields[:3] - weight = int(weight) - mtype = lookup(type_name) - if len(fields) > 3: - flags = fields[3].split(',') - else: - flags = () - - if pattern == '__NOGLOBS__': - # This signals to discard any previous globs - allglobs.pop(mtype, None) - continue - - allglobs[mtype].add((weight, pattern, tuple(flags))) - - def finalise(self): - """Prepare the GlobDB for matching. - - This should be called after all files have been merged into it. - """ - self.exts = defaultdict(list) # Maps extensions to [(type, weight),...] - self.cased_exts = defaultdict(list) - self.globs = [] # List of (regex, type, weight) triplets - self.literals = {} # Maps literal names to (type, weight) - self.cased_literals = {} - - for mtype, globs in self.allglobs.items(): - mtype = mtype.canonical() - for weight, pattern, flags in globs: - - cased = 'cs' in flags - - if pattern.startswith('*.'): - # *.foo -- extension pattern - rest = pattern[2:] - if not ('*' in rest or '[' in rest or '?' in rest): - if cased: - self.cased_exts[rest].append((mtype, weight)) - else: - self.exts[rest.lower()].append((mtype, weight)) - continue - - if ('*' in pattern or '[' in pattern or '?' in pattern): - # Translate the glob pattern to a regex & compile it - re_flags = 0 if cased else re.I - pattern = re.compile(fnmatch.translate(pattern), flags=re_flags) - self.globs.append((pattern, mtype, weight)) - else: - # No wildcards - literal pattern - if cased: - self.cased_literals[pattern] = (mtype, weight) - else: - self.literals[pattern.lower()] = (mtype, weight) - - # Sort globs by weight & length - self.globs.sort(reverse=True, key=lambda x: (x[2], len(x[0].pattern)) ) - - def first_match(self, path): - """Return the first match found for a given path, or None if no match - is found.""" - try: - return next(self._match_path(path))[0] - except StopIteration: - return None - - def all_matches(self, path): - """Return a list of (MIMEtype, glob weight) pairs for the path.""" - return list(self._match_path(path)) - - def _match_path(self, path): - """Yields pairs of (mimetype, glob weight).""" - leaf = os.path.basename(path) - - # Literals (no wildcards) - if leaf in self.cased_literals: - yield self.cased_literals[leaf] - - lleaf = leaf.lower() - if lleaf in self.literals: - yield self.literals[lleaf] - - # Extensions - ext = leaf - while 1: - p = ext.find('.') - if p < 0: break - ext = ext[p + 1:] - if ext in self.cased_exts: - for res in self.cased_exts[ext]: - yield res - ext = lleaf - while 1: - p = ext.find('.') - if p < 0: break - ext = ext[p+1:] - if ext in self.exts: - for res in self.exts[ext]: - yield res - - # Other globs - for (regex, mime_type, weight) in self.globs: - if regex.match(leaf): - yield (mime_type, weight) - -# Some well-known types -text = lookup('text', 'plain') -octet_stream = lookup('application', 'octet-stream') -inode_block = lookup('inode', 'blockdevice') -inode_char = lookup('inode', 'chardevice') -inode_dir = lookup('inode', 'directory') -inode_fifo = lookup('inode', 'fifo') -inode_socket = lookup('inode', 'socket') -inode_symlink = lookup('inode', 'symlink') -inode_door = lookup('inode', 'door') -app_exe = lookup('application', 'executable') - -_cache_uptodate = False - -def _cache_database(): - global globs, magic, aliases, inheritance, _cache_uptodate - - _cache_uptodate = True - - aliases = {} # Maps alias Mime types to canonical names - inheritance = defaultdict(set) # Maps to sets of parent mime types. - - # Load aliases - for path in BaseDirectory.load_data_paths(os.path.join('mime', 'aliases')): - with open(path, 'r') as f: - for line in f: - alias, canonical = line.strip().split(None, 1) - aliases[alias] = canonical - - # Load filename patterns (globs) - globs = GlobDB() - for path in BaseDirectory.load_data_paths(os.path.join('mime', 'globs2')): - globs.merge_file(path) - globs.finalise() - - # Load magic sniffing data - magic = MagicDB() - for path in BaseDirectory.load_data_paths(os.path.join('mime', 'magic')): - magic.merge_file(path) - magic.finalise() - - # Load subclasses - for path in BaseDirectory.load_data_paths(os.path.join('mime', 'subclasses')): - with open(path, 'r') as f: - for line in f: - sub, parent = line.strip().split(None, 1) - inheritance[sub].add(parent) - -def update_cache(): - if not _cache_uptodate: - _cache_database() - -def get_type_by_name(path): - """Returns type of file by its name, or None if not known""" - update_cache() - return globs.first_match(path) - -def get_type_by_contents(path, max_pri=100, min_pri=0): - """Returns type of file by its contents, or None if not known""" - update_cache() - - return magic.match(path, max_pri, min_pri) - -def get_type_by_data(data, max_pri=100, min_pri=0): - """Returns type of the data, which should be bytes.""" - update_cache() - - return magic.match_data(data, max_pri, min_pri) - -def _get_type_by_stat(st_mode): - """Match special filesystem objects to Mimetypes.""" - if stat.S_ISDIR(st_mode): return inode_dir - elif stat.S_ISCHR(st_mode): return inode_char - elif stat.S_ISBLK(st_mode): return inode_block - elif stat.S_ISFIFO(st_mode): return inode_fifo - elif stat.S_ISLNK(st_mode): return inode_symlink - elif stat.S_ISSOCK(st_mode): return inode_socket - return inode_door - -def get_type(path, follow=True, name_pri=100): - """Returns type of file indicated by path. - - This function is *deprecated* - :func:`get_type2` is more accurate. - - :param path: pathname to check (need not exist) - :param follow: when reading file, follow symbolic links - :param name_pri: Priority to do name matches. 100=override magic - - This tries to use the contents of the file, and falls back to the name. It - can also handle special filesystem objects like directories and sockets. - """ - update_cache() - - try: - if follow: - st = os.stat(path) - else: - st = os.lstat(path) - except: - t = get_type_by_name(path) - return t or text - - if stat.S_ISREG(st.st_mode): - # Regular file - t = get_type_by_contents(path, min_pri=name_pri) - if not t: t = get_type_by_name(path) - if not t: t = get_type_by_contents(path, max_pri=name_pri) - if t is None: - if stat.S_IMODE(st.st_mode) & 0o111: - return app_exe - else: - return text - return t - else: - return _get_type_by_stat(st.st_mode) - -def get_type2(path, follow=True): - """Find the MIMEtype of a file using the XDG recommended checking order. - - This first checks the filename, then uses file contents if the name doesn't - give an unambiguous MIMEtype. It can also handle special filesystem objects - like directories and sockets. - - :param path: file path to examine (need not exist) - :param follow: whether to follow symlinks - - :rtype: :class:`MIMEtype` - - .. versionadded:: 1.0 - """ - update_cache() - - try: - st = os.stat(path) if follow else os.lstat(path) - except OSError: - return get_type_by_name(path) or octet_stream - - if not stat.S_ISREG(st.st_mode): - # Special filesystem objects - return _get_type_by_stat(st.st_mode) - - mtypes = sorted(globs.all_matches(path), key=(lambda x: x[1]), reverse=True) - if mtypes: - max_weight = mtypes[0][1] - i = 1 - for mt, w in mtypes[1:]: - if w < max_weight: - break - i += 1 - mtypes = mtypes[:i] - if len(mtypes) == 1: - return mtypes[0][0] - - possible = [mt for mt,w in mtypes] - else: - possible = None # Try all magic matches - - try: - t = magic.match(path, possible=possible) - except IOError: - t = None - - if t: - return t - elif mtypes: - return mtypes[0][0] - elif stat.S_IMODE(st.st_mode) & 0o111: - return app_exe - else: - return text if is_text_file(path) else octet_stream - -def is_text_file(path): - """Guess whether a file contains text or binary data. - - Heuristic: binary if the first 32 bytes include ASCII control characters. - This rule may change in future versions. - - .. versionadded:: 1.0 - """ - try: - f = open(path, 'rb') - except IOError: - return False - - with f: - return _is_text(f.read(32)) - -if PY3: - def _is_text(data): - return not any(b <= 0x8 or 0xe <= b < 0x20 or b == 0x7f for b in data) -else: - def _is_text(data): - return not any(b <= '\x08' or '\x0e' <= b < '\x20' or b == '\x7f' \ - for b in data) - -_mime2ext_cache = None -_mime2ext_cache_uptodate = False - -def get_extensions(mimetype): - """Retrieve the set of filename extensions matching a given MIMEtype. - - Extensions are returned without a leading dot, e.g. 'py'. If no extensions - are registered for the MIMEtype, returns an empty set. - - The extensions are stored in a cache the first time this is called. - - .. versionadded:: 1.0 - """ - global _mime2ext_cache, _mime2ext_cache_uptodate - update_cache() - if not _mime2ext_cache_uptodate: - _mime2ext_cache = defaultdict(set) - for ext, mtypes in globs.exts.items(): - for mtype, prio in mtypes: - _mime2ext_cache[mtype].add(ext) - _mime2ext_cache_uptodate = True - - return _mime2ext_cache[mimetype] - - -def install_mime_info(application, package_file): - """Copy 'package_file' as ``~/.local/share/mime/packages/.xml.`` - If package_file is None, install ``/.xml``. - If already installed, does nothing. May overwrite an existing - file with the same name (if the contents are different)""" - application += '.xml' - - with open(package_file) as f: - new_data = f.read() - - # See if the file is already installed - package_dir = os.path.join('mime', 'packages') - resource = os.path.join(package_dir, application) - for x in BaseDirectory.load_data_paths(resource): - try: - with open(x) as f: - old_data = f.read() - except: - continue - if old_data == new_data: - return # Already installed - - global _cache_uptodate - _cache_uptodate = False - - # Not already installed; add a new copy - # Create the directory structure... - new_file = os.path.join(BaseDirectory.save_data_path(package_dir), application) - - # Write the file... - with open(new_file, 'w') as f: - f.write(new_data) - - # Update the database... - command = 'update-mime-database' - if os.spawnlp(os.P_WAIT, command, command, BaseDirectory.save_data_path('mime')): - os.unlink(new_file) - raise Exception("The '%s' command returned an error code!\n" \ - "Make sure you have the freedesktop.org shared MIME package:\n" \ - "http://standards.freedesktop.org/shared-mime-info/" % command) diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/RecentFiles.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/RecentFiles.py deleted file mode 100644 index 7ee7ee5..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/RecentFiles.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -Implementation of the XDG Recent File Storage Specification -http://standards.freedesktop.org/recent-file-spec -""" - -import xml.dom.minidom, xml.sax.saxutils -import os, time, fcntl -from xdg.Exceptions import ParsingError - -class RecentFiles: - def __init__(self): - self.RecentFiles = [] - self.filename = "" - - def parse(self, filename=None): - """Parse a list of recently used files. - - filename defaults to ``~/.recently-used``. - """ - if not filename: - filename = os.path.join(os.getenv("HOME"), ".recently-used") - - try: - doc = xml.dom.minidom.parse(filename) - except IOError: - raise ParsingError('File not found', filename) - except xml.parsers.expat.ExpatError: - raise ParsingError('Not a valid .menu file', filename) - - self.filename = filename - - for child in doc.childNodes: - if child.nodeType == xml.dom.Node.ELEMENT_NODE: - if child.tagName == "RecentFiles": - for recent in child.childNodes: - if recent.nodeType == xml.dom.Node.ELEMENT_NODE: - if recent.tagName == "RecentItem": - self.__parseRecentItem(recent) - - self.sort() - - def __parseRecentItem(self, item): - recent = RecentFile() - self.RecentFiles.append(recent) - - for attribute in item.childNodes: - if attribute.nodeType == xml.dom.Node.ELEMENT_NODE: - if attribute.tagName == "URI": - recent.URI = attribute.childNodes[0].nodeValue - elif attribute.tagName == "Mime-Type": - recent.MimeType = attribute.childNodes[0].nodeValue - elif attribute.tagName == "Timestamp": - recent.Timestamp = int(attribute.childNodes[0].nodeValue) - elif attribute.tagName == "Private": - recent.Prviate = True - elif attribute.tagName == "Groups": - - for group in attribute.childNodes: - if group.nodeType == xml.dom.Node.ELEMENT_NODE: - if group.tagName == "Group": - recent.Groups.append(group.childNodes[0].nodeValue) - - def write(self, filename=None): - """Write the list of recently used files to disk. - - If the instance is already associated with a file, filename can be - omitted to save it there again. - """ - if not filename and not self.filename: - raise ParsingError('File not found', filename) - elif not filename: - filename = self.filename - - with open(filename, "w") as f: - fcntl.lockf(f, fcntl.LOCK_EX) - f.write('\n') - f.write("\n") - - for r in self.RecentFiles: - f.write(" \n") - f.write(" %s\n" % xml.sax.saxutils.escape(r.URI)) - f.write(" %s\n" % r.MimeType) - f.write(" %s\n" % r.Timestamp) - if r.Private == True: - f.write(" \n") - if len(r.Groups) > 0: - f.write(" \n") - for group in r.Groups: - f.write(" %s\n" % group) - f.write(" \n") - f.write(" \n") - - f.write("\n") - fcntl.lockf(f, fcntl.LOCK_UN) - - def getFiles(self, mimetypes=None, groups=None, limit=0): - """Get a list of recently used files. - - The parameters can be used to filter by mime types, by group, or to - limit the number of items returned. By default, the entire list is - returned, except for items marked private. - """ - tmp = [] - i = 0 - for item in self.RecentFiles: - if groups: - for group in groups: - if group in item.Groups: - tmp.append(item) - i += 1 - elif mimetypes: - for mimetype in mimetypes: - if mimetype == item.MimeType: - tmp.append(item) - i += 1 - else: - if item.Private == False: - tmp.append(item) - i += 1 - if limit != 0 and i == limit: - break - - return tmp - - def addFile(self, item, mimetype, groups=None, private=False): - """Add a recently used file. - - item should be the URI of the file, typically starting with ``file:///``. - """ - # check if entry already there - if item in self.RecentFiles: - index = self.RecentFiles.index(item) - recent = self.RecentFiles[index] - else: - # delete if more then 500 files - if len(self.RecentFiles) == 500: - self.RecentFiles.pop() - # add entry - recent = RecentFile() - self.RecentFiles.append(recent) - - recent.URI = item - recent.MimeType = mimetype - recent.Timestamp = int(time.time()) - recent.Private = private - if groups: - recent.Groups = groups - - self.sort() - - def deleteFile(self, item): - """Remove a recently used file, by URI, from the list. - """ - if item in self.RecentFiles: - self.RecentFiles.remove(item) - - def sort(self): - self.RecentFiles.sort() - self.RecentFiles.reverse() - - -class RecentFile: - def __init__(self): - self.URI = "" - self.MimeType = "" - self.Timestamp = "" - self.Private = False - self.Groups = [] - - def __cmp__(self, other): - return cmp(self.Timestamp, other.Timestamp) - - def __lt__ (self, other): - return self.Timestamp < other.Timestamp - - def __eq__(self, other): - return self.URI == str(other) - - def __str__(self): - return self.URI diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/__init__.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/__init__.py deleted file mode 100644 index 725eb50..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -__all__ = [ "BaseDirectory", "DesktopEntry", "Menu", "Exceptions", "IniFile", "IconTheme", "Locale", "Config", "Mime", "RecentFiles", "MenuEditor" ] - -__version__ = "0.27" diff --git a/src/shellfm/windows/view/icons/mixins/xdg_0.27/util.py b/src/shellfm/windows/view/icons/mixins/xdg_0.27/util.py deleted file mode 100644 index 1637aa5..0000000 --- a/src/shellfm/windows/view/icons/mixins/xdg_0.27/util.py +++ /dev/null @@ -1,75 +0,0 @@ -import sys - -PY3 = sys.version_info[0] >= 3 - -if PY3: - def u(s): - return s -else: - # Unicode-like literals - def u(s): - return s.decode('utf-8') - -try: - # which() is available from Python 3.3 - from shutil import which -except ImportError: - import os - # This is a copy of which() from Python 3.3 - def which(cmd, mode=os.F_OK | os.X_OK, path=None): - """Given a command, mode, and a PATH string, return the path which - conforms to the given mode on the PATH, or None if there is no such - file. - - `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result - of os.environ.get("PATH"), or can be overridden with a custom search - path. - - """ - # Check that a given file can be accessed with the correct mode. - # Additionally check that `file` is not a directory, as on Windows - # directories pass the os.access check. - def _access_check(fn, mode): - return (os.path.exists(fn) and os.access(fn, mode) - and not os.path.isdir(fn)) - - # If we're given a path with a directory part, look it up directly rather - # than referring to PATH directories. This includes checking relative to the - # current directory, e.g. ./script - if os.path.dirname(cmd): - if _access_check(cmd, mode): - return cmd - return None - - path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) - - if sys.platform == "win32": - # The current directory takes precedence on Windows. - if not os.curdir in path: - path.insert(0, os.curdir) - - # PATHEXT is necessary to check on Windows. - pathext = os.environ.get("PATHEXT", "").split(os.pathsep) - # See if the given file matches any of the expected path extensions. - # This will allow us to short circuit when given "python.exe". - # If it does match, only test that one, otherwise we have to try - # others. - if any(cmd.lower().endswith(ext.lower()) for ext in pathext): - files = [cmd] - else: - files = [cmd + ext for ext in pathext] - else: - # On other platforms you don't have things like PATHEXT to tell you - # what file suffixes are executable, so just pass on cmd as-is. - files = [cmd] - - seen = set() - for dir in path: - normdir = os.path.normcase(dir) - if not normdir in seen: - seen.add(normdir) - for thefile in files: - name = os.path.join(dir, thefile) - if _access_check(name, mode): - return name - return None