 13c54badcb
			
		
	
	
		13c54badcb
		
			
		
	
	
	
	
		
			
			No files in /usr should be modified during package runtime, `/var` is for that. So move this data there.
		
			
				
	
	
		
			368 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			368 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #! /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)
 |