123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367 |
- #! /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 `/var/lib/qubes/xdg`
- directory (XDG_CONFIG_DIRS=/var/lib/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 /var/lib/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 = '/vat/lib/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)
|