|
@@ -0,0 +1,367 @@
|
|
|
+#! /usr/bin/env python
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+# vim: set ft=python ts=4 sw=4 sts=4 et :
|
|
|
+
|
|
|
+# Copyright (C) 2015 Jason Mehring <nrgaway@gmail.com>
|
|
|
+# License: GPL-2+
|
|
|
+# Authors: Jason Mehring
|
|
|
+#
|
|
|
+# This program is free software; you can redistribute it and/or
|
|
|
+# modify it under the terms of the GNU General Public License
|
|
|
+# as published by the Free Software Foundation; either version 2
|
|
|
+# of the License, or (at your option) any later version.
|
|
|
+#
|
|
|
+# This program is distributed in the hope that it will be useful,
|
|
|
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+# GNU General Public License for more details.
|
|
|
+#
|
|
|
+# You should have received a copy of the GNU General Public License
|
|
|
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+
|
|
|
+'''Installation and edition of desktop files.
|
|
|
+
|
|
|
+Description:
|
|
|
+ The desktop-file-install program is a tool to install, and optionally edit,
|
|
|
+ desktop files. They are mostly useful for developers and packagers.
|
|
|
+
|
|
|
+ Various options are available to edit the desktop files. The edit options can
|
|
|
+ be specified more than once and will be processed in the same order as the
|
|
|
+ options passed to the program.
|
|
|
+
|
|
|
+ The original .desktop files are left untouched and left in place.
|
|
|
+
|
|
|
+ Qubes modifies the XDG_CONFIG_DIRS to first include the `/usr/share/qubes/xdg`
|
|
|
+ directory (XDG_CONFIG_DIRS=/usr/share/qubes/xdg:/etc/xdg).
|
|
|
+
|
|
|
+Usage:
|
|
|
+ qubes-desktop-file-install [--dir DIR] [--force]
|
|
|
+ [--remove-show-in]
|
|
|
+ [--remove-key KEY]
|
|
|
+ [--remove-only-show-in ENVIRONMENT]
|
|
|
+ [--add-only-show-in ENVIRONMENT]
|
|
|
+ [--remove-not-show-in ENVIRONMENT]
|
|
|
+ [--add-not-show-in ENVIRONMENT]
|
|
|
+ [(--set-key KEY VALUE)]
|
|
|
+ FILE
|
|
|
+ qubes-desktop-file-install -h | --help
|
|
|
+ qubes-desktop-file-install --version
|
|
|
+
|
|
|
+Examples:
|
|
|
+ qubes-desktop-file-install --dir /usr/share/qubes/xdg/autostart --add-only-show-in X-QUBES /etc/xdg/autostart/pulseaudio.desktop
|
|
|
+
|
|
|
+Arguments:
|
|
|
+ FILE Path to desktop entry file
|
|
|
+
|
|
|
+Help Options:
|
|
|
+ -h, --help show this help message and exit
|
|
|
+
|
|
|
+Installation options for desktop file:
|
|
|
+ --dir DIR Install desktop files to the DIR directory (default: <FILE>)
|
|
|
+ --force Force overwrite of existing desktop files (default: False)
|
|
|
+
|
|
|
+Edition options for desktop file:
|
|
|
+ --remove-show-in Remove the "OnlyShowIn" and "NotShowIn" entries from the desktop file (default: False)
|
|
|
+ --remove-key KEY Remove the KEY key from the desktop files, if present
|
|
|
+ --set-key (KEY VALUE) Set the KEY key to VALUE
|
|
|
+ --remove-only-show-in ENVIRONMENT Remove ENVIRONMENT from the list of desktop environment where the desktop files should be displayed
|
|
|
+ --add-only-show-in ENVIRONMENT Add ENVIRONMENT to the list of desktop environment where the desktop files should be displayed
|
|
|
+ --remove-not-show-in ENVIRONMENT Remove ENVIRONMENT from the list of desktop environment where the desktop files should not be displayed
|
|
|
+ --add-not-show-in ENVIRONMENT Add ENVIRONMENT to the list of desktop environment where the desktop files should not be displayed
|
|
|
+'''
|
|
|
+
|
|
|
+import argparse
|
|
|
+import codecs
|
|
|
+import io
|
|
|
+import locale
|
|
|
+import os
|
|
|
+import sys
|
|
|
+
|
|
|
+try:
|
|
|
+ import ConfigParser as configparser
|
|
|
+except ImportError:
|
|
|
+ import configparser
|
|
|
+
|
|
|
+from collections import OrderedDict
|
|
|
+
|
|
|
+# Third party libs
|
|
|
+import xdg.DesktopEntry
|
|
|
+
|
|
|
+__all__ = []
|
|
|
+__version__ = '1.0.0'
|
|
|
+
|
|
|
+# This is almost always a good thing to do at the beginning of your programs.
|
|
|
+locale.setlocale(locale.LC_ALL, '')
|
|
|
+
|
|
|
+# Default Qubes directory that modified desktop entry config files are stored in
|
|
|
+QUBES_XDG_CONFIG_DIR = '/usr/share/qubes/xdg'
|
|
|
+
|
|
|
+
|
|
|
+class DesktopEntry(xdg.DesktopEntry.DesktopEntry):
|
|
|
+ '''Class to parse and validate Desktop Entries (OVERRIDE).
|
|
|
+
|
|
|
+ xdg.DesktopEntry.DesktopEntry does not maintain order of content
|
|
|
+ '''
|
|
|
+
|
|
|
+ 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 = OrderedDict()
|
|
|
+ if filename and os.path.exists(filename):
|
|
|
+ self.parse(filename)
|
|
|
+ elif filename:
|
|
|
+ self.new(filename)
|
|
|
+
|
|
|
+ def parse(self, filename):
|
|
|
+ '''Parse a desktop entry file.'''
|
|
|
+ headers = [u'Desktop Entry', u'KDE Desktop Entry']
|
|
|
+ cfgparser = configparser.RawConfigParser()
|
|
|
+ cfgparser.optionxform = unicode
|
|
|
+
|
|
|
+ try:
|
|
|
+ cfgparser.readfp(codecs.open(filename, 'r', 'utf8'))
|
|
|
+ except configparser.MissingSectionHeaderError:
|
|
|
+ sys.exit('{0} missing header!'.format(filename, headers[0]))
|
|
|
+
|
|
|
+ self.filename = filename
|
|
|
+ self.tainted = False
|
|
|
+
|
|
|
+ for header in headers:
|
|
|
+ if cfgparser.has_section(header):
|
|
|
+ self.content[header] = OrderedDict(cfgparser.items(header))
|
|
|
+ if not self.defaultGroup:
|
|
|
+ self.defaultGroup = header
|
|
|
+
|
|
|
+ if not self.defaultGroup:
|
|
|
+ sys.exit('{0} missing header!'.format(filename, headers[0]))
|
|
|
+
|
|
|
+ # Write support broken in Wheezy; override here
|
|
|
+ def write(self, filename=None, trusted=False):
|
|
|
+ if not filename and not self.filename:
|
|
|
+ raise ParsingError("File not found", "")
|
|
|
+
|
|
|
+ if filename:
|
|
|
+ self.filename = filename
|
|
|
+ else:
|
|
|
+ filename = self.filename
|
|
|
+
|
|
|
+ if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)):
|
|
|
+ os.makedirs(os.path.dirname(filename))
|
|
|
+
|
|
|
+ with io.open(filename, 'w', encoding='utf-8') as fp:
|
|
|
+
|
|
|
+ # An executable bit signifies that the desktop file is
|
|
|
+ # trusted, but then the file can be executed. Add hashbang to
|
|
|
+ # make sure that the file is opened by something that
|
|
|
+ # understands desktop files.
|
|
|
+ if trusted:
|
|
|
+ fp.write(u("#!/usr/bin/env xdg-open\n"))
|
|
|
+
|
|
|
+ if self.defaultGroup:
|
|
|
+ fp.write(unicode("[%s]\n") % self.defaultGroup)
|
|
|
+ for (key, value) in self.content[self.defaultGroup].items():
|
|
|
+ fp.write(unicode("%s=%s\n") % (key, value))
|
|
|
+ fp.write(unicode("\n"))
|
|
|
+ for (name, group) in self.content.items():
|
|
|
+ if name != self.defaultGroup:
|
|
|
+ fp.write(unicode("[%s]\n") % name)
|
|
|
+ for (key, value) in group.items():
|
|
|
+ fp.write(unicode("%s=%s\n") % (key, value))
|
|
|
+ fp.write(unicode("\n"))
|
|
|
+
|
|
|
+ # Add executable bits to the file to show that it's trusted.
|
|
|
+ if trusted:
|
|
|
+ oldmode = os.stat(filename).st_mode
|
|
|
+ mode = oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
|
+ os.chmod(filename, mode)
|
|
|
+
|
|
|
+ self.tainted = False
|
|
|
+
|
|
|
+
|
|
|
+def delete(path):
|
|
|
+ '''Delete a file.
|
|
|
+ '''
|
|
|
+ if os.path.exists(path):
|
|
|
+ try:
|
|
|
+ os.unlink(os.path.abspath(path))
|
|
|
+ except IOError as error:
|
|
|
+ sys.exit('Unable to delete file: {0}\n{1}'.format(path, error))
|
|
|
+
|
|
|
+
|
|
|
+def set_key(entry, key, value):
|
|
|
+ '''Set a key with value within an desktop-file entry object.
|
|
|
+ '''
|
|
|
+ key = unicode(key)
|
|
|
+ if isinstance(value, list):
|
|
|
+ entry.set(key, u';'.join(value))
|
|
|
+ else:
|
|
|
+ entry.set(key, unicode(value))
|
|
|
+
|
|
|
+
|
|
|
+def remove_key(entry, key):
|
|
|
+ '''Remove a key within an desktop-file entry object.
|
|
|
+ '''
|
|
|
+ entry.removeKey(unicode(key))
|
|
|
+
|
|
|
+
|
|
|
+def add_value(entry, key, value):
|
|
|
+ '''Add a value to a desktop-file entry object.
|
|
|
+ '''
|
|
|
+ values = entry.getList(unicode(value))
|
|
|
+ for value in values:
|
|
|
+ entry_values = entry.get(key, list=True)
|
|
|
+ if value not in entry_values:
|
|
|
+ entry_values.append(value)
|
|
|
+ set_key(entry, key, entry_values)
|
|
|
+
|
|
|
+
|
|
|
+def remove_value(entry, key, value):
|
|
|
+ '''Remove a value to a desktop-file entry object.
|
|
|
+ '''
|
|
|
+ value = unicode(value)
|
|
|
+ entry_values = entry.get(key, list=True)
|
|
|
+ if value in entry_values:
|
|
|
+ entry_values.remove(value)
|
|
|
+ if entry_values:
|
|
|
+ set_key(entry, key, entry_values)
|
|
|
+ else:
|
|
|
+ remove_key(entry, key)
|
|
|
+
|
|
|
+
|
|
|
+def install(**kwargs):
|
|
|
+ '''Install a copy of a desktop-file entry file to a new location and
|
|
|
+ optionally edit it.
|
|
|
+ '''
|
|
|
+ paths = kwargs.get('path', [])
|
|
|
+ for path in paths:
|
|
|
+ if not path:
|
|
|
+ sys.exit('No path selected!')
|
|
|
+
|
|
|
+ filename, extension = os.path.splitext(path)
|
|
|
+ if extension.lower() not in ['.desktop']:
|
|
|
+ sys.exit("Invalid desktop extenstion '{0}'! Was expecting '.desktop'.".format(extension))
|
|
|
+
|
|
|
+ new_path = os.path.join(kwargs['dir'], os.path.basename(path))
|
|
|
+
|
|
|
+ if os.path.exists(path) and os.path.isfile(path):
|
|
|
+ stat_info = os.stat(path)
|
|
|
+
|
|
|
+ # Don't update if file has previously been updated unless force is True
|
|
|
+ if os.path.exists(new_path) and not kwargs['force']:
|
|
|
+ if os.stat(new_path).st_mtime == stat_info.st_mtime:
|
|
|
+ continue
|
|
|
+ else:
|
|
|
+ if os.path.exists(new_path) and os.path.isfile(new_path):
|
|
|
+ delete(new_path)
|
|
|
+ continue
|
|
|
+
|
|
|
+ entry = DesktopEntry(path)
|
|
|
+
|
|
|
+ if kwargs['remove_show_in']:
|
|
|
+ kwargs['remove_key'].append(u'OnlyShowIn')
|
|
|
+ kwargs['remove_key'].append(u'NotShowIn')
|
|
|
+
|
|
|
+ if kwargs['remove_key']:
|
|
|
+ for value in kwargs['remove_key']:
|
|
|
+ remove_key(entry, value)
|
|
|
+
|
|
|
+ if kwargs['remove_only_show_in']:
|
|
|
+ for value in kwargs['remove_only_show_in']:
|
|
|
+ remove_value(entry, u'OnlyShowIn', value)
|
|
|
+
|
|
|
+ if kwargs['add_only_show_in']:
|
|
|
+ for value in kwargs['add_only_show_in']:
|
|
|
+ add_value(entry, u'OnlyShowIn', value)
|
|
|
+
|
|
|
+ if kwargs['remove_not_show_in']:
|
|
|
+ for value in kwargs['remove_not_show_in']:
|
|
|
+ remove_value(entry, u'NotShowIn', value)
|
|
|
+
|
|
|
+ if kwargs['add_not_show_in']:
|
|
|
+ for value in kwargs['add_not_show_in']:
|
|
|
+ add_value(entry, u'NotShowIn', value)
|
|
|
+
|
|
|
+ if kwargs['set_key']:
|
|
|
+ for key, value in kwargs['set_key']:
|
|
|
+ set_key(entry, key, value)
|
|
|
+
|
|
|
+ entry.write(new_path)
|
|
|
+ if stat_info:
|
|
|
+ os.utime(new_path, (stat_info.st_atime, stat_info.st_mtime))
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+def parse(args):
|
|
|
+ '''Argparse configuration.
|
|
|
+ '''
|
|
|
+ parser = argparse.ArgumentParser()
|
|
|
+
|
|
|
+ parser.add_argument('--version', action='version', version='%(prog)s (version {0})'.format(__version__))
|
|
|
+
|
|
|
+ parser.add_argument('--force', action='store_true', default=False, help='\
|
|
|
+ Force overwrite of existing desktop files.')
|
|
|
+
|
|
|
+ parser.add_argument('--dir', default=QUBES_XDG_CONFIG_DIR, help='\
|
|
|
+ Install desktop files to the DIR directory.')
|
|
|
+
|
|
|
+ parser.add_argument('--remove-show-in', action='store_true', default=False, help='\
|
|
|
+ Remove the "OnlyShowIn" and "NotShowIn" entries from the desktop file')
|
|
|
+
|
|
|
+ parser.add_argument('--remove-key', action='append', metavar='KEY', default=[], help='\
|
|
|
+ Remove the KEY key from the desktop file')
|
|
|
+
|
|
|
+ parser.add_argument('--remove-only-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
|
|
|
+ Remove ENVIRONMENT from the list of desktop environments where the\
|
|
|
+ desktop files should be displayed (key OnlyShowIn). If ENVIRONMENT was\
|
|
|
+ not present in the list, this operation is a no-op.')
|
|
|
+
|
|
|
+ parser.add_argument('--add-only-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
|
|
|
+ Add ENVIRONMENT to the list of desktop environments where the desktop\
|
|
|
+ files should be displayed (key OnlyShowIn). If ENVIRONMENT was already\
|
|
|
+ present in the list, this operation is a no-op. A non-registered desktop\
|
|
|
+ environment should be prefixed with X-. Note that an empty OnlyShowIn\
|
|
|
+ key in a desktop file means that the desktop file will be displayed in\
|
|
|
+ all environments.')
|
|
|
+
|
|
|
+ parser.add_argument('--remove-not-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
|
|
|
+ Remove ENVIRONMENT from the list of desktop environments where the\
|
|
|
+ desktop files should not be displayed (key NotShowIn). If\
|
|
|
+ ENVIRONMENT was not present in the list, this operation is a no-op.')
|
|
|
+
|
|
|
+ parser.add_argument('--add-not-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
|
|
|
+ Add ENVIRONMENT to the list of desktop environments where the desktop\
|
|
|
+ files should not be displayed (key NotShowIn). If ENVIRONMENT was\
|
|
|
+ already present in the list, this operation is a no-op. A non-registered\
|
|
|
+ desktop environment should be prefixed with X-. Note that an empty\
|
|
|
+ NotShowIn key in a desktop file means that the desktop file will be\
|
|
|
+ displayed in all environments.')
|
|
|
+
|
|
|
+ parser.add_argument('--set-key', action='append', nargs=2, metavar=('KEY', 'VALUE'), default=[], help='\
|
|
|
+ Set the KEY key to the VALUE passed.')
|
|
|
+
|
|
|
+ parser.add_argument('path', action='store', nargs='+', metavar='FILE', default=None,
|
|
|
+ help='Path to desktop entry file')
|
|
|
+
|
|
|
+ args = parser.parse_args(args)
|
|
|
+
|
|
|
+ if not os.path.isabs(args.dir):
|
|
|
+ args.dir = os.path.join(QUBES_XDG_CONFIG_DIR, args.dir)
|
|
|
+
|
|
|
+ return args
|
|
|
+
|
|
|
+
|
|
|
+def main(argv):
|
|
|
+ '''Main function.
|
|
|
+ '''
|
|
|
+ args = parse(argv[1:])
|
|
|
+ install(**vars(args))
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ main(sys.argv)
|
|
|
+ sys.exit(0)
|