qubes-desktop-file-install 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. #! /usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # vim: set ft=python ts=4 sw=4 sts=4 et :
  4. # Copyright (C) 2015 Jason Mehring <nrgaway@gmail.com>
  5. # License: GPL-2+
  6. # Authors: Jason Mehring
  7. #
  8. # This program is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU General Public License
  10. # as published by the Free Software Foundation; either version 2
  11. # of the License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. '''Installation and edition of desktop files.
  21. Description:
  22. The desktop-file-install program is a tool to install, and optionally edit,
  23. desktop files. They are mostly useful for developers and packagers.
  24. Various options are available to edit the desktop files. The edit options can
  25. be specified more than once and will be processed in the same order as the
  26. options passed to the program.
  27. The original .desktop files are left untouched and left in place.
  28. Qubes modifies the XDG_CONFIG_DIRS to first include the `/var/lib/qubes/xdg`
  29. directory (XDG_CONFIG_DIRS=/var/lib/qubes/xdg:/etc/xdg).
  30. Usage:
  31. qubes-desktop-file-install [--dir DIR] [--force]
  32. [--remove-show-in]
  33. [--remove-key KEY]
  34. [--remove-only-show-in ENVIRONMENT]
  35. [--add-only-show-in ENVIRONMENT]
  36. [--remove-not-show-in ENVIRONMENT]
  37. [--add-not-show-in ENVIRONMENT]
  38. [(--set-key KEY VALUE)]
  39. FILE
  40. qubes-desktop-file-install -h | --help
  41. qubes-desktop-file-install --version
  42. Examples:
  43. qubes-desktop-file-install --dir /var/lib/qubes/xdg/autostart --add-only-show-in X-QUBES /etc/xdg/autostart/pulseaudio.desktop
  44. Arguments:
  45. FILE Path to desktop entry file
  46. Help Options:
  47. -h, --help show this help message and exit
  48. Installation options for desktop file:
  49. --dir DIR Install desktop files to the DIR directory (default: <FILE>)
  50. --force Force overwrite of existing desktop files (default: False)
  51. Edition options for desktop file:
  52. --remove-show-in Remove the "OnlyShowIn" and "NotShowIn" entries from the desktop file (default: False)
  53. --remove-key KEY Remove the KEY key from the desktop files, if present
  54. --set-key (KEY VALUE) Set the KEY key to VALUE
  55. --remove-only-show-in ENVIRONMENT Remove ENVIRONMENT from the list of desktop environment where the desktop files should be displayed
  56. --add-only-show-in ENVIRONMENT Add ENVIRONMENT to the list of desktop environment where the desktop files should be displayed
  57. --remove-not-show-in ENVIRONMENT Remove ENVIRONMENT from the list of desktop environment where the desktop files should not be displayed
  58. --add-not-show-in ENVIRONMENT Add ENVIRONMENT to the list of desktop environment where the desktop files should not be displayed
  59. '''
  60. import argparse
  61. import codecs
  62. import io
  63. import locale
  64. import os
  65. import sys
  66. try:
  67. import ConfigParser as configparser
  68. except ImportError:
  69. import configparser
  70. from collections import OrderedDict
  71. # Third party libs
  72. import xdg.DesktopEntry
  73. __all__ = []
  74. __version__ = '1.0.0'
  75. # This is almost always a good thing to do at the beginning of your programs.
  76. locale.setlocale(locale.LC_ALL, '')
  77. # Default Qubes directory that modified desktop entry config files are stored in
  78. QUBES_XDG_CONFIG_DIR = '/vat/lib/qubes/xdg'
  79. class DesktopEntry(xdg.DesktopEntry.DesktopEntry):
  80. '''Class to parse and validate Desktop Entries (OVERRIDE).
  81. xdg.DesktopEntry.DesktopEntry does not maintain order of content
  82. '''
  83. defaultGroup = 'Desktop Entry'
  84. def __init__(self, filename=None):
  85. """Create a new DesktopEntry
  86. If filename exists, it will be parsed as a desktop entry file. If not,
  87. or if filename is None, a blank DesktopEntry is created.
  88. """
  89. self.content = OrderedDict()
  90. if filename and os.path.exists(filename):
  91. self.parse(filename)
  92. elif filename:
  93. self.new(filename)
  94. def parse(self, filename):
  95. '''Parse a desktop entry file.'''
  96. headers = [u'Desktop Entry', u'KDE Desktop Entry']
  97. cfgparser = configparser.RawConfigParser()
  98. cfgparser.optionxform = unicode
  99. try:
  100. cfgparser.readfp(codecs.open(filename, 'r', 'utf8'))
  101. except configparser.MissingSectionHeaderError:
  102. sys.exit('{0} missing header!'.format(filename, headers[0]))
  103. self.filename = filename
  104. self.tainted = False
  105. for header in headers:
  106. if cfgparser.has_section(header):
  107. self.content[header] = OrderedDict(cfgparser.items(header))
  108. if not self.defaultGroup:
  109. self.defaultGroup = header
  110. if not self.defaultGroup:
  111. sys.exit('{0} missing header!'.format(filename, headers[0]))
  112. # Write support broken in Wheezy; override here
  113. def write(self, filename=None, trusted=False):
  114. if not filename and not self.filename:
  115. raise ParsingError("File not found", "")
  116. if filename:
  117. self.filename = filename
  118. else:
  119. filename = self.filename
  120. if os.path.dirname(filename) and not os.path.isdir(os.path.dirname(filename)):
  121. os.makedirs(os.path.dirname(filename))
  122. with io.open(filename, 'w', encoding='utf-8') as fp:
  123. # An executable bit signifies that the desktop file is
  124. # trusted, but then the file can be executed. Add hashbang to
  125. # make sure that the file is opened by something that
  126. # understands desktop files.
  127. if trusted:
  128. fp.write(u("#!/usr/bin/env xdg-open\n"))
  129. if self.defaultGroup:
  130. fp.write(unicode("[%s]\n") % self.defaultGroup)
  131. for (key, value) in self.content[self.defaultGroup].items():
  132. fp.write(unicode("%s=%s\n") % (key, value))
  133. fp.write(unicode("\n"))
  134. for (name, group) in self.content.items():
  135. if name != self.defaultGroup:
  136. fp.write(unicode("[%s]\n") % name)
  137. for (key, value) in group.items():
  138. fp.write(unicode("%s=%s\n") % (key, value))
  139. fp.write(unicode("\n"))
  140. # Add executable bits to the file to show that it's trusted.
  141. if trusted:
  142. oldmode = os.stat(filename).st_mode
  143. mode = oldmode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
  144. os.chmod(filename, mode)
  145. self.tainted = False
  146. def delete(path):
  147. '''Delete a file.
  148. '''
  149. if os.path.exists(path):
  150. try:
  151. os.unlink(os.path.abspath(path))
  152. except IOError as error:
  153. sys.exit('Unable to delete file: {0}\n{1}'.format(path, error))
  154. def set_key(entry, key, value):
  155. '''Set a key with value within an desktop-file entry object.
  156. '''
  157. key = unicode(key)
  158. if isinstance(value, list):
  159. entry.set(key, u';'.join(value))
  160. else:
  161. entry.set(key, unicode(value))
  162. def remove_key(entry, key):
  163. '''Remove a key within an desktop-file entry object.
  164. '''
  165. entry.removeKey(unicode(key))
  166. def add_value(entry, key, value):
  167. '''Add a value to a desktop-file entry object.
  168. '''
  169. values = entry.getList(unicode(value))
  170. for value in values:
  171. entry_values = entry.get(key, list=True)
  172. if value not in entry_values:
  173. entry_values.append(value)
  174. set_key(entry, key, entry_values)
  175. def remove_value(entry, key, value):
  176. '''Remove a value to a desktop-file entry object.
  177. '''
  178. value = unicode(value)
  179. entry_values = entry.get(key, list=True)
  180. if value in entry_values:
  181. entry_values.remove(value)
  182. if entry_values:
  183. set_key(entry, key, entry_values)
  184. else:
  185. remove_key(entry, key)
  186. def install(**kwargs):
  187. '''Install a copy of a desktop-file entry file to a new location and
  188. optionally edit it.
  189. '''
  190. paths = kwargs.get('path', [])
  191. for path in paths:
  192. if not path:
  193. sys.exit('No path selected!')
  194. filename, extension = os.path.splitext(path)
  195. if extension.lower() not in ['.desktop']:
  196. sys.exit("Invalid desktop extenstion '{0}'! Was expecting '.desktop'.".format(extension))
  197. new_path = os.path.join(kwargs['dir'], os.path.basename(path))
  198. if os.path.exists(path) and os.path.isfile(path):
  199. stat_info = os.stat(path)
  200. # Don't update if file has previously been updated unless force is True
  201. if os.path.exists(new_path) and not kwargs['force']:
  202. if os.stat(new_path).st_mtime == stat_info.st_mtime:
  203. continue
  204. else:
  205. if os.path.exists(new_path) and os.path.isfile(new_path):
  206. delete(new_path)
  207. continue
  208. entry = DesktopEntry(path)
  209. if kwargs['remove_show_in']:
  210. kwargs['remove_key'].append(u'OnlyShowIn')
  211. kwargs['remove_key'].append(u'NotShowIn')
  212. if kwargs['remove_key']:
  213. for value in kwargs['remove_key']:
  214. remove_key(entry, value)
  215. if kwargs['remove_only_show_in']:
  216. for value in kwargs['remove_only_show_in']:
  217. remove_value(entry, u'OnlyShowIn', value)
  218. if kwargs['add_only_show_in']:
  219. for value in kwargs['add_only_show_in']:
  220. add_value(entry, u'OnlyShowIn', value)
  221. if kwargs['remove_not_show_in']:
  222. for value in kwargs['remove_not_show_in']:
  223. remove_value(entry, u'NotShowIn', value)
  224. if kwargs['add_not_show_in']:
  225. for value in kwargs['add_not_show_in']:
  226. add_value(entry, u'NotShowIn', value)
  227. if kwargs['set_key']:
  228. for key, value in kwargs['set_key']:
  229. set_key(entry, key, value)
  230. entry.write(new_path)
  231. if stat_info:
  232. os.utime(new_path, (stat_info.st_atime, stat_info.st_mtime))
  233. def parse(args):
  234. '''Argparse configuration.
  235. '''
  236. parser = argparse.ArgumentParser()
  237. parser.add_argument('--version', action='version', version='%(prog)s (version {0})'.format(__version__))
  238. parser.add_argument('--force', action='store_true', default=False, help='\
  239. Force overwrite of existing desktop files.')
  240. parser.add_argument('--dir', default=QUBES_XDG_CONFIG_DIR, help='\
  241. Install desktop files to the DIR directory.')
  242. parser.add_argument('--remove-show-in', action='store_true', default=False, help='\
  243. Remove the "OnlyShowIn" and "NotShowIn" entries from the desktop file')
  244. parser.add_argument('--remove-key', action='append', metavar='KEY', default=[], help='\
  245. Remove the KEY key from the desktop file')
  246. parser.add_argument('--remove-only-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
  247. Remove ENVIRONMENT from the list of desktop environments where the\
  248. desktop files should be displayed (key OnlyShowIn). If ENVIRONMENT was\
  249. not present in the list, this operation is a no-op.')
  250. parser.add_argument('--add-only-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
  251. Add ENVIRONMENT to the list of desktop environments where the desktop\
  252. files should be displayed (key OnlyShowIn). If ENVIRONMENT was already\
  253. present in the list, this operation is a no-op. A non-registered desktop\
  254. environment should be prefixed with X-. Note that an empty OnlyShowIn\
  255. key in a desktop file means that the desktop file will be displayed in\
  256. all environments.')
  257. parser.add_argument('--remove-not-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
  258. Remove ENVIRONMENT from the list of desktop environments where the\
  259. desktop files should not be displayed (key NotShowIn). If\
  260. ENVIRONMENT was not present in the list, this operation is a no-op.')
  261. parser.add_argument('--add-not-show-in', action='append', metavar='ENVIRONMENT', default=[], help='\
  262. Add ENVIRONMENT to the list of desktop environments where the desktop\
  263. files should not be displayed (key NotShowIn). If ENVIRONMENT was\
  264. already present in the list, this operation is a no-op. A non-registered\
  265. desktop environment should be prefixed with X-. Note that an empty\
  266. NotShowIn key in a desktop file means that the desktop file will be\
  267. displayed in all environments.')
  268. parser.add_argument('--set-key', action='append', nargs=2, metavar=('KEY', 'VALUE'), default=[], help='\
  269. Set the KEY key to the VALUE passed.')
  270. parser.add_argument('path', action='store', nargs='+', metavar='FILE', default=None,
  271. help='Path to desktop entry file')
  272. args = parser.parse_args(args)
  273. if not os.path.isabs(args.dir):
  274. args.dir = os.path.join(QUBES_XDG_CONFIG_DIR, args.dir)
  275. return args
  276. def main(argv):
  277. '''Main function.
  278. '''
  279. args = parse(argv[1:])
  280. install(**vars(args))
  281. if __name__ == '__main__':
  282. main(sys.argv)
  283. sys.exit(0)