Move appmenus/icons related to desktop-linux-common
This is the right place for desktop related files - later it will be installed in GUI VM (but core-admin-linux will not). QubesOS/qubes-issues#2735
@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Name=Command Prompt
|
||||
Comment=Use the command line
|
||||
Categories=GNOME;GTK;Utility;TerminalEmulator;System;
|
||||
Exec=cmd /c start cmd
|
@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Name=Explorer
|
||||
Comment=Browse files
|
||||
Categories=Utility;Core;
|
||||
Exec=explorer
|
@ -1,8 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Terminal=false
|
||||
Name=Internet Explorer
|
||||
Comment=Browse the Web
|
||||
Categories=Network;WebBrowser;
|
||||
Exec=C:\\Program Files\\Internet Explorer\\iexplore.exe
|
@ -1,10 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Exec=sh -c 'echo firefox | /usr/lib/qubes/qfile-daemon-dvm qubes.VMShell dom0 DEFAULT red'
|
||||
Icon=dispvm-red
|
||||
Terminal=false
|
||||
Name=DispVM: Firefox web browser
|
||||
GenericName=DispVM: Web browser
|
||||
StartupNotify=false
|
||||
Categories=Network;X-Qubes-VM;
|
@ -1,10 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Exec=sh -c 'echo xterm | /usr/lib/qubes/qfile-daemon-dvm qubes.VMShell dom0 DEFAULT red'
|
||||
Icon=dispvm-red
|
||||
Terminal=false
|
||||
Name=DispVM: xterm
|
||||
GenericName=DispVM: Terminal
|
||||
StartupNotify=false
|
||||
Categories=Network;X-Qubes-VM;
|
@ -1,5 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Encoding=UTF-8
|
||||
Type=Directory
|
||||
Name=DisposableVM
|
||||
Icon=dispvm-red
|
@ -1,10 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Exec=qvm-start --quiet --tray %VMNAME%
|
||||
Icon=%XDGICON%
|
||||
Terminal=false
|
||||
Name=%VMNAME%: Start
|
||||
GenericName=%VMNAME%: Start
|
||||
StartupNotify=false
|
||||
Categories=System;X-Qubes-VM;
|
@ -1,56 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
#
|
||||
import sys
|
||||
from qubes.qubes import QubesVmCollection
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4:
|
||||
print >> sys.stderr, \
|
||||
'Usage: qvm-appmenu-replace VM_NAME OLD_NAME.desktop NEW_NAME.desktop'
|
||||
sys.exit(1)
|
||||
vm_name = sys.argv[1]
|
||||
old_name = sys.argv[2]
|
||||
new_name = sys.argv[3]
|
||||
|
||||
qvm_collection = QubesVmCollection()
|
||||
qvm_collection.lock_db_for_reading()
|
||||
qvm_collection.load()
|
||||
qvm_collection.unlock_db()
|
||||
|
||||
vm = qvm_collection.get_vm_by_name(vm_name)
|
||||
|
||||
if vm is None:
|
||||
print >> sys.stderr, "ERROR: A VM with the name '{0}' " \
|
||||
"does not exist in the system.".format(
|
||||
vm_name)
|
||||
exit(1)
|
||||
|
||||
if vm.template is not None:
|
||||
print >> sys.stderr, "ERROR: To replace appmenu for template based VM, " \
|
||||
"do it on template instead"
|
||||
exit(1)
|
||||
|
||||
vm.appmenus_replace_entry(old_name, new_name)
|
||||
if hasattr(vm, 'appvms'):
|
||||
for child_vm in vm.appvms.values():
|
||||
child_vm.appmenus_replace_entry(old_name, new_name)
|
||||
|
||||
main()
|
BIN
icons/black.png
Before Width: | Height: | Size: 169 KiB |
BIN
icons/blue.png
Before Width: | Height: | Size: 181 KiB |
@ -1,10 +0,0 @@
|
||||
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
This copyright and license notice covers the images in this directory.
|
||||
************************************************************************
|
||||
|
||||
TITLE: Crystal Project Icons
|
||||
AUTHOR: Everaldo Coelho
|
||||
SITE: http://www.everaldo.com
|
||||
CONTACT: everaldo@everaldo.com
|
||||
|
||||
Copyright (c) 2006-2007 Everaldo Coelho.
|
@ -1 +0,0 @@
|
||||
dom0-update-avail icon from gnome-packagekit project distributed under GPLv2
|
@ -1 +0,0 @@
|
||||
Color padlock images downloaded from www.openclipart.org
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 2.5 KiB |
BIN
icons/gray.png
Before Width: | Height: | Size: 192 KiB |
BIN
icons/green.png
Before Width: | Height: | Size: 187 KiB |
BIN
icons/netvm.png
Before Width: | Height: | Size: 15 KiB |
BIN
icons/orange.png
Before Width: | Height: | Size: 188 KiB |
BIN
icons/purple.png
Before Width: | Height: | Size: 188 KiB |
BIN
icons/qubes.png
Before Width: | Height: | Size: 20 KiB |
BIN
icons/red.png
Before Width: | Height: | Size: 177 KiB |
Before Width: | Height: | Size: 20 KiB |
BIN
icons/yellow.png
Before Width: | Height: | Size: 185 KiB |
@ -1,491 +0,0 @@
|
||||
#!/usr/bin/python2
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2010 Joanna Rutkowska <joanna@invisiblethingslab.com>
|
||||
# Copyright (C) 2013 Marek Marczykowski <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
|
||||
# USA.
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import dbus
|
||||
import pkg_resources
|
||||
|
||||
import qubes.ext
|
||||
import qubes.vm.dispvm
|
||||
|
||||
import qubesimgconverter
|
||||
|
||||
|
||||
class AppmenusSubdirs:
|
||||
templates_subdir = 'apps.templates'
|
||||
template_icons_subdir = 'apps.tempicons'
|
||||
subdir = 'apps'
|
||||
icons_subdir = 'apps.icons'
|
||||
template_templates_subdir = 'apps-template.templates'
|
||||
whitelist = 'whitelisted-appmenus.list'
|
||||
|
||||
|
||||
class AppmenusPaths:
|
||||
appmenu_start_hvm_template = \
|
||||
'/usr/share/qubes-appmenus/qubes-start.desktop'
|
||||
|
||||
|
||||
class AppmenusExtension(qubes.ext.Extension):
|
||||
def __init__(self, *args):
|
||||
super(AppmenusExtension, self).__init__(*args)
|
||||
import qubes.vm.qubesvm
|
||||
import qubes.vm.templatevm
|
||||
|
||||
def templates_dir(self, vm):
|
||||
"""
|
||||
|
||||
:type vm: qubes.vm.qubesvm.QubesVM
|
||||
"""
|
||||
if vm.updateable:
|
||||
return os.path.join(vm.dir_path,
|
||||
AppmenusSubdirs.templates_subdir)
|
||||
elif hasattr(vm, 'template'):
|
||||
return self.templates_dir(vm.template)
|
||||
else:
|
||||
return None
|
||||
|
||||
def template_icons_dir(self, vm):
|
||||
'''Directory for not yet colore icons'''
|
||||
if vm.updateable:
|
||||
return os.path.join(vm.dir_path,
|
||||
AppmenusSubdirs.template_icons_subdir)
|
||||
elif hasattr(vm, 'template'):
|
||||
return self.template_icons_dir(vm.template)
|
||||
else:
|
||||
return None
|
||||
|
||||
def appmenus_dir(self, vm):
|
||||
'''Desktop files generated for particular VM'''
|
||||
return os.path.join(vm.dir_path, AppmenusSubdirs.subdir)
|
||||
|
||||
def icons_dir(self, vm):
|
||||
'''Icon files generated (colored) for particular VM'''
|
||||
return os.path.join(vm.dir_path, AppmenusSubdirs.icons_subdir)
|
||||
|
||||
def whitelist_path(self, vm):
|
||||
'''File listing files wanted in menu'''
|
||||
return os.path.join(vm.dir_path, AppmenusSubdirs.whitelist)
|
||||
|
||||
def directory_template_name(self, vm):
|
||||
'''File name of desktop directory entry template'''
|
||||
if isinstance(vm, qubes.vm.templatevm.TemplateVM):
|
||||
return 'qubes-templatevm.directory.template'
|
||||
elif vm.provides_network:
|
||||
return 'qubes-servicevm.directory.template'
|
||||
else:
|
||||
return 'qubes-vm.directory.template'
|
||||
|
||||
def write_desktop_file(self, vm, source, destination_path):
|
||||
"""Format .desktop/.directory file
|
||||
|
||||
:param vm: QubesVM object for which write desktop file
|
||||
:param source: desktop file template (path or template itself)
|
||||
:param destination_path: where to write the desktop file
|
||||
:return: True if target file was changed, otherwise False
|
||||
"""
|
||||
if source.startswith('/'):
|
||||
source = open(source).read()
|
||||
data = source.\
|
||||
replace("%VMNAME%", vm.name).\
|
||||
replace("%VMDIR%", vm.dir_path).\
|
||||
replace("%XDGICON%", vm.label.icon)
|
||||
if os.path.exists(destination_path):
|
||||
current_dest = open(destination_path).read()
|
||||
if current_dest == data:
|
||||
return False
|
||||
with open(destination_path, "w") as f:
|
||||
f.write(data)
|
||||
return True
|
||||
|
||||
def appmenus_create(self, vm, refresh_cache=True):
|
||||
"""Create/update .desktop files
|
||||
|
||||
:param vm: QubesVM object for which create entries
|
||||
:param refresh_cache: refresh desktop environment cache; if false,
|
||||
must be refreshed manually later
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if vm.internal:
|
||||
return
|
||||
if isinstance(vm, qubes.vm.dispvm.DispVM):
|
||||
return
|
||||
|
||||
vm.log.info("Creating appmenus")
|
||||
appmenus_dir = self.appmenus_dir(vm)
|
||||
if not os.path.exists(appmenus_dir):
|
||||
os.makedirs(appmenus_dir)
|
||||
|
||||
anything_changed = False
|
||||
directory_file = os.path.join(appmenus_dir, vm.name + '-vm.directory')
|
||||
if self.write_desktop_file(vm,
|
||||
pkg_resources.resource_string(__name__,
|
||||
self.directory_template_name(vm)), directory_file):
|
||||
anything_changed = True
|
||||
|
||||
templates_dir = self.templates_dir(vm)
|
||||
if os.path.exists(templates_dir):
|
||||
appmenus = os.listdir(templates_dir)
|
||||
else:
|
||||
appmenus = []
|
||||
changed_appmenus = []
|
||||
if os.path.exists(self.whitelist_path(vm)):
|
||||
whitelist = [x.rstrip() for x in open(self.whitelist_path(vm))]
|
||||
appmenus = [x for x in appmenus if x in whitelist]
|
||||
|
||||
for appmenu in appmenus:
|
||||
if self.write_desktop_file(vm,
|
||||
os.path.join(templates_dir, appmenu),
|
||||
os.path.join(appmenus_dir,
|
||||
'-'.join((vm.name, appmenu)))):
|
||||
changed_appmenus.append(appmenu)
|
||||
if self.write_desktop_file(vm,
|
||||
pkg_resources.resource_string(
|
||||
__name__, 'qubes-appmenu-select.desktop.template'
|
||||
),
|
||||
os.path.join(appmenus_dir,
|
||||
'-'.join((vm.name, 'qubes-appmenu-select.desktop')))):
|
||||
changed_appmenus.append('qubes-appmenu-select.desktop')
|
||||
|
||||
if changed_appmenus:
|
||||
anything_changed = True
|
||||
|
||||
target_appmenus = map(
|
||||
lambda x: '-'.join((vm.name, x)),
|
||||
appmenus + ['qubes-appmenu-select.desktop']
|
||||
)
|
||||
|
||||
# remove old entries
|
||||
installed_appmenus = os.listdir(appmenus_dir)
|
||||
installed_appmenus.remove(os.path.basename(directory_file))
|
||||
appmenus_to_remove = set(installed_appmenus).difference(set(
|
||||
target_appmenus))
|
||||
if len(appmenus_to_remove):
|
||||
appmenus_to_remove_fnames = map(
|
||||
lambda x: os.path.join(appmenus_dir, x), appmenus_to_remove)
|
||||
try:
|
||||
desktop_menu_cmd = ['xdg-desktop-menu', 'uninstall']
|
||||
if not refresh_cache:
|
||||
desktop_menu_cmd.append('--noupdate')
|
||||
desktop_menu_cmd.append(directory_file)
|
||||
desktop_menu_cmd.extend(appmenus_to_remove_fnames)
|
||||
desktop_menu_env = os.environ.copy()
|
||||
desktop_menu_env['LC_COLLATE'] = 'C'
|
||||
subprocess.check_call(desktop_menu_cmd, env=desktop_menu_env)
|
||||
except subprocess.CalledProcessError:
|
||||
vm.log.warning("Problem removing old appmenus")
|
||||
|
||||
for appmenu in appmenus_to_remove_fnames:
|
||||
os.unlink(appmenu)
|
||||
|
||||
# add new entries
|
||||
if anything_changed:
|
||||
try:
|
||||
desktop_menu_cmd = ['xdg-desktop-menu', 'install']
|
||||
if not refresh_cache:
|
||||
desktop_menu_cmd.append('--noupdate')
|
||||
desktop_menu_cmd.append(directory_file)
|
||||
desktop_menu_cmd.extend(map(
|
||||
lambda x: os.path.join(
|
||||
appmenus_dir, '-'.join((vm.name, x))),
|
||||
changed_appmenus))
|
||||
desktop_menu_env = os.environ.copy()
|
||||
desktop_menu_env['LC_COLLATE'] = 'C'
|
||||
subprocess.check_call(desktop_menu_cmd, env=desktop_menu_env)
|
||||
except subprocess.CalledProcessError:
|
||||
vm.log.warning("Problem creating appmenus")
|
||||
|
||||
if refresh_cache:
|
||||
if 'KDE_SESSION_UID' in os.environ:
|
||||
subprocess.call(['kbuildsycoca' +
|
||||
os.environ.get('KDE_SESSION_VERSION', '4')])
|
||||
|
||||
def appmenus_remove(self, vm, refresh_cache=True):
|
||||
'''Remove desktop files for particular VM'''
|
||||
appmenus_dir = self.appmenus_dir(vm)
|
||||
if os.path.exists(appmenus_dir):
|
||||
vm.log.info("Removing appmenus")
|
||||
installed_appmenus = os.listdir(appmenus_dir)
|
||||
directory_file = os.path.join(self.appmenus_dir(vm),
|
||||
vm.name + '-vm.directory')
|
||||
installed_appmenus.remove(os.path.basename(directory_file))
|
||||
if installed_appmenus:
|
||||
appmenus_to_remove_fnames = map(
|
||||
lambda x: os.path.join(appmenus_dir, x), installed_appmenus)
|
||||
try:
|
||||
desktop_menu_cmd = ['xdg-desktop-menu', 'uninstall']
|
||||
if not refresh_cache:
|
||||
desktop_menu_cmd.append('--noupdate')
|
||||
desktop_menu_cmd.append(directory_file)
|
||||
desktop_menu_cmd.extend(appmenus_to_remove_fnames)
|
||||
desktop_menu_env = os.environ.copy()
|
||||
desktop_menu_env['LC_COLLATE'] = 'C'
|
||||
subprocess.check_call(desktop_menu_cmd,
|
||||
env=desktop_menu_env)
|
||||
except subprocess.CalledProcessError:
|
||||
vm.log.warning("Problem removing appmenus")
|
||||
shutil.rmtree(appmenus_dir)
|
||||
|
||||
if refresh_cache:
|
||||
if 'KDE_SESSION_UID' in os.environ:
|
||||
subprocess.call(['kbuildsycoca' +
|
||||
os.environ.get('KDE_SESSION_VERSION', '4')])
|
||||
|
||||
def appicons_create(self, vm, srcdir=None, force=False):
|
||||
"""Create/update applications icons"""
|
||||
if srcdir is None:
|
||||
srcdir = self.template_icons_dir(vm)
|
||||
if srcdir is None:
|
||||
return
|
||||
if not os.path.exists(srcdir):
|
||||
return
|
||||
|
||||
if vm.internal:
|
||||
return
|
||||
if isinstance(vm, qubes.vm.dispvm.DispVM):
|
||||
return
|
||||
|
||||
whitelist = self.whitelist_path(vm)
|
||||
if os.path.exists(whitelist):
|
||||
whitelist = [line.strip() for line in open(whitelist)]
|
||||
else:
|
||||
whitelist = None
|
||||
|
||||
dstdir = self.icons_dir(vm)
|
||||
if not os.path.exists(dstdir):
|
||||
os.mkdir(dstdir)
|
||||
elif not os.path.isdir(dstdir):
|
||||
os.unlink(dstdir)
|
||||
os.mkdir(dstdir)
|
||||
|
||||
if whitelist:
|
||||
expected_icons = \
|
||||
map(lambda x: os.path.splitext(x)[0] + '.png', whitelist)
|
||||
else:
|
||||
expected_icons = os.listdir(srcdir)
|
||||
|
||||
for icon in os.listdir(srcdir):
|
||||
if icon not in expected_icons:
|
||||
continue
|
||||
|
||||
src_icon = os.path.join(srcdir, icon)
|
||||
dst_icon = os.path.join(dstdir, icon)
|
||||
if not os.path.exists(dst_icon) or force or \
|
||||
os.path.getmtime(src_icon) > os.path.getmtime(dst_icon):
|
||||
qubesimgconverter.tint(src_icon, dst_icon, vm.label.color)
|
||||
|
||||
for icon in os.listdir(dstdir):
|
||||
if icon not in expected_icons:
|
||||
os.unlink(os.path.join(dstdir, icon))
|
||||
|
||||
def appicons_remove(self, vm):
|
||||
'''Remove icons'''
|
||||
if not os.path.exists(self.icons_dir(vm)):
|
||||
return
|
||||
shutil.rmtree(self.icons_dir(vm))
|
||||
|
||||
@qubes.ext.handler('property-pre-set:name', vm=qubes.vm.qubesvm.QubesVM)
|
||||
def pre_rename(self, vm, event, prop, *args):
|
||||
if not vm.dir_path or not os.path.exists(vm.dir_path):
|
||||
return
|
||||
self.appmenus_remove(vm)
|
||||
|
||||
@qubes.ext.handler('property-set:name', vm=qubes.vm.qubesvm.QubesVM)
|
||||
def post_rename(self, vm, event, prop, *args):
|
||||
if not vm.dir_path or not os.path.exists(vm.dir_path):
|
||||
return
|
||||
self.appmenus_create(vm)
|
||||
|
||||
@qubes.ext.handler('domain-create-on-disk')
|
||||
def create_on_disk(self, vm, event):
|
||||
if not vm.dir_path or not os.path.exists(vm.dir_path):
|
||||
return
|
||||
try:
|
||||
source_template = vm.template
|
||||
except AttributeError:
|
||||
source_template = None
|
||||
if vm.updateable and source_template is None:
|
||||
os.mkdir(self.templates_dir(vm))
|
||||
os.mkdir(self.template_icons_dir(vm))
|
||||
if vm.hvm and source_template is None:
|
||||
vm.log.info("Creating appmenus directory: {0}".format(
|
||||
self.templates_dir(vm)))
|
||||
shutil.copy(AppmenusPaths.appmenu_start_hvm_template,
|
||||
self.templates_dir(vm))
|
||||
|
||||
source_whitelist_filename = 'vm-' + AppmenusSubdirs.whitelist
|
||||
if source_template and os.path.exists(
|
||||
os.path.join(source_template.dir_path, source_whitelist_filename)):
|
||||
vm.log.info("Creating default whitelisted apps list: {0}".
|
||||
format(vm.dir_path + '/' + AppmenusSubdirs.whitelist))
|
||||
shutil.copy(
|
||||
os.path.join(source_template.dir_path, source_whitelist_filename),
|
||||
os.path.join(vm.dir_path, AppmenusSubdirs.whitelist))
|
||||
|
||||
if vm.updateable:
|
||||
vm.log.info("Creating/copying appmenus templates")
|
||||
if source_template and os.path.isdir(self.templates_dir(
|
||||
source_template)):
|
||||
shutil.copytree(self.templates_dir(source_template),
|
||||
self.templates_dir(vm))
|
||||
if source_template and os.path.isdir(self.template_icons_dir(
|
||||
source_template)):
|
||||
shutil.copytree(self.template_icons_dir(source_template),
|
||||
self.template_icons_dir(vm))
|
||||
|
||||
# Create appmenus
|
||||
self.appicons_create(vm)
|
||||
self.appmenus_create(vm)
|
||||
|
||||
@qubes.ext.handler('domain-clone-files')
|
||||
def clone_disk_files(self, vm, event, src_vm):
|
||||
if not vm.dir_path or not os.path.exists(vm.dir_path):
|
||||
return
|
||||
if src_vm.updateable and self.templates_dir(vm) is not None and \
|
||||
self.templates_dir(vm) is not None:
|
||||
vm.log.info("Copying the template's appmenus templates "
|
||||
"dir:\n{0} ==>\n{1}".
|
||||
format(self.templates_dir(src_vm),
|
||||
self.templates_dir(vm)))
|
||||
shutil.copytree(self.templates_dir(src_vm),
|
||||
self.templates_dir(vm))
|
||||
|
||||
if src_vm.updateable and self.template_icons_dir(vm) is not None \
|
||||
and self.template_icons_dir(vm) is not None and \
|
||||
os.path.isdir(self.template_icons_dir(src_vm)):
|
||||
vm.log.info("Copying the template's appmenus "
|
||||
"template icons dir:\n{0} ==>\n{1}".
|
||||
format(self.template_icons_dir(src_vm),
|
||||
self.template_icons_dir(vm)))
|
||||
shutil.copytree(self.template_icons_dir(src_vm),
|
||||
self.template_icons_dir(vm))
|
||||
|
||||
for whitelist in (
|
||||
AppmenusSubdirs.whitelist,
|
||||
'vm-' + AppmenusSubdirs.whitelist,
|
||||
'netvm-' + AppmenusSubdirs.whitelist):
|
||||
if os.path.exists(os.path.join(src_vm.dir_path, whitelist)):
|
||||
vm.log.info("Copying whitelisted apps list: {0}".
|
||||
format(whitelist))
|
||||
shutil.copy(os.path.join(src_vm.dir_path, whitelist),
|
||||
os.path.join(vm.dir_path, whitelist))
|
||||
|
||||
# Create appmenus
|
||||
self.appicons_create(vm)
|
||||
self.appmenus_create(vm)
|
||||
|
||||
|
||||
@qubes.ext.handler('domain-remove-from-disk')
|
||||
def remove_from_disk(self, vm, event):
|
||||
self.appmenus_remove(vm)
|
||||
|
||||
|
||||
@qubes.ext.handler('property-set:label')
|
||||
def label_setter(self, vm, event, *args):
|
||||
if not vm.dir_path or not os.path.exists(vm.dir_path):
|
||||
return
|
||||
self.appicons_create(vm, force=True)
|
||||
|
||||
# Apparently desktop environments heavily caches the icons,
|
||||
# see #751 for details
|
||||
if "plasma" in os.environ.get("DESKTOP_SESSION", ""):
|
||||
try:
|
||||
os.unlink(os.path.expandvars(
|
||||
"$HOME/.kde/cache-$HOSTNAME/icon-cache.kcache"))
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
notify_object = dbus.SessionBus().get_object(
|
||||
"org.freedesktop.Notifications",
|
||||
"/org/freedesktop/Notifications")
|
||||
notify_object.Notify(
|
||||
"Qubes", 0, vm.label.icon, "Qubes",
|
||||
"You will need to log off and log in again for the VM icons "
|
||||
"to update in the KDE launcher menu",
|
||||
[], [], 10000,
|
||||
dbus_interface="org.freedesktop.Notifications")
|
||||
except:
|
||||
pass
|
||||
elif "xfce" in os.environ.get("DESKTOP_SESSION", ""):
|
||||
self.appmenus_remove(vm)
|
||||
self.appmenus_create(vm)
|
||||
|
||||
@qubes.ext.handler('property-set:internal')
|
||||
def on_property_set_internal(self, vm, event, prop, newvalue, *args):
|
||||
if not vm.dir_path or not os.path.exists(vm.dir_path):
|
||||
return
|
||||
if len(args):
|
||||
oldvalue = args[0]
|
||||
else:
|
||||
oldvalue = vm.__class__.internal._default
|
||||
if newvalue and not oldvalue:
|
||||
self.appmenus_remove(vm)
|
||||
elif not newvalue and oldvalue:
|
||||
self.appmenus_create(vm)
|
||||
|
||||
@qubes.ext.handler('backup-get-files')
|
||||
def files_for_backup(self, vm, event):
|
||||
if not vm.dir_path or not os.path.exists(vm.dir_path):
|
||||
return
|
||||
if vm.internal:
|
||||
return
|
||||
if vm.updateable:
|
||||
yield self.templates_dir(vm)
|
||||
yield self.template_icons_dir(vm)
|
||||
if os.path.exists(self.whitelist_path(vm)):
|
||||
yield self.whitelist_path(vm)
|
||||
for whitelist in (
|
||||
'vm-' + AppmenusSubdirs.whitelist,
|
||||
'netvm-' + AppmenusSubdirs.whitelist):
|
||||
if os.path.exists(os.path.join(vm.dir_path, whitelist)):
|
||||
yield os.path.join(vm.dir_path, whitelist)
|
||||
|
||||
def appmenus_update(self, vm):
|
||||
'''Update (regenerate) desktop files and icons for this VM and (in
|
||||
case of template) child VMs'''
|
||||
self.appicons_create(vm)
|
||||
self.appmenus_create(vm)
|
||||
if hasattr(vm, 'appvms'):
|
||||
for child_vm in vm.appvms:
|
||||
try:
|
||||
self.appicons_create(child_vm)
|
||||
self.appmenus_create(child_vm, refresh_cache=False)
|
||||
except Exception as e:
|
||||
child_vm.log.error("Failed to recreate appmenus for "
|
||||
"'{0}': {1}".format(child_vm.name,
|
||||
str(e)))
|
||||
subprocess.call(['xdg-desktop-menu', 'forceupdate'])
|
||||
if 'KDE_SESSION_UID' in os.environ:
|
||||
subprocess.call([
|
||||
'kbuildsycoca' + os.environ.get('KDE_SESSION_VERSION',
|
||||
'4')])
|
||||
|
||||
@qubes.ext.handler('template-postinstall')
|
||||
def on_template_postinstall(self, vm, event):
|
||||
import qubesappmenus.receive
|
||||
new_appmenus = qubesappmenus.receive.retrieve_appmenus_templates(
|
||||
vm, use_stdin=False)
|
||||
qubesappmenus.receive.process_appmenus_templates(self, vm, new_appmenus)
|
@ -1,10 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Exec=qubes-vm-settings %VMNAME% applications
|
||||
Icon=qubes-appmenu-select
|
||||
Terminal=false
|
||||
Name=%VMNAME%: Add more shortcuts...
|
||||
GenericName=%VMNAME%: Add more shortcuts...
|
||||
StartupNotify=false
|
||||
Categories=System;X-Qubes-VM;
|
@ -1,5 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Encoding=UTF-8
|
||||
Type=Directory
|
||||
Name=ServiceVM: %VMNAME%
|
||||
Icon=%XDGICON%
|
@ -1,5 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Encoding=UTF-8
|
||||
Type=Directory
|
||||
Name=Template: %VMNAME%
|
||||
Icon=%XDGICON%
|
@ -1,5 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Encoding=UTF-8
|
||||
Type=Directory
|
||||
Name=Domain: %VMNAME%
|
||||
Icon=%XDGICON%
|
@ -1 +0,0 @@
|
||||
/usr/bin/qvm-sync-appmenus
|
@ -1,357 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2011 Marek Marczykowski <marmarek@mimuw.edu.pl>
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
#
|
||||
import optparse
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import pipes
|
||||
|
||||
from optparse import OptionParser
|
||||
import qubes.exc
|
||||
import qubes.tools
|
||||
import qubesappmenus
|
||||
|
||||
import qubesimgconverter
|
||||
|
||||
parser = qubes.tools.QubesArgumentParser(
|
||||
vmname_nargs='?',
|
||||
want_force_root=True,
|
||||
description='retrieve appmenus')
|
||||
|
||||
parser.add_argument('--force-rpc',
|
||||
action='store_true', default=False,
|
||||
help="Force to start a new RPC call, even if called from existing one")
|
||||
|
||||
parser.add_argument('--regenerate-only',
|
||||
action='store_true', default=False,
|
||||
help='Only regenerate appmenus entries, do not synchronize with system '
|
||||
'in template')
|
||||
|
||||
# TODO offline mode
|
||||
|
||||
# fields required to be present (and verified) in retrieved desktop file
|
||||
required_fields = ["Name", "Exec"]
|
||||
|
||||
# limits
|
||||
appmenus_line_size = 1024
|
||||
appmenus_line_count = 100000
|
||||
|
||||
# regexps for sanitization of retrieved values
|
||||
std_re = re.compile(r"^[/a-zA-Z0-9.,:&()_ -]*$")
|
||||
fields_regexp = {
|
||||
"Name": std_re,
|
||||
"GenericName": std_re,
|
||||
"Comment": std_re,
|
||||
"Categories": re.compile(r"^[a-zA-Z0-9/.;:'() -]*$"),
|
||||
"Exec": re.compile(r"^[a-zA-Z0-9()_%&>/{}\"'\\:.= -]*$"),
|
||||
"Icon": re.compile(r"^[a-zA-Z0-9/_.-]*$"),
|
||||
}
|
||||
|
||||
CATEGORIES_WHITELIST = {
|
||||
# Main Categories
|
||||
# http://standards.freedesktop.org/menu-spec/1.1/apa.html 20140507
|
||||
'AudioVideo', 'Audio', 'Video', 'Development', 'Education', 'Game',
|
||||
'Graphics', 'Network', 'Office', 'Science', 'Settings', 'System',
|
||||
'Utility',
|
||||
|
||||
# Additional Categories
|
||||
# http://standards.freedesktop.org/menu-spec/1.1/apas02.html
|
||||
'Building', 'Debugger', 'IDE', 'GUIDesigner', 'Profiling',
|
||||
'RevisionControl', 'Translation', 'Calendar', 'ContactManagement',
|
||||
'Database', 'Dictionary', 'Chart', 'Email', 'Finance', 'FlowChart', 'PDA',
|
||||
'ProjectManagement', 'Presentation', 'Spreadsheet', 'WordProcessor',
|
||||
'2DGraphics', 'VectorGraphics', 'RasterGraphics', '3DGraphics', 'Scanning',
|
||||
'OCR', 'Photography', 'Publishing', 'Viewer', 'TextTools',
|
||||
'DesktopSettings', 'HardwareSettings', 'Printing', 'PackageManager',
|
||||
'Dialup', 'InstantMessaging', 'Chat', 'IRCClient', 'Feed', 'FileTransfer',
|
||||
'HamRadio', 'News', 'P2P', 'RemoteAccess', 'Telephony', 'TelephonyTools',
|
||||
'VideoConference', 'WebBrowser', 'WebDevelopment', 'Midi', 'Mixer',
|
||||
'Sequencer', 'Tuner', 'TV', 'AudioVideoEditing', 'Player', 'Recorder',
|
||||
'DiscBurning', 'ActionGame', 'AdventureGame', 'ArcadeGame', 'BoardGame',
|
||||
'BlocksGame', 'CardGame', 'KidsGame', 'LogicGame', 'RolePlaying',
|
||||
'Shooter', 'Simulation', 'SportsGame', 'StrategyGame', 'Art',
|
||||
'Construction', 'Music', 'Languages', 'ArtificialIntelligence',
|
||||
'Astronomy', 'Biology', 'Chemistry', 'ComputerScience',
|
||||
'DataVisualization', 'Economy', 'Electricity', 'Geography', 'Geology',
|
||||
'Geoscience', 'History', 'Humanities', 'ImageProcessing', 'Literature',
|
||||
'Maps', 'Math', 'NumericalAnalysis', 'MedicalSoftware', 'Physics',
|
||||
'Robotics', 'Spirituality', 'Sports', 'ParallelComputing', 'Amusement',
|
||||
'Archiving', 'Compression', 'Electronics', 'Emulator', 'Engineering',
|
||||
'FileTools', 'FileManager', 'TerminalEmulator', 'Filesystem', 'Monitor',
|
||||
'Security', 'Accessibility', 'Calculator', 'Clock', 'TextEditor',
|
||||
'Documentation', 'Adult', 'Core', 'KDE', 'GNOME', 'XFCE', 'GTK', 'Qt',
|
||||
'Motif', 'Java', 'ConsoleOnly',
|
||||
|
||||
# Reserved Categories (not whitelisted)
|
||||
# http://standards.freedesktop.org/menu-spec/1.1/apas03.html
|
||||
# 'Screensaver', 'TrayIcon', 'Applet', 'Shell',
|
||||
}
|
||||
|
||||
|
||||
def sanitise_categories(untrusted_value):
|
||||
untrusted_categories = (c.strip() for c in untrusted_value.split(';') if c)
|
||||
categories = (c for c in untrusted_categories if c in CATEGORIES_WHITELIST)
|
||||
|
||||
return ';'.join(categories) + ';'
|
||||
|
||||
|
||||
def fallback_hvm_appmenulist():
|
||||
p = subprocess.Popen(["grep", "-rH", "=", "/usr/share/qubes-appmenus/hvm"],
|
||||
stdout=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
return stdout.splitlines()
|
||||
|
||||
|
||||
def get_appmenus(vm):
|
||||
appmenus_line_limit_left = appmenus_line_count
|
||||
untrusted_appmenulist = []
|
||||
if vm is None:
|
||||
while appmenus_line_limit_left > 0:
|
||||
untrusted_line = sys.stdin.readline(appmenus_line_size)
|
||||
if untrusted_line == "":
|
||||
break
|
||||
untrusted_appmenulist.append(untrusted_line.strip())
|
||||
appmenus_line_limit_left -= 1
|
||||
if appmenus_line_limit_left == 0:
|
||||
raise qubes.exc.QubesException("Line count limit exceeded")
|
||||
else:
|
||||
p = vm.run('QUBESRPC qubes.GetAppmenus dom0', passio_popen=True,
|
||||
gui=False)
|
||||
while appmenus_line_limit_left > 0:
|
||||
untrusted_line = p.stdout.readline(appmenus_line_size)
|
||||
if untrusted_line == "":
|
||||
break
|
||||
untrusted_appmenulist.append(untrusted_line.strip())
|
||||
appmenus_line_limit_left -= 1
|
||||
p.wait()
|
||||
if p.returncode != 0:
|
||||
if vm.hvm:
|
||||
untrusted_appmenulist = fallback_hvm_appmenulist()
|
||||
else:
|
||||
raise qubes.exc.QubesException("Error getting application list")
|
||||
if appmenus_line_limit_left == 0:
|
||||
raise qubes.exc.QubesException("Line count limit exceeded")
|
||||
|
||||
appmenus = {}
|
||||
line_rx = re.compile(
|
||||
r"([a-zA-Z0-9.()_-]+.desktop):([a-zA-Z0-9-]+(?:\[[a-zA-Z@_]+\])?)=(.*)")
|
||||
ignore_rx = re.compile(r".*([a-zA-Z0-9._-]+.desktop):(#.*|\s+)$")
|
||||
for untrusted_line in untrusted_appmenulist:
|
||||
# Ignore blank lines and comments
|
||||
if len(untrusted_line) == 0 or ignore_rx.match(untrusted_line):
|
||||
continue
|
||||
# use search instead of match to skip file path
|
||||
untrusted_m = line_rx.search(untrusted_line)
|
||||
if untrusted_m:
|
||||
filename = untrusted_m.group(1)
|
||||
assert '/' not in filename
|
||||
assert '\0' not in filename
|
||||
|
||||
untrusted_key = untrusted_m.group(2)
|
||||
assert '\0' not in untrusted_key
|
||||
assert '\x1b' not in untrusted_key
|
||||
assert '=' not in untrusted_key
|
||||
|
||||
untrusted_value = untrusted_m.group(3)
|
||||
# TODO add key-dependent asserts
|
||||
|
||||
# Look only at predefined keys
|
||||
if untrusted_key in fields_regexp:
|
||||
if fields_regexp[untrusted_key].match(untrusted_value):
|
||||
# now values are sanitized
|
||||
key = untrusted_key
|
||||
if key == 'Categories':
|
||||
value = sanitise_categories(untrusted_value)
|
||||
else:
|
||||
value = untrusted_value
|
||||
|
||||
if filename not in appmenus:
|
||||
appmenus[filename] = {}
|
||||
|
||||
appmenus[filename][key] = value
|
||||
else:
|
||||
print >> sys.stderr, \
|
||||
"Warning: ignoring key %r of %s" % \
|
||||
(untrusted_key, filename)
|
||||
# else: ignore this key
|
||||
|
||||
return appmenus
|
||||
|
||||
|
||||
def create_template(path, values):
|
||||
# check if all required fields are present
|
||||
for key in required_fields:
|
||||
if key not in values:
|
||||
print >> sys.stderr, "Warning: not creating/updating '%s' " \
|
||||
"because of missing '%s' key" % (
|
||||
path, key)
|
||||
return
|
||||
|
||||
desktop_entry = ""
|
||||
desktop_entry += "[Desktop Entry]\n"
|
||||
desktop_entry += "Version=1.0\n"
|
||||
desktop_entry += "Type=Application\n"
|
||||
desktop_entry += "Terminal=false\n"
|
||||
desktop_entry += "X-Qubes-VmName=%VMNAME%\n"
|
||||
|
||||
if 'Icon' in values:
|
||||
icon_file = os.path.splitext(os.path.split(path)[1])[0] + '.png'
|
||||
desktop_entry += "Icon={0}\n".format(os.path.join(
|
||||
'%VMDIR%', qubesappmenus.AppmenusSubdirs.icons_subdir, icon_file))
|
||||
else:
|
||||
desktop_entry += "Icon=%XDGICON%\n"
|
||||
|
||||
for key in ["Name", "GenericName"]:
|
||||
if key in values:
|
||||
desktop_entry += "{0}=%VMNAME%: {1}\n".format(key, values[key])
|
||||
|
||||
# force category X-Qubes-VM
|
||||
values["Categories"] = values.get("Categories", "") + "X-Qubes-VM;"
|
||||
|
||||
for key in ["Comment", "Categories"]:
|
||||
if key in values:
|
||||
desktop_entry += "{0}={1}\n".format(key, values[key])
|
||||
|
||||
desktop_entry += "Exec=qvm-run -q --tray -a %VMNAME% -- {0}\n".format(
|
||||
pipes.quote(values['Exec']))
|
||||
if not os.path.exists(path) or desktop_entry != open(path, "r").read():
|
||||
desktop_file = open(path, "w")
|
||||
desktop_file.write(desktop_entry)
|
||||
desktop_file.close()
|
||||
|
||||
|
||||
def process_appmenus_templates(appmenusext, vm, appmenus):
|
||||
old_umask = os.umask(002)
|
||||
|
||||
if not os.path.exists(appmenusext.templates_dir(vm)):
|
||||
os.mkdir(appmenusext.templates_dir(vm))
|
||||
|
||||
if not os.path.exists(appmenusext.template_icons_dir(vm)):
|
||||
os.mkdir(appmenusext.template_icons_dir(vm))
|
||||
|
||||
if vm.hvm:
|
||||
if not os.path.exists(os.path.join(
|
||||
appmenusext.templates_dir(vm),
|
||||
os.path.basename(
|
||||
qubesappmenus.AppmenusPaths.appmenu_start_hvm_template))):
|
||||
shutil.copy(qubesappmenus.AppmenusPaths.appmenu_start_hvm_template,
|
||||
appmenusext.templates_dir(vm))
|
||||
|
||||
|
||||
for appmenu_file in appmenus.keys():
|
||||
if os.path.exists(
|
||||
os.path.join(appmenusext.templates_dir(vm),
|
||||
appmenu_file)):
|
||||
vm.log.info("Updating {0}".format(appmenu_file))
|
||||
else:
|
||||
vm.log.info("Creating {0}".format(appmenu_file))
|
||||
|
||||
# TODO: icons support in offline mode
|
||||
# TODO if options.offline_mode:
|
||||
# TODO new_appmenus[appmenu_file].pop('Icon', None)
|
||||
if 'Icon' in appmenus[appmenu_file]:
|
||||
# the following line is used for time comparison
|
||||
icondest = os.path.join(appmenusext.template_icons_dir(vm),
|
||||
os.path.splitext(appmenu_file)[0] + '.png')
|
||||
|
||||
try:
|
||||
icon = qubesimgconverter.Image. \
|
||||
get_xdg_icon_from_vm(vm, appmenus[appmenu_file]['Icon'])
|
||||
if os.path.exists(icondest):
|
||||
old_icon = qubesimgconverter.Image.load_from_file(icondest)
|
||||
else:
|
||||
old_icon = None
|
||||
if old_icon is None or icon != old_icon:
|
||||
icon.save(icondest)
|
||||
except Exception as e:
|
||||
vm.log.warning('Failed to get icon for {0}: {1!s}'.\
|
||||
format(appmenu_file, e))
|
||||
|
||||
if os.path.exists(icondest):
|
||||
vm.log.warning('Found old icon, using it instead')
|
||||
else:
|
||||
del appmenus[appmenu_file]['Icon']
|
||||
|
||||
create_template(os.path.join(appmenusext.templates_dir(vm),
|
||||
appmenu_file), appmenus[appmenu_file])
|
||||
|
||||
# Delete appmenus of removed applications
|
||||
for appmenu_file in os.listdir(appmenusext.templates_dir(vm)):
|
||||
if not appmenu_file.endswith('.desktop'):
|
||||
continue
|
||||
|
||||
if appmenu_file not in appmenus:
|
||||
vm.log.info("Removing {0}".format(appmenu_file))
|
||||
os.unlink(os.path.join(appmenusext.templates_dir(vm),
|
||||
appmenu_file))
|
||||
|
||||
os.umask(old_umask)
|
||||
|
||||
|
||||
def retrieve_appmenus_templates(vm, use_stdin=True):
|
||||
'''Retrieve appmenus from the VM. If not running in offline mode,
|
||||
additionally retrieve application icons and store them into
|
||||
:py:metch:`template_icons_dir`.
|
||||
|
||||
Returns: dict of desktop entries, each being dict itself.
|
||||
'''
|
||||
if hasattr(vm, 'template'):
|
||||
raise qubes.exc.QubesException(
|
||||
"To sync appmenus for template based VM, do it on template instead")
|
||||
|
||||
if not vm.is_running():
|
||||
raise qubes.exc.QubesVMNotRunningError(vm,
|
||||
"Appmenus can be retrieved only from running VM")
|
||||
|
||||
new_appmenus = get_appmenus(vm if not use_stdin else None)
|
||||
|
||||
if len(new_appmenus) == 0:
|
||||
raise qubes.exc.QubesException("No appmenus received, terminating")
|
||||
return new_appmenus
|
||||
|
||||
|
||||
def main(args=None):
|
||||
env_vmname = os.environ.get("QREXEC_REMOTE_DOMAIN")
|
||||
|
||||
args = parser.parse_args(args)
|
||||
|
||||
if env_vmname:
|
||||
vm = args.app.domains[env_vmname]
|
||||
else:
|
||||
vm = args.domains[0]
|
||||
|
||||
if vm is None:
|
||||
parser.error("You must specify at least the VM name!")
|
||||
|
||||
if env_vmname is None or args.force_rpc:
|
||||
use_stdin = False
|
||||
else:
|
||||
use_stdin = True
|
||||
appmenusext = qubesappmenus.AppmenusExtension()
|
||||
if not args.regenerate_only:
|
||||
new_appmenus = retrieve_appmenus_templates(vm, use_stdin=use_stdin)
|
||||
process_appmenus_templates(appmenusext, vm, new_appmenus)
|
||||
appmenusext.appmenus_update(vm)
|
@ -1,383 +0,0 @@
|
||||
#!/usr/bin/python2
|
||||
# coding=utf-8
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2016 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# 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, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
|
||||
# USA.
|
||||
|
||||
import colorsys
|
||||
import os
|
||||
|
||||
import unittest
|
||||
import pkg_resources
|
||||
import xdg
|
||||
import xdg.BaseDirectory
|
||||
import xdg.DesktopEntry
|
||||
import qubes
|
||||
import qubes.tests
|
||||
import qubes.tests.extra
|
||||
import qubes.vm.appvm
|
||||
import qubes.vm.templatevm
|
||||
import qubesappmenus
|
||||
import qubesappmenus.receive
|
||||
import qubesimgconverter
|
||||
|
||||
|
||||
class TestApp(object):
|
||||
labels = {1: qubes.Label(1, '0xcc0000', 'red')}
|
||||
|
||||
def __init__(self):
|
||||
self.domains = {}
|
||||
|
||||
|
||||
class TestVM(object):
|
||||
# pylint: disable=too-few-public-methods
|
||||
app = TestApp()
|
||||
|
||||
def __init__(self, name, **kwargs):
|
||||
self.running = False
|
||||
self.installed_by_rpm = False
|
||||
self.is_template = False
|
||||
self.name = name
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def is_running(self):
|
||||
return self.running
|
||||
|
||||
|
||||
class TC_00_Appmenus(qubes.tests.QubesTestCase):
|
||||
"""Unittests for appmenus, theoretically runnable from git checkout"""
|
||||
def setUp(self):
|
||||
super(TC_00_Appmenus, self).setUp()
|
||||
vmname = qubes.tests.VMPREFIX + 'standalone'
|
||||
self.standalone = TestVM(
|
||||
name=vmname,
|
||||
dir_path=os.path.join(qubes.config.qubes_base_dir, 'appvms',
|
||||
vmname),
|
||||
updateable=True,
|
||||
)
|
||||
vmname = qubes.tests.VMPREFIX + 'template'
|
||||
self.template = TestVM(
|
||||
name=vmname,
|
||||
dir_path=os.path.join(
|
||||
qubes.config.qubes_base_dir,
|
||||
'vm-templates', vmname),
|
||||
is_template=True,
|
||||
updateable=True,
|
||||
)
|
||||
vmname = qubes.tests.VMPREFIX + 'vm'
|
||||
self.appvm = TestVM(
|
||||
name=vmname,
|
||||
dir_path=os.path.join(
|
||||
qubes.config.qubes_base_dir,
|
||||
'appvms', vmname),
|
||||
template=self.template,
|
||||
updateable=False,
|
||||
)
|
||||
self.app = TestApp()
|
||||
self.ext = qubesappmenus.AppmenusExtension()
|
||||
|
||||
def test_000_templates_dir(self):
|
||||
self.assertEquals(
|
||||
self.ext.templates_dir(self.standalone),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'appvms',
|
||||
self.standalone.name, 'apps.templates')
|
||||
)
|
||||
self.assertEquals(
|
||||
self.ext.templates_dir(self.template),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'vm-templates',
|
||||
self.template.name, 'apps.templates')
|
||||
)
|
||||
self.assertEquals(
|
||||
self.ext.templates_dir(self.appvm),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'vm-templates',
|
||||
self.template.name, 'apps.templates')
|
||||
)
|
||||
|
||||
def test_001_template_icons_dir(self):
|
||||
self.assertEquals(
|
||||
self.ext.template_icons_dir(self.standalone),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'appvms',
|
||||
self.standalone.name, 'apps.tempicons')
|
||||
)
|
||||
self.assertEquals(
|
||||
self.ext.template_icons_dir(self.template),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'vm-templates',
|
||||
self.template.name, 'apps.tempicons')
|
||||
)
|
||||
self.assertEquals(
|
||||
self.ext.template_icons_dir(self.appvm),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'vm-templates',
|
||||
self.template.name, 'apps.tempicons')
|
||||
)
|
||||
|
||||
def test_002_appmenus_dir(self):
|
||||
self.assertEquals(
|
||||
self.ext.appmenus_dir(self.standalone),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'appvms',
|
||||
self.standalone.name, 'apps')
|
||||
)
|
||||
self.assertEquals(
|
||||
self.ext.appmenus_dir(self.template),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'vm-templates',
|
||||
self.template.name, 'apps')
|
||||
)
|
||||
self.assertEquals(
|
||||
self.ext.appmenus_dir(self.appvm),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'appvms',
|
||||
self.appvm.name, 'apps')
|
||||
)
|
||||
|
||||
def test_003_icons_dir(self):
|
||||
self.assertEquals(
|
||||
self.ext.icons_dir(self.standalone),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'appvms',
|
||||
self.standalone.name, 'apps.icons')
|
||||
)
|
||||
self.assertEquals(
|
||||
self.ext.icons_dir(self.template),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'vm-templates',
|
||||
self.template.name, 'apps.icons')
|
||||
)
|
||||
self.assertEquals(
|
||||
self.ext.icons_dir(self.appvm),
|
||||
os.path.join(qubes.config.qubes_base_dir, 'appvms',
|
||||
self.appvm.name, 'apps.icons')
|
||||
)
|
||||
|
||||
def test_100_get_appmenus(self):
|
||||
def _run(cmd, **kwargs):
|
||||
class PopenMockup(object):
|
||||
pass
|
||||
self.assertEquals(cmd, 'QUBESRPC qubes.GetAppmenus dom0')
|
||||
self.assertEquals(kwargs.get('passio_popen', False), True)
|
||||
self.assertEquals(kwargs.get('gui', True), False)
|
||||
p = PopenMockup()
|
||||
p.stdout = pkg_resources.resource_stream(__name__,
|
||||
'test-data/appmenus.input')
|
||||
p.wait = lambda: None
|
||||
p.returncode = 0
|
||||
return p
|
||||
vm = TestVM('test-vm', run=_run)
|
||||
appmenus = qubesappmenus.receive.get_appmenus(vm)
|
||||
expected_appmenus = {
|
||||
'org.gnome.Nautilus.desktop': {
|
||||
'Name': 'Files',
|
||||
'Comment': 'Access and organize files',
|
||||
'Categories': 'GNOME;GTK;Utility;Core;FileManager;',
|
||||
'Exec': 'qubes-desktop-run '
|
||||
'/usr/share/applications/org.gnome.Nautilus.desktop',
|
||||
'Icon': 'system-file-manager',
|
||||
},
|
||||
'org.gnome.Weather.Application.desktop': {
|
||||
'Name': 'Weather',
|
||||
'Comment': 'Show weather conditions and forecast',
|
||||
'Categories': 'GNOME;GTK;Utility;Core;',
|
||||
'Exec': 'qubes-desktop-run '
|
||||
'/usr/share/applications/org.gnome.Weather.Application.desktop',
|
||||
'Icon': 'org.gnome.Weather.Application',
|
||||
},
|
||||
'org.gnome.Cheese.desktop': {
|
||||
'Name': 'Cheese',
|
||||
'GenericName': 'Webcam Booth',
|
||||
'Comment': 'Take photos and videos with your webcam, with fun graphical effects',
|
||||
'Categories': 'GNOME;AudioVideo;Video;Recorder;',
|
||||
'Exec': 'qubes-desktop-run '
|
||||
'/usr/share/applications/org.gnome.Cheese.desktop',
|
||||
'Icon': 'cheese',
|
||||
},
|
||||
'evince.desktop': {
|
||||
'Name': 'Document Viewer',
|
||||
'Comment': 'View multi-page documents',
|
||||
'Categories': 'GNOME;GTK;Office;Viewer;Graphics;2DGraphics;VectorGraphics;',
|
||||
'Exec': 'qubes-desktop-run '
|
||||
'/usr/share/applications/evince.desktop',
|
||||
'Icon': 'evince',
|
||||
},
|
||||
}
|
||||
self.assertEquals(expected_appmenus, appmenus)
|
||||
|
||||
|
||||
class TC_10_AppmenusIntegration(qubes.tests.extra.ExtraTestCase):
|
||||
def setUp(self):
|
||||
super(TC_10_AppmenusIntegration, self).setUp()
|
||||
self.vm = self.create_vms(['vm'])[0]
|
||||
self.appmenus = qubesappmenus.AppmenusExtension()
|
||||
|
||||
def assertPathExists(self, path):
|
||||
if not os.path.exists(path):
|
||||
self.fail("Path {} does not exist".format(path))
|
||||
|
||||
def assertPathNotExists(self, path):
|
||||
if os.path.exists(path):
|
||||
self.fail("Path {} exists while it should not".format(path))
|
||||
|
||||
def get_whitelist(self, whitelist_path):
|
||||
self.assertPathExists(whitelist_path)
|
||||
with open(whitelist_path) as f:
|
||||
whitelisted = [x.rstrip() for x in f.readlines()]
|
||||
return whitelisted
|
||||
|
||||
def test_000_created(self, vm=None):
|
||||
if vm is None:
|
||||
vm = self.vm
|
||||
whitelist_path = os.path.join(vm.dir_path,
|
||||
qubesappmenus.AppmenusSubdirs.whitelist)
|
||||
whitelisted = self.get_whitelist(whitelist_path)
|
||||
self.assertPathExists(self.appmenus.appmenus_dir(vm))
|
||||
appmenus = os.listdir(self.appmenus.appmenus_dir(vm))
|
||||
self.assertTrue(all(x.startswith(vm.name + '-') for x in appmenus))
|
||||
appmenus = [x[len(vm.name) + 1:] for x in appmenus]
|
||||
self.assertIn('vm.directory', appmenus)
|
||||
appmenus.remove('vm.directory')
|
||||
self.assertIn('qubes-appmenu-select.desktop', appmenus)
|
||||
appmenus.remove('qubes-appmenu-select.desktop')
|
||||
self.assertEquals(set(whitelisted), set(appmenus))
|
||||
self.assertPathExists(self.appmenus.icons_dir(vm))
|
||||
appicons = os.listdir(self.appmenus.icons_dir(vm))
|
||||
whitelisted_icons = set()
|
||||
for appmenu in whitelisted:
|
||||
desktop = xdg.DesktopEntry.DesktopEntry(
|
||||
os.path.join(self.appmenus.appmenus_dir(vm),
|
||||
'-'.join((vm.name, appmenu))))
|
||||
if desktop.getIcon():
|
||||
whitelisted_icons.add(os.path.basename(desktop.getIcon()))
|
||||
self.assertEquals(set(whitelisted_icons), set(appicons))
|
||||
|
||||
def test_001_created_registered(self):
|
||||
"""Check whether appmenus was registered in desktop environment"""
|
||||
whitelist_path = os.path.join(self.vm.dir_path,
|
||||
qubesappmenus.AppmenusSubdirs.whitelist)
|
||||
if not os.path.exists(whitelist_path):
|
||||
self.skipTest("Appmenus whitelist does not exists")
|
||||
whitelisted = self.get_whitelist(whitelist_path)
|
||||
for appmenu in whitelisted:
|
||||
if appmenu.endswith('.directory'):
|
||||
subdir = 'desktop-directories'
|
||||
else:
|
||||
subdir = 'applications'
|
||||
self.assertPathExists(os.path.join(
|
||||
xdg.BaseDirectory.xdg_data_home, subdir,
|
||||
'-'.join([self.vm.name, appmenu])))
|
||||
# TODO: some KDE specific dir?
|
||||
|
||||
def test_002_unregistered_after_remove(self):
|
||||
"""Check whether appmenus was unregistered after VM removal"""
|
||||
whitelist_path = os.path.join(self.vm.dir_path,
|
||||
qubesappmenus.AppmenusSubdirs.whitelist)
|
||||
if not os.path.exists(whitelist_path):
|
||||
self.skipTest("Appmenus whitelist does not exists")
|
||||
whitelisted = self.get_whitelist(whitelist_path)
|
||||
self.vm.remove_from_disk()
|
||||
for appmenu in whitelisted:
|
||||
if appmenu.endswith('.directory'):
|
||||
subdir = 'desktop-directories'
|
||||
else:
|
||||
subdir = 'applications'
|
||||
self.assertPathNotExists(os.path.join(
|
||||
xdg.BaseDirectory.xdg_data_home, subdir,
|
||||
'-'.join([self.vm.name, appmenu])))
|
||||
|
||||
def test_003_created_template_empty(self):
|
||||
tpl = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
|
||||
name=self.make_vm_name('tpl'), label='red')
|
||||
tpl.create_on_disk()
|
||||
self.assertPathExists(self.appmenus.templates_dir(tpl))
|
||||
self.assertPathExists(self.appmenus.template_icons_dir(tpl))
|
||||
|
||||
def test_004_created_template_from_other(self):
|
||||
tpl = self.app.add_new_vm(qubes.vm.templatevm.TemplateVM,
|
||||
name=self.make_vm_name('tpl'), label='red')
|
||||
tpl.clone_disk_files(self.app.default_template)
|
||||
self.assertPathExists(self.appmenus.templates_dir(tpl))
|
||||
self.assertPathExists(self.appmenus.template_icons_dir(tpl))
|
||||
self.assertPathExists(os.path.join(tpl.dir_path,
|
||||
qubesappmenus.AppmenusSubdirs.whitelist))
|
||||
|
||||
for appmenu in os.listdir(self.appmenus.templates_dir(
|
||||
self.app.default_template)):
|
||||
self.assertPathExists(os.path.join(
|
||||
self.appmenus.templates_dir(tpl), appmenu))
|
||||
|
||||
for appicon in os.listdir(self.appmenus.template_icons_dir(
|
||||
self.app.default_template)):
|
||||
self.assertPathExists(os.path.join(
|
||||
self.appmenus.template_icons_dir(tpl), appicon))
|
||||
|
||||
def get_image_color(self, path, expected_color):
|
||||
"""Return mean color of the image as (r, g, b) in float"""
|
||||
image = qubesimgconverter.Image.load_from_file(path)
|
||||
_, l, _ = colorsys.rgb_to_hls(
|
||||
*qubesimgconverter.hex_to_float(expected_color))
|
||||
|
||||
def get_hls(pixels, l):
|
||||
for i in xrange(0, len(pixels), 4):
|
||||
r, g, b, a = tuple(ord(c) / 255. for c in pixels[i:i + 4])
|
||||
if a == 0.0:
|
||||
continue
|
||||
h, _, s = colorsys.rgb_to_hls(r, g, b)
|
||||
yield h, l, s
|
||||
|
||||
mean_hls = reduce(
|
||||
lambda x, y: (x[0] + y[0], x[1] + y[1], x[2] + y[2]),
|
||||
get_hls(image.data, l),
|
||||
(0, 0, 0)
|
||||
)
|
||||
mean_hls = map(lambda x: x / (mean_hls[1] / l), mean_hls)
|
||||
image_color = colorsys.hls_to_rgb(*mean_hls)
|
||||
return image_color
|
||||
|
||||
def assertIconColor(self, path, expected_color):
|
||||
image_color_float = self.get_image_color(path, expected_color)
|
||||
expected_color_float = qubesimgconverter.hex_to_float(expected_color)
|
||||
if not all(map(lambda a, b: abs(a - b) <= 0.15,
|
||||
image_color_float, expected_color_float)):
|
||||
self.fail(
|
||||
"Icon {} is not colored as {}".format(path, expected_color))
|
||||
|
||||
def test_010_icon_color(self, vm=None):
|
||||
if vm is None:
|
||||
vm = self.vm
|
||||
icons_dir = self.appmenus.icons_dir(vm)
|
||||
appicons = os.listdir(icons_dir)
|
||||
for icon in appicons:
|
||||
self.assertIconColor(os.path.join(icons_dir, icon),
|
||||
vm.label.color)
|
||||
|
||||
def test_011_icon_color_label_change(self):
|
||||
"""Regression test for #1606"""
|
||||
self.vm.label = 'green'
|
||||
self.test_010_icon_color()
|
||||
|
||||
def test_020_clone(self):
|
||||
vm2 = self.app.add_new_vm(qubes.vm.appvm.AppVM,
|
||||
name=self.make_vm_name('vm2'), label='green')
|
||||
|
||||
vm2.clone_properties(self.vm)
|
||||
vm2.clone_disk_files(self.vm)
|
||||
self.test_000_created(vm=vm2)
|
||||
self.test_010_icon_color(vm=vm2)
|
||||
|
||||
|
||||
def list_tests():
|
||||
return (
|
||||
TC_00_Appmenus,
|
||||
TC_10_AppmenusIntegration,
|
||||
)
|
@ -76,7 +76,6 @@ ln -sf . %{name}-%{version}
|
||||
%setup -T -D
|
||||
|
||||
%build
|
||||
python setup.py build
|
||||
(cd dom0-updates; make)
|
||||
(cd qrexec; make)
|
||||
(cd file-copy-vm; make)
|
||||
@ -84,17 +83,10 @@ python setup.py build
|
||||
|
||||
%install
|
||||
|
||||
### Appmenus
|
||||
# force /usr/bin before /bin to have /usr/bin/python instead of /bin/python
|
||||
PATH="/usr/bin:$PATH" python setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT
|
||||
|
||||
mkdir -p $RPM_BUILD_ROOT/etc/qubes-rpc/policy
|
||||
cp qubesappmenus/qubes.SyncAppMenus $RPM_BUILD_ROOT/etc/qubes-rpc/
|
||||
## Appmenus
|
||||
install -d $RPM_BUILD_ROOT/etc/qubes-rpc/policy
|
||||
cp qubesappmenus/qubes.SyncAppMenus.policy $RPM_BUILD_ROOT/etc/qubes-rpc/policy/qubes.SyncAppMenus
|
||||
|
||||
mkdir -p $RPM_BUILD_ROOT/usr/share/qubes-appmenus/
|
||||
cp -r appmenus-files/* $RPM_BUILD_ROOT/usr/share/qubes-appmenus/
|
||||
|
||||
### Dom0 updates
|
||||
install -D dom0-updates/qubes-dom0-updates.cron $RPM_BUILD_ROOT/etc/cron.daily/qubes-dom0-updates.cron
|
||||
install -D dom0-updates/qubes-dom0-update $RPM_BUILD_ROOT/usr/bin/qubes-dom0-update
|
||||
@ -156,12 +148,6 @@ install -m 755 file-copy-vm/qfile-dom0-agent $RPM_BUILD_ROOT/usr/lib/qubes/
|
||||
install -m 755 file-copy-vm/qvm-copy-to-vm $RPM_BUILD_ROOT/usr/bin/
|
||||
install -m 755 file-copy-vm/qvm-move-to-vm $RPM_BUILD_ROOT/usr/bin/
|
||||
|
||||
### Icons
|
||||
mkdir -p $RPM_BUILD_ROOT/usr/share/qubes/icons
|
||||
for icon in icons/*.png; do
|
||||
convert -resize 48 $icon $RPM_BUILD_ROOT/usr/share/qubes/$icon
|
||||
done
|
||||
|
||||
### Documentation
|
||||
(cd doc; make DESTDIR=$RPM_BUILD_ROOT install)
|
||||
|
||||
@ -172,13 +158,6 @@ fi
|
||||
|
||||
%post
|
||||
|
||||
for i in /usr/share/qubes/icons/*.png ; do
|
||||
xdg-icon-resource install --noupdate --novendor --size 48 $i
|
||||
done
|
||||
xdg-icon-resource forceupdate
|
||||
|
||||
xdg-desktop-menu install /usr/share/qubes-appmenus/qubes-dispvm.directory /usr/share/qubes-appmenus/qubes-dispvm-*.desktop
|
||||
|
||||
/usr/lib/qubes/patch-dnf-yum-config
|
||||
|
||||
systemctl enable qubes-suspend.service >/dev/null 2>&1
|
||||
@ -187,12 +166,6 @@ systemctl enable qubes-suspend.service >/dev/null 2>&1
|
||||
if [ "$1" = 0 ] ; then
|
||||
# no more packages left
|
||||
|
||||
for i in /usr/share/qubes/icons/*.png ; do
|
||||
xdg-icon-resource uninstall --novendor --size 48 $i
|
||||
done
|
||||
|
||||
xdg-desktop-menu uninstall /usr/share/qubes-appmenus/qubes-dispvm.directory /usr/share/qubes-appmenus/qubes-dispvm-*.desktop
|
||||
|
||||
systemctl disable qubes-suspend.service > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
@ -208,27 +181,7 @@ rm -f /lib/udev/rules.d/69-xorg-vmmouse.rules
|
||||
chmod -x /etc/grub.d/10_linux
|
||||
|
||||
%files
|
||||
%attr(2775,root,qubes) %dir /etc/qubes-rpc
|
||||
%attr(2775,root,qubes) %dir /etc/qubes-rpc/policy
|
||||
%dir %{python_sitelib}/qubeslinux-*.egg-info
|
||||
%{python_sitelib}/qubeslinux-*.egg-info/*
|
||||
/usr/lib/python2.7/site-packages/qubesappmenus/__init__.py*
|
||||
/usr/lib/python2.7/site-packages/qubesappmenus/receive.py*
|
||||
/usr/lib/python2.7/site-packages/qubesappmenus/qubes-appmenu-select.desktop.template
|
||||
/usr/lib/python2.7/site-packages/qubesappmenus/qubes-servicevm.directory.template
|
||||
/usr/lib/python2.7/site-packages/qubesappmenus/qubes-templatevm.directory.template
|
||||
/usr/lib/python2.7/site-packages/qubesappmenus/qubes-vm.directory.template
|
||||
/usr/lib/python2.7/site-packages/qubesappmenus/tests.py*
|
||||
/usr/lib/python2.7/site-packages/qubesappmenus/test-data
|
||||
/etc/qubes-rpc/policy/qubes.SyncAppMenus
|
||||
/etc/qubes-rpc/qubes.SyncAppMenus
|
||||
/usr/share/qubes-appmenus/qubes-dispvm-firefox.desktop
|
||||
/usr/share/qubes-appmenus/qubes-dispvm-xterm.desktop
|
||||
/usr/share/qubes-appmenus/qubes-dispvm.directory
|
||||
/usr/share/qubes-appmenus/qubes-start.desktop
|
||||
/usr/share/qubes-appmenus/hvm
|
||||
/usr/share/qubes/icons/*.png
|
||||
/usr/bin/qvm-sync-appmenus
|
||||
# Dom0 updates
|
||||
/etc/cron.daily/qubes-dom0-updates.cron
|
||||
/etc/yum.real.repos.d/qubes-cached.repo
|
||||
|
32
setup.py
@ -1,32 +0,0 @@
|
||||
# vim: fileencoding=utf-8
|
||||
|
||||
import setuptools
|
||||
|
||||
if __name__ == '__main__':
|
||||
setuptools.setup(
|
||||
name='qubeslinux',
|
||||
version=open('version').read().strip(),
|
||||
author='Invisible Things Lab',
|
||||
author_email='woju@invisiblethingslab.com',
|
||||
description='Qubes core-linux package',
|
||||
license='GPL2+',
|
||||
url='https://www.qubes-os.org/',
|
||||
|
||||
packages=('qubesappmenus',),
|
||||
|
||||
package_data = {
|
||||
'qubesappmenus': ['test-data/*', '*.template'],
|
||||
},
|
||||
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'qvm-sync-appmenus = qubesappmenus.receive:main'
|
||||
],
|
||||
'qubes.ext': [
|
||||
'qubesappmenus = qubesappmenus:AppmenusExtension'
|
||||
],
|
||||
'qubes.tests.extra': [
|
||||
'qubesappmenus = qubesappmenus.tests:list_tests',
|
||||
],
|
||||
}
|
||||
)
|