Repository: QubesOS/qubes-manager
Pull request #412: Settings applications tab
Author: login: alimirjamali
Labels: name: openqa-pending
Created: 2025-03-07T23:38:31Z | Merged: 2025-03-10T22:14:06Z

This PR contains 3 individual commits:

1. Fix for Drag-and-Drop in Qubes VM Settings GUI -> Applications tab
2. PyQt bindings for QImage tinting
3. Icons for Qubes VM Settings GUI -> Applications tab

fixes: https://github.com/QubesOS/qubes-issues/issues/9829


Diff:
diff --git a/.gitignore b/.gitignore
index dc443f40b..064ea19de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
 rpm/
 pkgs/
 debian/changelog.*
+.coverage
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 21a42d08a..bf79edb7e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,6 +4,8 @@ checks:pylint:
   - pip3 install --quiet -r ci/requirements.txt
   - git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
   - (cd ~/core-admin-client;python3 setup.py egg_info)
+  - git clone https://github.com/QubesOS/qubes-linux-utils ~/linux-utils
+  - (cd ~/linux-utils/imgconverter;sudo python3 setup.py install)
   script:
   - PYTHONPATH=~/core-admin-client python3 -m pylint qubesmanager
   stage: checks
@@ -27,6 +29,8 @@ checks:tests:
     - pip3 install --quiet -r ci/requirements.txt
     - git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
     - (cd ~/core-admin-client;python3 setup.py egg_info)
+    - git clone https://github.com/QubesOS/qubes-linux-utils ~/linux-utils
+    - (cd ~/linux-utils/imgconverter;sudo python3 setup.py install)
   script:
     - make ui
     - make res
diff --git a/ci/requirements.txt b/ci/requirements.txt
index 70b29c187..5249b0ee9 100644
--- a/ci/requirements.txt
+++ b/ci/requirements.txt
@@ -9,3 +9,5 @@ pylint
 sphinx
 PyYAML
 qasync
+Pillow
+numpy
diff --git a/debian/control b/debian/control
index cf86c27b4..687b67a9e 100644
--- a/debian/control
+++ b/debian/control
@@ -23,6 +23,7 @@ Package: qubes-manager
 Architecture: any
 Depends:
   python3-qubesadmin (>= 4.3.7),
+  python3-qubesimgconverter,
   python3-pyqt6,
   python3-pyinotify,
   python3-qasync,
