diff --git a/src/calibre/gui2/open_with.py b/src/calibre/gui2/open_with.py index 7297a42a3c..a4a48002e8 100644 --- a/src/calibre/gui2/open_with.py +++ b/src/calibre/gui2/open_with.py @@ -6,23 +6,159 @@ from __future__ import (unicode_literals, division, absolute_import, __license__ = 'GPL v3' __copyright__ = '2015, Kovid Goyal ' -import os +import os, re, shlex, cPickle +from collections import defaultdict -from calibre import force_unicode, walk -from calibre.constants import iswindows, isosx, filesystem_encoding +from calibre import force_unicode, walk, guess_type, prints +from calibre.constants import iswindows, isosx, filesystem_encoding, cache_dir +from calibre.utils.localization import canonicalize_lang, get_lang if iswindows: pass elif isosx: pass else: + def parse_localized_key(key): + name, rest = key.partition('[')[0::2] + if not rest: + return name, None + rest = rest[:-1] + lang = re.split(r'[_.@]', rest)[0] + return name, canonicalize_lang(lang) + + def unquote_exec(val): + val = val.replace(r'\\', '\\') + return shlex.split(val) + def parse_desktop_file(path): + gpat = re.compile(r'^\[(.+?)\]\s*$') + kpat = re.compile(r'^([-a-zA-Z0-9\[\]@_.]+)\s*=\s*(.+)$') try: with open(path, 'rb') as f: - raw = f.read() - raw - except EnvironmentError: + raw = f.read().decode('utf-8') + except (EnvironmentError, UnicodeDecodeError): return + group = None + ans = {} + for line in raw.splitlines(): + m = gpat.match(line) + if m is not None: + if group == 'Desktop Entry': + break + group = m.group(1) + continue + if group == 'Desktop Entry': + m = kpat.match(line) + if m is not None: + k, v = m.group(1), m.group(2) + if k == 'Hidden' and v == 'true': + return + if k == 'Type' and v != 'Application': + return + if k == 'Exec': + cmdline = unquote_exec(v) + if cmdline and (not os.path.isabs(cmdline[0]) or os.access(cmdline[0], os.X_OK)): + ans[k] = cmdline + elif k == 'MimeType': + ans[k] = frozenset(x.strip() for x in v.split(';')) + elif k in {'Name', 'GenericName', 'Comment', 'Icon'} or '[' in k: + name, lang = parse_localized_key(k) + if name not in ans: + ans[name] = {} + ans[name][lang] = v + else: + ans[k] = v + if 'Exec' in ans and 'MimeType' in ans: + return ans + + icon_data = None + + def find_icons(): + global icon_data + if icon_data is not None: + return icon_data + base_dirs = [os.path.expanduser('~/.icons')] + [ + os.path.join(b, 'icons') for b in os.environ.get( + 'XDG_DATA_DIRS', '/usr/local/share:/usr/share').split(os.pathsep)] + [ + '/usr/share/pixmaps'] + ans = defaultdict(list) + sz_pat = re.compile(r'/((?:\d+x\d+)|scalable)/') + cache_file = os.path.join(cache_dir(), 'icon-theme-cache.pickle') + exts = {'.svg', '.png', '.xpm'} + + def read_icon_theme_dir(dirpath): + ans = defaultdict(list) + for path in walk(dirpath): + bn = os.path.basename(path) + name, ext = os.path.splitext(bn) + if ext in exts: + sz = sz_pat.findall(path) + if sz: + sz = sz[-1] + if sz == 'scalable': + sz = 100000 + else: + sz = int(sz.partition('x')[0]) + idx = len(ans[name]) + ans[name].append((-sz, idx, sz, path)) + for icons in ans.itervalues(): + icons.sort() + return {k:(-v[0][2], v[0][3]) for k, v in ans.iteritems()} + + try: + with open(cache_file, 'rb') as f: + cache = cPickle.load(f) + mtimes, cache = cache['mtimes'], cache['data'] + except Exception: + mtimes, cache = defaultdict(int), defaultdict(dict) + + seen_dirs = set() + changed = False + + for loc in base_dirs: + try: + subdirs = os.listdir(loc) + except EnvironmentError: + continue + for dname in subdirs: + d = os.path.join(loc, dname) + if os.path.isdir(d): + try: + mtime = os.stat(d).st_mtime + except EnvironmentError: + continue + seen_dirs.add(d) + if mtime != mtimes[d]: + changed = True + try: + cache[d] = read_icon_theme_dir(d) + except Exception: + prints('Failed to read icon theme dir: %r with error:' % d) + import traceback + traceback.print_exc() + mtimes[d] = mtime + for name, data in cache[d].iteritems(): + ans[name].append(data) + for removed in set(mtimes) - seen_dirs: + mtimes.pop(removed), cache.pop(removed) + changed = True + + if changed: + try: + with open(cache_file, 'wb') as f: + cPickle.dump({'data':cache, 'mtimes':mtimes}, f, -1) + except Exception: + import traceback + traceback.print_exc() + + for icons in ans.itervalues(): + icons.sort() + icon_data = {k:v[0][1] for k, v in ans.iteritems()} + return icon_data + + def localize_string(data): + lang = canonicalize_lang(get_lang()) + return data.get(lang, data.get(None)) or '' def find_programs(extensions): extensions = {ext.lower() for ext in extensions} @@ -31,13 +167,31 @@ else: data_dirs = [force_unicode(x, filesystem_encoding).rstrip(os.sep) for x in data_dirs] data_dirs = [x for x in data_dirs if x and os.path.isdir(x)] desktop_files = {} + mime_types = {guess_type('file.' + ext)[0] for ext in extensions} for base in data_dirs: for f in walk(os.path.join(base, 'applications')): if f.endswith('.desktop'): bn = os.path.basename(f) if f not in desktop_files: desktop_files[bn] = f + for bn, path in desktop_files.iteritems(): + try: + data = parse_desktop_file(path) + except Exception: + continue + if data is not None and mime_types.intersection(data['MimeType']): + icon = data.get('Icon', {}).get(None) + if icon and not os.path.isabs(icon): + icon = find_icons().get(icon) + if icon: + data['Icon'] = icon + else: + data.pop('Icon') + for k in ('Name', 'GenericName', 'Comment'): + val = data.get(k) + if val: + data[k] = localize_string(val) + yield data if __name__ == '__main__': print (find_programs('jpg jpeg'.split())) -