diff --git a/debian/install b/debian/install
index 411e25ff7..cbd682c3f 100644
--- a/debian/install
+++ b/debian/install
@@ -61,6 +61,7 @@
 /usr/lib/*/dist-packages/qubesmanager/tests/test_qube_manager.py
 /usr/lib/*/dist-packages/qubesmanager/tests/test_vm_settings.py
 /usr/lib/*/dist-packages/qubesmanager/tests/test_clone_vm.py
+/usr/lib/*/dist-packages/qubesmanager/tests/test_utils.py
 
 /usr/lib/*/dist-packages/qubesmanager-*.egg-info/*
 
diff --git a/qubesmanager/appmenu_select.py b/qubesmanager/appmenu_select.py
index 4d3c002ed..d29577156 100755
--- a/qubesmanager/appmenu_select.py
+++ b/qubesmanager/appmenu_select.py
@@ -18,12 +18,14 @@
 #
 
 import subprocess
-from PyQt6 import QtWidgets, QtCore  # pylint: disable=import-error
+from PyQt6 import QtWidgets, QtCore, QtGui  # pylint: disable=import-error
 from qubesadmin import exc
+from qubesmanager.utils import tint_qimage
+from os import path
 
-# TODO description in tooltip
-# TODO icon
 # pylint: disable=too-few-public-methods
+
+
 class AppListWidgetItem(QtWidgets.QListWidgetItem):
     def __init__(self, name, ident, tooltip=None, parent=None):
         super().__init__(name, parent)
@@ -33,44 +35,54 @@ def __init__(self, name, ident, tooltip=None, parent=None):
         else:
             tooltip += "\n" + additional_description
         self.setToolTip(tooltip)
-        self.ident = ident
+        self.setWhatsThis(ident)
         # Using identity as tooltip which also enables drag-and-drop
         self.setWhatsThis(ident)
 
     @classmethod
     def from_line(cls, line):
-        ident, name, comment = line.split('|', maxsplit=3)
+        ident, name, comment = line.split("|", maxsplit=3)
         return cls(name=name, ident=ident, tooltip=comment)
 
     @classmethod
     def from_ident(cls, ident):
-        name = 'Application missing in template! ({})'.format(ident)
-        comment = 'The listed application was available at some point to ' \
-                  'this qube, but not any more. The most likely cause is ' \
-                  'template change. Install the application in the template ' \
-                  'if you want to restore it.'
+        name = "Application missing in template! ({})".format(ident)
+        comment = (
+            "The listed application was available at some point to "
+            "this qube, but not any more. The most likely cause is "
+            "template change. Install the application in the template "
+            "if you want to restore it."
+        )
         return cls(name=name, ident=ident, tooltip=comment)
 
 
 class AppmenuSelectManager:
     def __init__(self, vm, apps_multiselect):
         self.vm = vm
-        self.app_list = apps_multiselect # this is a multiselect wiget
+        self.app_list = apps_multiselect  # this is a multiselect wiget
         self.whitelisted = None
         self.has_missing = False
         self.fill_apps_list(template=None)
 
     def fill_apps_list(self, template=None):
         try:
-            self.whitelisted = [line for line in subprocess.check_output(
-                    ['qvm-appmenus', '--get-whitelist', self.vm.name]
-                ).decode().strip().split('\n') if line]
+            self.whitelisted = [
+                line
+                for line in subprocess.check_output(
+                    ["qvm-appmenus", "--get-whitelist", self.vm.name]
+                )
+                .decode()
+                .strip()
+                .split("\n")
+                if line
+            ]
         except exc.QubesException:
             self.whitelisted = []
 
         currently_selected = [
-            self.app_list.selected_list.item(i).ident
-            for i in range(self.app_list.selected_list.count())]
+            self.app_list.selected_list.item(i).whatsThis()
+            for i in range(self.app_list.selected_list.count())
+        ]
 
         whitelist = set(self.whitelisted + currently_selected)
 
@@ -81,25 +93,68 @@ def fill_apps_list(self, template=None):
 
         self.app_list.clear()
 
-        command = ['qvm-appmenus', '--get-available',
-                   '--i-understand-format-is-unstable', '--file-field',
-                   'Comment']
+        command = [
+            "qvm-appmenus",
+            "--get-available",
+            "--i-understand-format-is-unstable",
+            "--file-field",
+            "Comment",
+            "--file-field",
+            "Icon",
+        ]
         if template:
-            command.extend(['--template', template.name])
+            command.extend(["--template", template.name])
         command.append(self.vm.name)
 
+        if not hasattr(self.vm, "template"):
+            # TemplateVMs and StandaloneVMs
+            main_template = self.vm.name
+        elif not hasattr(self.vm.template, "template"):
+            # AppVMs
+            main_template = self.vm.template.name
+        else:
+            # DispVMs
+            main_template = self.vm.template.template.name
+
+        template_icons_path = path.join(
+            path.expanduser("~"),
+            ".local",
+            "share",
+            "qubes-appmenus",
+            f"{main_template}",
+            "apps.tempicons",
+        )
+
         try:
-            available_appmenus = [
-                AppListWidgetItem.from_line(line)
-                for line in subprocess.check_output(
-                    command).decode().splitlines()]
+            available_appmenus = []
+            for line in subprocess.check_output(command).decode().splitlines():
+                ident, name, comment, icon_path = line.split("|", maxsplit=4)
+                app_item = AppListWidgetItem.from_line(
+                    "|".join([ident, name, comment])
+                )
+                icon_path = icon_path.replace(
+                    "%VMDIR%/apps.icons", template_icons_path
+                )
+                if path.exists(icon_path):
+                    icon = QtGui.QIcon(icon_path)
+                    qpixmap = icon.pixmap(QtCore.QSize(512, 512))
+                    qimage = QtGui.QImage(qpixmap)
+                    qimage = tint_qimage(qimage, self.vm.label.color)
+                    qpixmap = QtGui.QPixmap(qimage)
+                    icon = QtGui.QIcon(qpixmap)
+                else:
+                    # for .desktop files with  missing icons
+                    icon = QtGui.QIcon.fromTheme(self.vm.icon)
+                app_item.setIcon(icon)
+                available_appmenus.append(app_item)
+
         except exc.QubesException:
             available_appmenus = []
 
         for app in available_appmenus:
-            if app.ident in whitelist:
+            if app.whatsThis() in whitelist:
                 self.app_list.selected_list.addItem(app)
-                whitelist.remove(app.ident)
+                whitelist.remove(app.whatsThis())
             else:
                 self.app_list.available_list.addItem(app)
 
@@ -113,16 +168,21 @@ def fill_apps_list(self, template=None):
         self.app_list.selected_list.sortItems()
 
     def save_appmenu_select_changes(self):
-        new_whitelisted = [self.app_list.selected_list.item(i).ident
-                           for i in range(self.app_list.selected_list.count())]
+        new_whitelisted = [
+            self.app_list.selected_list.item(i).whatsThis()
+            for i in range(self.app_list.selected_list.count())
+        ]
 
         if set(new_whitelisted) == set(self.whitelisted):
             return False
 
         try:
-            self.vm.features['menu-items'] = " ".join(new_whitelisted)
+            self.vm.features["menu-items"] = " ".join(new_whitelisted)
         except exc.QubesException as ex:
-            raise RuntimeError(QtCore.QCoreApplication.translate(
-                "exception", 'Failed to set menu items')) from ex
+            raise RuntimeError(
+                QtCore.QCoreApplication.translate(
+                    "exception", "Failed to set menu items"
+                )
+            ) from ex
 
         return True
diff --git a/qubesmanager/tests/test_utils.py b/qubesmanager/tests/test_utils.py
new file mode 100644
index 000000000..64541889d
--- /dev/null
+++ b/qubesmanager/tests/test_utils.py
@@ -0,0 +1,73 @@
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2024 Marta Marczykowska-Górecka
+#                                       <marmarta@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.
+#
+
+from PyQt6 import QtGui  # pylint: disable=import-error
+from qubesmanager import utils
+import unittest
+
+
+class TestCaseQImage(unittest.TestCase):
+    def setUp(self):
+        self.rgba = (
+            b"\x00\x00\x00\xff"
+            b"\xff\x00\x00\xff"
+            b"\x00\xff\x00\xff"
+            b"\x00\x00\x00\xff"
+        )
+        self.width = 2
+        self.height = 2
+
+    def test_00_empty_image(self):
+        empty_image = QtGui.QImage()
+        tinted_image = utils.tint_qimage(empty_image, "0x0000ff")
+        self.assertIsInstance(
+            tinted_image,
+            QtGui.QImage,
+            "Tint of empty QImage failed",
+        )
+
+    def test_01_tint(self):
+        source = QtGui.QImage(
+            self.rgba,
+            self.width,
+            self.height,
+            QtGui.QImage.Format.Format_RGBA8888,
+        )
+        tinted_image = utils.tint_qimage(source, "0x0000ff")
+        self.assertIsInstance(
+            tinted_image,
+            QtGui.QImage,
+            "Tinting of a 2x2 RGBA QImage did not return a QImage",
+        )
+        internal_data = tinted_image.constBits()
+        internal_data.setsize(self.width * self.height * 4)
+        raw_data = bytes(internal_data)
+        self.assertEqual(
+            raw_data,
+            b"\x00\x00\x3f\xff"
+            b"\x00\x00\xff\xff"
+            b"\x00\x00\xff\xff"
+            b"\x00\x00\x3f\xff",
+            "Tinting of refrence image returned wrong results",
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/qubesmanager/tests/test_vm_settings.py b/qubesmanager/tests/test_vm_settings.py
index 571d7e6c2..d8c55498b 100644
--- a/qubesmanager/tests/test_vm_settings.py
+++ b/qubesmanager/tests/test_vm_settings.py
@@ -48,16 +48,16 @@ def mock_subprocess_complex(command):
     vm_name = command[-1]
     if command[1] == '--get-available':
         if vm_name == 'test-vm-set':
-            return (b'test.desktop|Test App|\n'
-                    b'test2.desktop|Test2 App| test2\n'
-                    b'test3.desktop|Test3 App|\n'
-                    b'myvm.desktop|My VM app|\n')
+            return (b'test.desktop|Test App||\n'
+                    b'test2.desktop|Test2 App| test2|\n'
+                    b'test3.desktop|Test3 App||\n'
+                    b'myvm.desktop|My VM app||\n')
         elif vm_name == 'fedora-36':
-            return b'tpl.desktop|Template App|\n'
+            return b'tpl.desktop|Template App||\n'
         else:
-            return (b'test.desktop|Test App|\n'
-                    b'test2.desktop|Test2 App| test2\n'
-                    b'test3.desktop|Test3 App|\n')
+            return (b'test.desktop|Test App||\n'
+                    b'test2.desktop|Test2 App| test2|\n'
+                    b'test3.desktop|Test3 App||\n')
     elif command[1] == '--get-whitelist':
         if vm_name == 'test-vm-set':
             return b'test.desktop\nmissing.desktop'
diff --git a/qubesmanager/utils.py b/qubesmanager/utils.py
index 51184a1b6..9709ab08c 100644
--- a/qubesmanager/utils.py
+++ b/qubesmanager/utils.py
@@ -32,6 +32,7 @@
 import sys
 from qubesadmin import events
 from qubesadmin import exc
+import qubesimgconverter
 import xdg.BaseDirectory
 import pathlib
 import shutil
@@ -599,3 +600,26 @@ def run_synchronous(window_class):
     qt_app.exit()
 
     return window
+
+
+def tint_qimage(source: QtGui.QImage, color: str) -> QtGui.QImage:
+    """Use qubesimgconverter.Image.tint() to tint a PyQt6.QtGui.QImage"""
+    assert isinstance(source, QtGui.QImage)
+    size_bytes = source.width() * source.height() * 4
+    if not size_bytes:
+        # Could not tint empty image
+        return source
+    source = source.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
+    internal_data = source.constBits()
+    internal_data.setsize(size_bytes)
+    raw_data = bytes(internal_data)
+    tinted_image = qubesimgconverter.Image(
+        raw_data, (source.width(), source.height())
+    ).tint(color)
+    destination = QtGui.QImage(
+        tinted_image.data,
+        tinted_image.width,
+        tinted_image.height,
+        QtGui.QImage.Format.Format_RGBA8888,
+    )
+    return destination
diff --git a/rpm_spec/qmgr.spec.in b/rpm_spec/qmgr.spec.in
index 22ceef0ba..75c420084 100644
--- a/rpm_spec/qmgr.spec.in
+++ b/rpm_spec/qmgr.spec.in
@@ -11,6 +11,7 @@ Requires:	python%{python3_pkgversion}
 Requires:	python%{python3_pkgversion}-pyqt6
 Requires:	python%{python3_pkgversion}-inotify
 Requires:	python%{python3_pkgversion}-qubesadmin >= 4.3.7
+Requires:	python%{python3_pkgversion}-qubesimgconverter
 Requires:	python%{python3_pkgversion}-qasync
 Requires:	python%{python3_pkgversion}-pyxdg
 Requires:	qubes-desktop-linux-common >= 4.1.2
@@ -121,6 +122,7 @@ rm -rf $RPM_BUILD_ROOT
 %{python3_sitelib}/qubesmanager/tests/test_qube_manager.py
 %{python3_sitelib}/qubesmanager/tests/test_vm_settings.py
 %{python3_sitelib}/qubesmanager/tests/test_clone_vm.py
+%{python3_sitelib}/qubesmanager/tests/test_utils.py
 
 %dir %{python3_sitelib}/qubesmanager-*.egg-info
 %{python3_sitelib}/qubesmanager-*.egg-info/*


Reviews:
user: login: marmarek
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-10T14:30:30Z
user: login: alimirjamali
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T14:59:51Z
user: login: alimirjamali
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T15:06:44Z
user: login: alimirjamali
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T15:07:11Z
user: login: alimirjamali
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T15:20:12Z
user: login: marmarek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T21:40:25Z
user: login: marmarek
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T21:41:57Z

Review comments:
diff_hunk: @@ -9,3 +9,5 @@ pylint
 sphinx
 PyYAML
 qasync
+Pillow                                                                                                                              
+numpy                                                                                                                               
path: ci/requirements.txt
user: login: marmarek
body: A lot of spaces here...
created_at: 2025-03-10T14:21:22Z
author_association: MEMBER
diff_hunk: @@ -81,25 +93,68 @@ def fill_apps_list(self, template=None):
 
         self.app_list.clear()
 
-        command = ['qvm-appmenus', '--get-available',
-                   '--i-understand-format-is-unstable', '--file-field',
-                   'Comment']
+        command = [
+            "qvm-appmenus",
+            "--get-available",
+            "--i-understand-format-is-unstable",
+            "--file-field",
+            "Comment",
+            "--file-field",
+            "Icon",
+        ]
         if template:
-            command.extend(['--template', template.name])
+            command.extend(["--template", template.name])
         command.append(self.vm.name)
 
+        if not hasattr(self.vm, "template"):
+            # TemplateVMs and StandaloneVMs
+            main_template = self.vm.name
+        elif not hasattr(self.vm.template, "template"):
+            # AppVMs
+            main_template = self.vm.template.name
+        else:
+            # DispVMs
+            main_template = self.vm.template.template.name
+
+        template_icons_path = path.join(
+            path.expanduser("~"),
+            ".local",
+            "share",
+            "qubes-appmenus",
+            f"{main_template}",
+            "apps.tempicons",
+        )
path: qubesmanager/appmenu_select.py
user: login: marmarek
body: Wouldn't it be better to extend `qvm-appmenus` to print the path too? Maybe add some "field" like `OriginalIconPath`?
Especially, the current implementation doesn't handle applications installed in the AppVM itself (you can put applications into `~/.local/share/applications` and they will be available same as those in `/usr/share/applications` in the template).
created_at: 2025-03-10T14:27:51Z
author_association: MEMBER
diff_hunk: @@ -81,25 +93,68 @@ def fill_apps_list(self, template=None):
 
         self.app_list.clear()
 
-        command = ['qvm-appmenus', '--get-available',
-                   '--i-understand-format-is-unstable', '--file-field',
-                   'Comment']
+        command = [
+            "qvm-appmenus",
+            "--get-available",
+            "--i-understand-format-is-unstable",
+            "--file-field",
+            "Comment",
+            "--file-field",
+            "Icon",
+        ]
         if template:
-            command.extend(['--template', template.name])
+            command.extend(["--template", template.name])
         command.append(self.vm.name)
 
+        if not hasattr(self.vm, "template"):
+            # TemplateVMs and StandaloneVMs
+            main_template = self.vm.name
+        elif not hasattr(self.vm.template, "template"):
+            # AppVMs
+            main_template = self.vm.template.name
+        else:
+            # DispVMs
+            main_template = self.vm.template.template.name
+
+        template_icons_path = path.join(
+            path.expanduser("~"),
+            ".local",
+            "share",
+            "qubes-appmenus",
+            f"{main_template}",
+            "apps.tempicons",
+        )
+
         try:
-            available_appmenus = [
-                AppListWidgetItem.from_line(line)
-                for line in subprocess.check_output(
-                    command).decode().splitlines()]
+            available_appmenus = []
+            for line in subprocess.check_output(command).decode().splitlines():
+                ident, name, comment, icon_path = line.split("|", maxsplit=4)
+                app_item = AppListWidgetItem.from_line(
+                    "|".join([ident, name, comment])
+                )
+                icon_path = icon_path.replace(
+                    "%VMDIR%/apps.icons", template_icons_path
+                )
+                if path.exists(icon_path):
+                    icon = QtGui.QIcon(icon_path)
+                    qpixmap = icon.pixmap(QtCore.QSize(512, 512))
+                    qimage = QtGui.QImage(qpixmap)
+                    qimage = tint_qimage(qimage, self.vm.label.color)
+                    qpixmap = QtGui.QPixmap(qimage)
+                    icon = QtGui.QIcon(qpixmap)
+                else:
+                    # for `qubes-start.desktop` & missing icons
+                    icon = QtGui.QIcon.fromTheme(self.vm.icon)
path: qubesmanager/appmenu_select.py
user: login: marmarek
body: While this fallback case should remain anyway, I think qubes-start.desktop should also have an icon (probably a work for separate PR).
created_at: 2025-03-10T14:28:57Z
author_association: MEMBER
diff_hunk: @@ -81,25 +93,68 @@ def fill_apps_list(self, template=None):
 
         self.app_list.clear()
 
-        command = ['qvm-appmenus', '--get-available',
-                   '--i-understand-format-is-unstable', '--file-field',
-                   'Comment']
+        command = [
+            "qvm-appmenus",
+            "--get-available",
+            "--i-understand-format-is-unstable",
+            "--file-field",
+            "Comment",
+            "--file-field",
+            "Icon",
+        ]
         if template:
-            command.extend(['--template', template.name])
+            command.extend(["--template", template.name])
         command.append(self.vm.name)
 
+        if not hasattr(self.vm, "template"):
+            # TemplateVMs and StandaloneVMs
+            main_template = self.vm.name
+        elif not hasattr(self.vm.template, "template"):
+            # AppVMs
+            main_template = self.vm.template.name
+        else:
+            # DispVMs
+            main_template = self.vm.template.template.name
+
+        template_icons_path = path.join(
+            path.expanduser("~"),
+            ".local",
+            "share",
+            "qubes-appmenus",
+            f"{main_template}",
+            "apps.tempicons",
+        )
+
         try:
-            available_appmenus = [
-                AppListWidgetItem.from_line(line)
-                for line in subprocess.check_output(
-                    command).decode().splitlines()]
+            available_appmenus = []
+            for line in subprocess.check_output(command).decode().splitlines():
+                ident, name, comment, icon_path = line.split("|", maxsplit=4)
+                app_item = AppListWidgetItem.from_line(
+                    "|".join([ident, name, comment])
+                )
+                icon_path = icon_path.replace(
+                    "%VMDIR%/apps.icons", template_icons_path
+                )
+                if path.exists(icon_path):
+                    icon = QtGui.QIcon(icon_path)
+                    qpixmap = icon.pixmap(QtCore.QSize(512, 512))
+                    qimage = QtGui.QImage(qpixmap)
+                    qimage = tint_qimage(qimage, self.vm.label.color)
+                    qpixmap = QtGui.QPixmap(qimage)
+                    icon = QtGui.QIcon(qpixmap)
+                else:
+                    # for `qubes-start.desktop` & missing icons
+                    icon = QtGui.QIcon.fromTheme(self.vm.icon)
path: qubesmanager/appmenu_select.py
user: login: alimirjamali
body: > While this fallback case should remain anyway, I think qubes-start.desktop should also have an icon (probably a work for separate PR).

That would be for `desktop-linux-common` repository. I updated the comment (to say it is only for missing icons).
created_at: 2025-03-10T14:59:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,25 +93,68 @@ def fill_apps_list(self, template=None):
 
         self.app_list.clear()
 
-        command = ['qvm-appmenus', '--get-available',
-                   '--i-understand-format-is-unstable', '--file-field',
-                   'Comment']
+        command = [
+            "qvm-appmenus",
+            "--get-available",
+            "--i-understand-format-is-unstable",
+            "--file-field",
+            "Comment",
+            "--file-field",
+            "Icon",
+        ]
         if template:
-            command.extend(['--template', template.name])
+            command.extend(["--template", template.name])
         command.append(self.vm.name)
 
+        if not hasattr(self.vm, "template"):
+            # TemplateVMs and StandaloneVMs
+            main_template = self.vm.name
+        elif not hasattr(self.vm.template, "template"):
+            # AppVMs
+            main_template = self.vm.template.name
+        else:
+            # DispVMs
+            main_template = self.vm.template.template.name
+
+        template_icons_path = path.join(
+            path.expanduser("~"),
+            ".local",
+            "share",
+            "qubes-appmenus",
+            f"{main_template}",
+            "apps.tempicons",
+        )
path: qubesmanager/appmenu_select.py
user: login: alimirjamali
body: > Wouldn't it be better to extend `qvm-appmenus` to print the path too? Maybe add some "field" like `OriginalIconPath`? Especially, the current implementation doesn't handle applications installed in the AppVM itself (you can put applications into `~/.local/share/applications` and they will be available same as those in `/usr/share/applications` in the template).

Yes. It does not work with icons for flatpak, snap and similar cases (will show the VM's label icon instead). But the fix for `qvm-appmenus` will be for `desktop-linux-common` repository. I suggest we leave this for a supplementary PR. After `qvm-appemnus` is patched to provide `OriginalIconPath`,  I will come back and fix it here as well. This PR is already long and complex and I want to work on the pending Qube Comment PRs as well (after some of the pending PRs are concluded).
created_at: 2025-03-10T15:06:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,3 +9,5 @@ pylint
 sphinx
 PyYAML
 qasync
+Pillow                                                                                                                              
+numpy                                                                                                                               
path: ci/requirements.txt
user: login: alimirjamali
body: Fixed.
created_at: 2025-03-10T15:07:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,25 +93,68 @@ def fill_apps_list(self, template=None):
 
         self.app_list.clear()
 
-        command = ['qvm-appmenus', '--get-available',
-                   '--i-understand-format-is-unstable', '--file-field',
-                   'Comment']
+        command = [
+            "qvm-appmenus",
+            "--get-available",
+            "--i-understand-format-is-unstable",
+            "--file-field",
+            "Comment",
+            "--file-field",
+            "Icon",
+        ]
         if template:
-            command.extend(['--template', template.name])
+            command.extend(["--template", template.name])
         command.append(self.vm.name)
 
+        if not hasattr(self.vm, "template"):
+            # TemplateVMs and StandaloneVMs
+            main_template = self.vm.name
+        elif not hasattr(self.vm.template, "template"):
+            # AppVMs
+            main_template = self.vm.template.name
+        else:
+            # DispVMs
+            main_template = self.vm.template.template.name
+
+        template_icons_path = path.join(
+            path.expanduser("~"),
+            ".local",
+            "share",
+            "qubes-appmenus",
+            f"{main_template}",
+            "apps.tempicons",
+        )
path: qubesmanager/appmenu_select.py
user: login: alimirjamali
body: P.S. One more consideration. The GUIVM icons in `~/.local/share/qubes-appmenus/VMNAME/apps.icons` provided via qvm-appmenus are already tinted (applicable for Qube specific Flatpak, Snap, ...). And we have to consider this as well if we ever want to patch `qvm-appmenus` to provide real original icon.
created_at: 2025-03-10T15:20:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,25 +93,68 @@ def fill_apps_list(self, template=None):
 
         self.app_list.clear()
 
-        command = ['qvm-appmenus', '--get-available',
-                   '--i-understand-format-is-unstable', '--file-field',
-                   'Comment']
+        command = [
+            "qvm-appmenus",
+            "--get-available",
+            "--i-understand-format-is-unstable",
+            "--file-field",
+            "Comment",
+            "--file-field",
+            "Icon",
+        ]
         if template:
-            command.extend(['--template', template.name])
+            command.extend(["--template", template.name])
         command.append(self.vm.name)
 
+        if not hasattr(self.vm, "template"):
+            # TemplateVMs and StandaloneVMs
+            main_template = self.vm.name
+        elif not hasattr(self.vm.template, "template"):
+            # AppVMs
+            main_template = self.vm.template.name
+        else:
+            # DispVMs
+            main_template = self.vm.template.template.name
+
+        template_icons_path = path.join(
+            path.expanduser("~"),
+            ".local",
+            "share",
+            "qubes-appmenus",
+            f"{main_template}",
+            "apps.tempicons",
+        )
path: qubesmanager/appmenu_select.py
user: login: marmarek
body: > The GUIVM icons in `~/.local/share/qubes-appmenus/VMNAME/apps.icons`

Yes, but there is also `apps.tempicons` dir, similar to icons from the template.
created_at: 2025-03-10T21:40:25Z
author_association: MEMBER

PR comments:
user: login: marmarek
created_at: 2025-03-08T00:32:46Z
body: This likely needs --enablerepo flag like here: https://github.com/QubesOS/qubes-core-admin-client/commit/c3759adc91a6a92f3e0927a5ce3459f93900c641
author_association: MEMBER
user: login: alimirjamali
created_at: 2025-03-08T01:10:46Z
body: > This likely needs --enablerepo flag like here: [QubesOS/qubes-core-admin-client@c3759ad](https://github.com/QubesOS/qubes-core-admin-client/commit/c3759adc91a6a92f3e0927a5ce3459f93900c641)

Did not work

p.s. Trying to fetch it from source.
author_association: CONTRIBUTOR
user: login: qubesos-bot
created_at: 2025-03-08T07:13:46Z
body: # OpenQA test summary
Complete test suite and dependencies: https://openqa.qubes-os.org/tests/overview?distri=qubesos&version=4.3&build=2025031004-4.3&flavor=pull-requests

Test run included the following:
- https://github.com/QubesOS/qubes-core-admin/pull/666 (https://github.com/QubesOS/qubes-core-admin/pull/666/commits/f628cb72014fcb46ee6bad01820bc75f594f0753)
- https://github.com/QubesOS/qubes-manager/pull/408 (https://github.com/QubesOS/qubes-manager/pull/408/commits/601e10b17e24ac9af343d4ec689a181b0544f1ca)
- https://github.com/QubesOS/qubes-core-admin-client/pull/332 (https://github.com/QubesOS/qubes-core-admin-client/pull/332/commits/e22c531c6eb2e0b5404b58295e499ca758627009)
- https://github.com/QubesOS/qubes-manager/pull/412 (https://github.com/QubesOS/qubes-manager/pull/412/commits/64d89d35b59427aac7bb0f98dfb7aa074b92e52d)
- https://github.com/QubesOS/qubes-desktop-linux-manager/pull/247 (https://github.com/QubesOS/qubes-desktop-linux-manager/pull/247/commits/777796a0caa5c99df3694dd86db25f4851aeb001)
- https://github.com/QubesOS/qubes-manager/pull/407 (https://github.com/QubesOS/qubes-manager/pull/407/commits/e7dc051240a6827f375f07a8e2b3e0516dc68cbd)
- https://github.com/QubesOS/qubes-desktop-linux-manager/pull/248 (https://github.com/QubesOS/qubes-desktop-linux-manager/pull/248/commits/1af77ee6de9ef4da5f16bbefcb3fcf0dbb92f364)
- https://github.com/QubesOS/qubes-core-admin/pull/661 (https://github.com/QubesOS/qubes-core-admin/pull/661/commits/87f187dafb4de4b88f4d27b9e4779567ca3fa043)
## New failures, excluding unstable
Compared to: https://openqa.qubes-os.org/tests/overview?distri=qubesos&version=4.3&build=2025021804-4.3&flavor=update
* system_tests_whonix
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/21) (unknown)
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/29) (unknown)
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/37) (unknown)
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/43) (unknown)
* system_tests_splitgpg
  * TC_10_Thunderbird_whonix-workstation-17: [test_000_send_receive_default](https://openqa.qubes-os.org/tests/131768#step/TC_10_Thunderbird_whonix-workstation-17/1) (failure)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_10_Thunderbird_whonix-workstation-17: [test_010_send_receive_inline_signed_only](https://openqa.qubes-os.org/tests/131768#step/TC_10_Thunderbird_whonix-workstation-17/2) (failure + cleanup)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_10_Thunderbird_whonix-workstation-17: [test_020_send_receive_inline_with_attachment](https://openqa.qubes-os.org/tests/131768#step/TC_10_Thunderbird_whonix-workstation-17/3) (failure)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

* system_tests_qrexec
  * TC_00_Qrexec_whonix-workstation-17: [test_051_qrexec_simple_eof_reverse](https://openqa.qubes-os.org/tests/131773#step/TC_00_Qrexec_whonix-workstation-17/2) (error + cleanup)
 `raise TimeoutError from exc_val... TimeoutError`

* system_tests_kde_gui_interactive
  * simple_gui_apps: [unnamed test](https://openqa.qubes-os.org/tests/131781#step/simple_gui_apps/23) (unknown)
  * simple_gui_apps: [Failed](https://openqa.qubes-os.org/tests/131781#step/simple_gui_apps/24) (test died)
 `# Test died: no candidate needle with tag(s) 'vm-settings-app-evinc...`

  * simple_gui_apps: [unnamed test](https://openqa.qubes-os.org/tests/131781#step/simple_gui_apps/26) (unknown)
* system_tests_whonix@hw7
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/21) (unknown)
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/29) (unknown)
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/37) (unknown)
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/43) (unknown)
* system_tests_extra
  * TC_10_Thunderbird_whonix-workstation-17: [test_000_send_receive_default](https://openqa.qubes-os.org/tests/131831#step/TC_10_Thunderbird_whonix-workstation-17/1) (failure + cleanup)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_10_Thunderbird_whonix-workstation-17: [test_010_send_receive_inline_signed_only](https://openqa.qubes-os.org/tests/131831#step/TC_10_Thunderbird_whonix-workstation-17/2) (failure + cleanup)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_10_Thunderbird_whonix-workstation-17: [test_020_send_receive_inline_with_attachment](https://openqa.qubes-os.org/tests/131831#step/TC_10_Thunderbird_whonix-workstation-17/3) (failure + cleanup)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_00_QVCTest_whonix-workstation-17: [test_010_screenshare](https://openqa.qubes-os.org/tests/131831#step/TC_00_QVCTest_whonix-workstation-17/1) (failure)
 `~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^... AssertionError: 0 == 0`

## Failed tests
<details><summary>32 failures</summary>

* system_tests_whonix
  * whonixcheck: [fail](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/20) (unknown)
 `Whonixcheck for anon-whonix failed...`

  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/21) (unknown)
  * whonixcheck: [fail](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/28) (unknown)
 `Whonixcheck for whonix-gateway-17 failed...`

  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/29) (unknown)
  * whonixcheck: [fail](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/36) (unknown)
 `Whonixcheck for sys-whonix failed...`

  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/37) (unknown)
  * whonixcheck: [fail](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/42) (unknown)
 `Whonixcheck for whonix-workstation-17 failed...`

  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131760#step/whonixcheck/43) (unknown)
* system_tests_suspend
  * suspend: [unnamed test](https://openqa.qubes-os.org/tests/131762#step/suspend/44) (unknown)
  * suspend: [Failed](https://openqa.qubes-os.org/tests/131762#step/suspend/45) (test died)
 `# Test died: no candidate needle with tag(s) 'SUSPEND-FAILED' match...`

* system_tests_splitgpg
  * TC_10_Thunderbird_whonix-workstation-17: [test_000_send_receive_default](https://openqa.qubes-os.org/tests/131768#step/TC_10_Thunderbird_whonix-workstation-17/1) (failure)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_10_Thunderbird_whonix-workstation-17: [test_010_send_receive_inline_signed_only](https://openqa.qubes-os.org/tests/131768#step/TC_10_Thunderbird_whonix-workstation-17/2) (failure + cleanup)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_10_Thunderbird_whonix-workstation-17: [test_020_send_receive_inline_with_attachment](https://openqa.qubes-os.org/tests/131768#step/TC_10_Thunderbird_whonix-workstation-17/3) (failure)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

* system_tests_qrexec
  * TC_00_Qrexec_whonix-workstation-17: [test_051_qrexec_simple_eof_reverse](https://openqa.qubes-os.org/tests/131773#step/TC_00_Qrexec_whonix-workstation-17/2) (error + cleanup)
 `raise TimeoutError from exc_val... TimeoutError`

* system_tests_kde_gui_interactive
  * simple_gui_apps: [unnamed test](https://openqa.qubes-os.org/tests/131781#step/simple_gui_apps/23) (unknown)
  * simple_gui_apps: [Failed](https://openqa.qubes-os.org/tests/131781#step/simple_gui_apps/24) (test died)
 `# Test died: no candidate needle with tag(s) 'vm-settings-app-evinc...`

  * simple_gui_apps: [unnamed test](https://openqa.qubes-os.org/tests/131781#step/simple_gui_apps/26) (unknown)
* system_tests_whonix@hw7
  * whonixcheck: [fail](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/20) (unknown)
 `Whonixcheck for anon-whonix failed...`

  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/21) (unknown)
  * whonixcheck: [fail](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/28) (unknown)
 `Whonixcheck for whonix-gateway-17 failed...`

  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/29) (unknown)
  * whonixcheck: [fail](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/36) (unknown)
 `Whonixcheck for sys-whonix failed...`

  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/37) (unknown)
  * whonixcheck: [fail](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/42) (unknown)
 `Whonixcheck for whonix-workstation-17 failed...`

  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/131788#step/whonixcheck/43) (unknown)
* system_tests_basic_vm_qrexec_gui_btrfs
  * TC_03_QvmRevertTemplateChanges: [test_000_revert_linux](https://openqa.qubes-os.org/tests/131794#step/TC_03_QvmRevertTemplateChanges/1) (error)
 `subprocess.CalledProcessError: Command '['sha1sum', '/var/lib/qubes...`

* system_tests_suspend@hw1
  * suspend: [unnamed test](https://openqa.qubes-os.org/tests/131798#step/suspend/44) (unknown)
  * suspend: [Failed](https://openqa.qubes-os.org/tests/131798#step/suspend/45) (test died)
 `# Test died: no candidate needle with tag(s) 'SUSPEND-FAILED' match...`

* system_tests_extra
  * TC_10_Thunderbird_whonix-workstation-17: [test_000_send_receive_default](https://openqa.qubes-os.org/tests/131831#step/TC_10_Thunderbird_whonix-workstation-17/1) (failure + cleanup)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_10_Thunderbird_whonix-workstation-17: [test_010_send_receive_inline_signed_only](https://openqa.qubes-os.org/tests/131831#step/TC_10_Thunderbird_whonix-workstation-17/2) (failure + cleanup)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_10_Thunderbird_whonix-workstation-17: [test_020_send_receive_inline_with_attachment](https://openqa.qubes-os.org/tests/131831#step/TC_10_Thunderbird_whonix-workstation-17/3) (failure + cleanup)
 `dogtail.tree.SearchError: descendent of [application | Thunderbird]...`

  * TC_00_QVCTest_whonix-workstation-17: [test_010_screenshare](https://openqa.qubes-os.org/tests/131831#step/TC_00_QVCTest_whonix-workstation-17/1) (failure)
 `~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^... AssertionError: 0 == 0`

</details>

## Fixed failures
Compared to: https://openqa.qubes-os.org/tests/129058#dependencies
<details><summary>8 fixed</summary>

* system_tests_whonix
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/129060#step/whonixcheck/33) (unknown)
* system_tests_qrexec
  * TC_00_Qrexec_whonix-workstation-17: [test_070_qrexec_vm_simultaneous_write](https://openqa.qubes-os.org/tests/129090#step/TC_00_Qrexec_whonix-workstation-17/8) (failure)
 `exited with non-zero code 125; stderr: b"dd: error writing 'standa... `

* system_tests_kde_gui_interactive
  * clipboard_and_web: [unnamed test](https://openqa.qubes-os.org/tests/129064#step/clipboard_and_web/18) (unknown)
  * clipboard_and_web: [Failed](https://openqa.qubes-os.org/tests/129064#step/clipboard_and_web/19) (test died)
 `# Test died: no candidate needle with tag(s) 'clipboard-paste-notif...`

* system_tests_audio
  * TC_20_AudioVM_Pulse_whonix-workstation-17: [test_223_audio_play_hvm](https://openqa.qubes-os.org/tests/129063#step/TC_20_AudioVM_Pulse_whonix-workstation-17/4) (failure)
 `AssertionError: too short audio, expected 10s, got 7.33528344671201...`

  * TC_20_AudioVM_PipeWire_whonix-workstation-17: [test_226_audio_playback_pipewire](https://openqa.qubes-os.org/tests/129063#step/TC_20_AudioVM_PipeWire_whonix-workstation-17/1) (failure)
 `AssertionError: too short audio, expected 10s, got 9.19027210884353...`

  * TC_20_AudioVM_PipeWire_whonix-workstation-17: [test_251_audio_playback_audiovm_pipewire_late_start](https://openqa.qubes-os.org/tests/129063#step/TC_20_AudioVM_PipeWire_whonix-workstation-17/5) (failure)
 `AssertionError: too short audio, expected 10s, got 9.22900226757369...`

* system_tests_whonix@hw7
  * whonixcheck: [unnamed test](https://openqa.qubes-os.org/tests/129068#step/whonixcheck/33) (unknown)
</details>

## Unstable tests
<details>


</details>

## Performance Tests

### Performance degradation:

<details><summary>28 performance degradations</summary>

* dom0_root_seq1m_q8t1_read 3:read_bandwidth_kb: 244937.00 :small_red_triangle_up: ( previous job: 486352.00, degradation: 50.36%)
* dom0_root_seq1m_q8t1_write 3:write_bandwidth_kb: 208754.00 :small_red_triangle_up: ( previous job: 276742.00, degradation: 75.43%)
* dom0_root_seq1m_q1t1_read 3:read_bandwidth_kb: 59601.00 :small_red_triangle_up: ( previous job: 423495.00, degradation: 14.07%)
* dom0_root_seq1m_q1t1_write 3:write_bandwidth_kb: 88041.00 :small_red_triangle_up: ( previous job: 185030.00, degradation: 47.58%)
* fedora-41-xfce_root_rnd4k_q32t1_write 3:write_bandwidth_kb: 3051.00 :small_red_triangle_up: ( previous job: 3785.00, degradation: 80.61%)
* fedora-41-xfce_private_rnd4k_q32t1_read 3:read_bandwidth_kb: 79056.00 :small_red_triangle_up: ( previous job: 87999.00, degradation: 89.84%)
* fedora-41-xfce_private_rnd4k_q32t1_write 3:write_bandwidth_kb: 3156.00 :small_red_triangle_up: ( previous job: 3885.00, degradation: 81.24%)
* fedora-41-xfce_private_rnd4k_q1t1_write 3:write_bandwidth_kb: 1022.00 :small_red_triangle_up: ( previous job: 1613.00, degradation: 63.36%)
* fedora-41-xfce_volatile_seq1m_q8t1_read 3:read_bandwidth_kb: 341222.00 :small_red_triangle_up: ( previous job: 392725.00, degradation: 86.89%)
* fedora-41-xfce_volatile_rnd4k_q32t1_write 3:write_bandwidth_kb: 2308.00 :small_red_triangle_up: ( previous job: 3959.00, degradation: 58.30%)
* fedora-41-xfce_volatile_rnd4k_q1t1_write 3:write_bandwidth_kb: 1844.00 :small_red_triangle_up: ( previous job: 2693.00, degradation: 68.47%)
* debian-12-xfce_exec: 8.37 :small_red_triangle_up: ( previous job: 7.15, degradation: 117.01%)
* debian-12-xfce_exec-data-simplex: 72.77 :small_red_triangle_up: ( previous job: 48.93, degradation: 148.73%)
* debian-12-xfce_exec-data-duplex: 67.40 :small_red_triangle_up: ( previous job: 50.76, degradation: 132.77%)
* debian-12-xfce_exec-data-duplex-root: 85.22 :small_red_triangle_up: ( previous job: 64.91, degradation: 131.30%)
* debian-12-xfce_socket-data-duplex: 162.33 :small_red_triangle_up: ( previous job: 81.49, degradation: 199.21%)
* fedora-41-xfce_exec-data-simplex: 66.12 :small_red_triangle_up: ( previous job: 49.65, degradation: 133.18%)
* fedora-41-xfce_exec-data-duplex: 66.77 :small_red_triangle_up: ( previous job: 49.08, degradation: 136.04%)
* fedora-41-xfce_exec-data-duplex-root: 92.61 :small_red_triangle_up: ( previous job: 81.65, degradation: 113.42%)
* fedora-41-xfce_socket-data-duplex: 153.65 :small_red_triangle_up: ( previous job: 78.62, degradation: 195.42%)
* whonix-gateway-17_exec-data-simplex: 59.47 :small_red_triangle_up: ( previous job: 48.76, degradation: 121.95%)
* whonix-gateway-17_exec-data-duplex: 78.68 :small_red_triangle_up: ( previous job: 48.55, degradation: 162.04%)
* whonix-gateway-17_exec-data-duplex-root: 93.82 :small_red_triangle_up: ( previous job: 70.13, degradation: 133.79%)
* whonix-gateway-17_socket-data-duplex: 163.34 :small_red_triangle_up: ( previous job: 82.74, degradation: 197.41%)
* whonix-workstation-17_exec-data-simplex: 64.19 :small_red_triangle_up: ( previous job: 47.01, degradation: 136.52%)
* whonix-workstation-17_exec-data-duplex: 72.97 :small_red_triangle_up: ( previous job: 49.48, degradation: 147.48%)
* whonix-workstation-17_exec-data-duplex-root: 99.46 :small_red_triangle_up: ( previous job: 79.93, degradation: 124.43%)
* whonix-workstation-17_socket-data-duplex: 162.49 :small_red_triangle_up: ( previous job: 81.71, degradation: 198.86%)
</details>

### Remaining performance tests:

<details><summary>44 tests</summary>

* dom0_root_rnd4k_q32t1_read 3:read_bandwidth_kb: 100592.00 :small_red_triangle_up: ( previous job: 100699.00, degradation: 99.89%)
* dom0_root_rnd4k_q32t1_write 3:write_bandwidth_kb: 7527.00 :green_circle: ( previous job: 3277.00, improvement: 229.69%)
* dom0_root_rnd4k_q1t1_read 3:read_bandwidth_kb: 12093.00 :green_circle: ( previous job: 10163.00, improvement: 118.99%)
* dom0_root_rnd4k_q1t1_write 3:write_bandwidth_kb: 4856.00 :green_circle: ( previous job: 282.00, improvement: 1721.99%)
* dom0_varlibqubes_seq1m_q8t1_read 3:read_bandwidth_kb: 495780.00 :green_circle: ( previous job: 475329.00, improvement: 104.30%)
* dom0_varlibqubes_seq1m_q8t1_write 3:write_bandwidth_kb: 273066.00 :green_circle: ( previous job: 95209.00, improvement: 286.81%)
* dom0_varlibqubes_seq1m_q1t1_read 3:read_bandwidth_kb: 438001.00 :green_circle: ( previous job: 433474.00, improvement: 101.04%)
* dom0_varlibqubes_seq1m_q1t1_write 3:write_bandwidth_kb: 184287.00 :green_circle: ( previous job: 164133.00, improvement: 112.28%)
* dom0_varlibqubes_rnd4k_q32t1_read 3:read_bandwidth_kb: 106824.00 :green_circle: ( previous job: 99808.00, improvement: 107.03%)
* dom0_varlibqubes_rnd4k_q32t1_write 3:write_bandwidth_kb: 9098.00 :green_circle: ( previous job: 8767.00, improvement: 103.78%)
* dom0_varlibqubes_rnd4k_q1t1_read 3:read_bandwidth_kb: 7600.00 :green_circle: ( previous job: 7053.00, improvement: 107.76%)
* dom0_varlibqubes_rnd4k_q1t1_write 3:write_bandwidth_kb: 3803.00 :small_red_triangle_up: ( previous job: 3868.00, degradation: 98.32%)
* fedora-41-xfce_root_seq1m_q8t1_read 3:read_bandwidth_kb: 387930.00 :small_red_triangle_up: ( previous job: 396586.00, degradation: 97.82%)
* fedora-41-xfce_root_seq1m_q8t1_write 3:write_bandwidth_kb: 282940.00 :green_circle: ( previous job: 99783.00, improvement: 283.56%)
* fedora-41-xfce_root_seq1m_q1t1_read 3:read_bandwidth_kb: 320469.00 :small_red_triangle_up: ( previous job: 343795.00, degradation: 93.22%)
* fedora-41-xfce_root_seq1m_q1t1_write 3:write_bandwidth_kb: 54193.00 :green_circle: ( previous job: 44770.00, improvement: 121.05%)
* fedora-41-xfce_root_rnd4k_q32t1_read 3:read_bandwidth_kb: 92146.00 :green_circle: ( previous job: 86742.00, improvement: 106.23%)
* fedora-41-xfce_root_rnd4k_q1t1_read 3:read_bandwidth_kb: 8728.00 :green_circle: ( previous job: 8623.00, improvement: 101.22%)
* fedora-41-xfce_root_rnd4k_q1t1_write 3:write_bandwidth_kb: 1545.00 :green_circle: ( previous job: 1126.00, improvement: 137.21%)
* fedora-41-xfce_private_seq1m_q8t1_read 3:read_bandwidth_kb: 384093.00 :small_red_triangle_up: ( previous job: 401907.00, degradation: 95.57%)
* fedora-41-xfce_private_seq1m_q8t1_write 3:write_bandwidth_kb: 147360.00 :green_circle: ( previous job: 116848.00, improvement: 126.11%)
* fedora-41-xfce_private_seq1m_q1t1_read 3:read_bandwidth_kb: 330781.00 :small_red_triangle_up: ( previous job: 357875.00, degradation: 92.43%)
* fedora-41-xfce_private_seq1m_q1t1_write 3:write_bandwidth_kb: 50945.00 :green_circle: ( previous job: 41375.00, improvement: 123.13%)
* fedora-41-xfce_private_rnd4k_q1t1_read 3:read_bandwidth_kb: 7980.00 :small_red_triangle_up: ( previous job: 8744.00, degradation: 91.26%)
* fedora-41-xfce_volatile_seq1m_q8t1_write 3:write_bandwidth_kb: 189167.00 :green_circle: ( previous job: 139933.00, improvement: 135.18%)
* fedora-41-xfce_volatile_seq1m_q1t1_read 3:read_bandwidth_kb: 307410.00 :green_circle: ( previous job: 294875.00, improvement: 104.25%)
* fedora-41-xfce_volatile_seq1m_q1t1_write 3:write_bandwidth_kb: 126235.00 :green_circle: ( previous job: 78093.00, improvement: 161.65%)
* fedora-41-xfce_volatile_rnd4k_q32t1_read 3:read_bandwidth_kb: 81383.00 :green_circle: ( previous job: 71108.00, improvement: 114.45%)
* fedora-41-xfce_volatile_rnd4k_q1t1_read 3:read_bandwidth_kb: 8338.00 :small_red_triangle_up: ( previous job: 8408.00, degradation: 99.17%)
* debian-12-xfce_exec-root: 29.21 :small_red_triangle_up: ( previous job: 27.97, degradation: 104.45%)
* debian-12-xfce_socket: 9.11 :small_red_triangle_up: ( previous job: 8.33, degradation: 109.38%)
* debian-12-xfce_socket-root: 9.02 :small_red_triangle_up: ( previous job: 8.20, degradation: 109.95%)
* fedora-41-xfce_exec: 9.39 :small_red_triangle_up: ( previous job: 9.13, degradation: 102.76%)
* fedora-41-xfce_exec-root: 61.54 :small_red_triangle_up: ( previous job: 61.17, degradation: 100.61%)
* fedora-41-xfce_socket: 8.10 :green_circle: ( previous job: 8.66, improvement: 93.55%)
* fedora-41-xfce_socket-root: 8.94 :small_red_triangle_up: ( previous job: 8.61, degradation: 103.89%)
* whonix-gateway-17_exec: 6.56 :green_circle: ( previous job: 7.87, improvement: 83.44%)
* whonix-gateway-17_exec-root: 40.25 :small_red_triangle_up: ( previous job: 38.36, degradation: 104.92%)
* whonix-gateway-17_socket: 7.39 :green_circle: ( previous job: 7.54, improvement: 97.92%)
* whonix-gateway-17_socket-root: 8.70 :small_red_triangle_up: ( previous job: 8.27, degradation: 105.15%)
* whonix-workstation-17_exec: 8.66 :small_red_triangle_up: ( previous job: 8.23, degradation: 105.24%)
* whonix-workstation-17_exec-root: 55.52 :small_red_triangle_up: ( previous job: 52.56, degradation: 105.62%)
* whonix-workstation-17_socket: 8.74 :small_red_triangle_up: ( previous job: 8.21, degradation: 106.50%)
* whonix-workstation-17_socket-root: 8.57 :small_red_triangle_up: ( previous job: 8.20, degradation: 104.56%)
</details>


author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-08T07:54:08Z
body: ## [Codecov](https://app.codecov.io/gh/QubesOS/qubes-manager/pull/412?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QubesOS) Report
Attention: Patch coverage is `84.00000%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 68.50%. Comparing base [(`6bdf5bd`)](https://app.codecov.io/gh/QubesOS/qubes-manager/commit/6bdf5bd8eeec1bf77b4245f7e0eba30e3fa36c56?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QubesOS) to head [(`52f6879`)](https://app.codecov.io/gh/QubesOS/qubes-manager/commit/52f68791b155d6680b524b82ce8dd167cb63086e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QubesOS).
> Report is 7 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/QubesOS/qubes-manager/pull/412?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QubesOS) | Patch % | Lines |
|---|---|---|
| [qubesmanager/appmenu\_select.py](https://app.codecov.io/gh/QubesOS/qubes-manager/pull/412?src=pr&el=tree&filepath=qubesmanager%2Fappmenu_select.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QubesOS#diff-cXViZXNtYW5hZ2VyL2FwcG1lbnVfc2VsZWN0LnB5) | 78.37% | [8 Missing :warning: ](https://app.codecov.io/gh/QubesOS/qubes-manager/pull/412?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QubesOS) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #412      +/-   ##
==========================================
+ Coverage   68.33%   68.50%   +0.16%     
==========================================
  Files          17       17              
  Lines        3708     3762      +54     
==========================================
+ Hits         2534     2577      +43     
- Misses       1174     1185      +11     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/QubesOS/qubes-manager/pull/412?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QubesOS).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=QubesOS).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: alimirjamali
created_at: 2025-03-08T08:04:15Z
body: OK. Unittests & Pylint are finally green (after hamnering my way to installing qubesimgconverter). The OpenQA installedTest fails most probably because of the icons.
author_association: CONTRIBUTOR
user: login: marmarek
created_at: 2025-03-08T10:17:58Z
body: > system_tests_gui_interactive@hw1
> 
>     * simple_gui_apps: [unnamed test](https://openqa.qubes-os.org/tests/131583#step/simple_gui_apps/19) (unknown)
> 
>     * simple_gui_apps: [Failed](https://openqa.qubes-os.org/tests/131583#step/simple_gui_apps/20) (test died)
>       `# Test died: no candidate needle with tag(s) 'vm-settings-app-xterm...`

Yes, all of those are due to different appearance of the applications tab. If Marta says she likes it, I'll adjust base screenshots ("needles"), unfortunately there is no easy way to do that for external contributors.
author_association: MEMBER