diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 51328de..2efdc32 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,15 +1,17 @@ include: - project: 'QubesOS/qubes-continuous-integration' - file: '/r4.2/gitlab-base.yml' + file: '/common.yml' - project: 'QubesOS/qubes-continuous-integration' - file: '/r4.2/gitlab-host.yml' + file: '/r4.3/gitlab-base.yml' - project: 'QubesOS/qubes-continuous-integration' - file: '/r4.2/gitlab-vm.yml' + file: '/r4.3/gitlab-host.yml' + - project: 'QubesOS/qubes-continuous-integration' + file: '/r4.3/gitlab-vm.yml' checks:pylint: stage: checks before_script: - - sudo dnf install -y python3-gobject gtk3 xorg-x11-server-Xvfb + - sudo dnf install -y python3-gobject gtk3 xorg-x11-server-Xvfb python3-pip python3-mypy python3-pyxdg gtk-layer-shell - pip3 install --quiet -r ci/requirements.txt - git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client @@ -24,7 +26,7 @@ checks:tests: before_script: &before-script - "PATH=$PATH:$HOME/.local/bin" - sudo dnf install -y python3-gobject gtk3 python3-pytest python3-pytest-asyncio - python3-coverage xorg-x11-server-Xvfb python3-inotify sequoia-sqv + python3-coverage xorg-x11-server-Xvfb python3-inotify sequoia-sqv python3-pip python3-pyxdg gtk-layer-shell - pip3 install --quiet -r ci/requirements.txt - git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client @@ -35,3 +37,10 @@ checks:tests: - "PATH=$PATH:$HOME/.local/bin" - ci/codecov-wrapper +checks:black: + extends: .lint + stage: checks + variables: + DIR: . + SKIP_PYLINT: 1 + BLACK_ARGS: -l88 -v --diff --color --check diff --git a/.pylintrc b/.pylintrc index 3fccc23..f229cb4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -80,7 +80,7 @@ notes=FIXME,FIX,XXX,TODO [FORMAT] # Maximum number of characters on a single line. -max-line-length=80 +max-line-length=88 # Maximum number of lines in a module max-module-lines=3000 diff --git a/qubes_menu/app_widgets.py b/qubes_menu/app_widgets.py index 540a7e0..bf435d7 100644 --- a/qubes_menu/app_widgets.py +++ b/qubes_menu/app_widgets.py @@ -26,21 +26,26 @@ from typing import Optional, List from functools import reduce -from .custom_widgets import (LimitedWidthLabel, SelfAwareMenu, HoverEventBox, - FavoritesMenu) +from .custom_widgets import ( + LimitedWidthLabel, + SelfAwareMenu, + HoverEventBox, + FavoritesMenu, +) from .desktop_file_manager import ApplicationInfo from .vm_manager import VMManager, VMEntry from .utils import load_icon, text_search, highlight_words, remove_from_feature from . import constants import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk -logger = logging.getLogger('qubes-appmenu') +logger = logging.getLogger("qubes-appmenu") -DISP_TEXT = 'new Disposable Qube from ' +DISP_TEXT = "new Disposable Qube from " class AppEntry(Gtk.ListBoxRow): @@ -53,6 +58,7 @@ class AppEntry(Gtk.ListBoxRow): - supports running an application on click; after click signals to the complete menu it might need hiding """ + def __init__(self, app_info: ApplicationInfo, **properties): """ :param app_info: ApplicationInfo obj with data about related app file @@ -61,23 +67,21 @@ def __init__(self, app_info: ApplicationInfo, **properties): super().__init__(**properties) self.app_info = app_info self.app_info.entries.append(self) - self.vm_name = app_info.vm.name if app_info.vm else 'dom0' + self.vm_name = app_info.vm.name if app_info.vm else "dom0" self.menu = SelfAwareMenu() self.event_box = HoverEventBox(focus_widget=self) self.add(self.event_box) self.event_box.add_events(Gdk.EventMask.BUTTON_PRESS_MASK) - self.event_box.connect('button-press-event', self.show_menu) + self.event_box.connect("button-press-event", self.show_menu) - self.drag_source_set( - Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY) + self.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY) self.drag_source_add_uri_targets() self.connect("drag-data-get", self._on_drag_data_get) def _on_drag_data_get(self, _widget, _drag_context, data, _info, _time): - data.set_uris(['file://' + - urllib.parse.quote(str(self.app_info.file_path))]) + data.set_uris(["file://" + urllib.parse.quote(str(self.app_info.file_path))]) def show_menu(self, _widget, event): """ @@ -107,6 +111,7 @@ class BaseAppEntry(AppEntry): """ A 'normal' Application row, used by main applications menu and system tools. """ + def __init__(self, app_info: ApplicationInfo, **properties): """ :param app_info: ApplicationInfo obj with data about related app file @@ -115,7 +120,7 @@ def __init__(self, app_info: ApplicationInfo, **properties): super().__init__(app_info, **properties) self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.event_box.add(self.box) - self.get_style_context().add_class('app_entry') + self.get_style_context().add_class("app_entry") self.menu = FavoritesMenu(lambda: self.app_info) self.icon = Gtk.Image() @@ -137,13 +142,15 @@ def show_menu(self, widget, event): def update_contents(self): """Update icon and app name.""" self.icon.set_from_pixbuf( - load_icon(self.app_info.app_icon, Gtk.IconSize.LARGE_TOOLBAR)) + load_icon(self.app_info.app_icon, Gtk.IconSize.LARGE_TOOLBAR) + ) self.label.set_label(self.app_info.app_name) self.show_all() class VMIcon(Gtk.Image): """Helper class for displaying and auto-updating""" + def __init__(self, vm_entry: Optional[VMEntry]): super().__init__() self.vm_entry = vm_entry @@ -151,11 +158,13 @@ def __init__(self, vm_entry: Optional[VMEntry]): self.vm_entry.entries.append(self) self.update_contents(update_label=True) - def update_contents(self, - update_power_state=False, - update_label=False, - update_has_network=False, - update_type=False): + def update_contents( + self, + update_power_state=False, + update_label=False, + update_has_network=False, + update_type=False, + ): # pylint: disable=unused-argument """ Update own contents (or related widgets, if applicable) based on state @@ -168,8 +177,7 @@ def update_contents(self, :return: """ if update_label and self.vm_entry: - vm_icon = load_icon(self.vm_entry.vm_icon_name, - Gtk.IconSize.LARGE_TOOLBAR) + vm_icon = load_icon(self.vm_entry.vm_icon_name, Gtk.IconSize.LARGE_TOOLBAR) self.set_from_pixbuf(vm_icon) self.show_all() @@ -177,10 +185,10 @@ def update_contents(self, class AppEntryWithVM(AppEntry): """Application Gtk.ListBoxRow with VM description underneath; to be used in Search and Favorites.""" - def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, - **properties): + + def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, **properties): super().__init__(app_info, **properties) - self.get_style_context().add_class('favorite_entry') + self.get_style_context().add_class("favorite_entry") self.grid = Gtk.Grid() self.event_box.add(self.grid) @@ -192,8 +200,8 @@ def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) box.pack_start(self.vm_icon, False, False, 5) box.pack_start(self.vm_label, False, False, 5) - self.vm_label.get_style_context().add_class('favorite_vm_name') - self.app_label.get_style_context().add_class('favorite_app_name') + self.vm_label.get_style_context().add_class("favorite_vm_name") + self.app_label.get_style_context().add_class("favorite_app_name") self.app_label.set_halign(Gtk.Align.START) self.grid.attach(self.app_icon, 0, 0, 1, 2) @@ -210,8 +218,7 @@ def update_contents(self): self.app_icon.set_from_pixbuf(app_icon) if self.app_info.disposable: - self.vm_label.set_text( - DISP_TEXT + str(self.app_info.vm)) + self.vm_label.set_text(DISP_TEXT + str(self.app_info.vm)) elif self.app_info.vm: self.vm_label.set_text(str(self.app_info.vm)) else: @@ -228,11 +235,11 @@ class FavoritesAppEntry(AppEntryWithVM): constants.py, as a space-separated list containing a subset of menu-items feature. """ - def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, - **properties): + + def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, **properties): super().__init__(app_info, vm_manager, **properties) - self.remove_item = Gtk.MenuItem(label='Remove from favorites') - self.remove_item.connect('activate', self._remove_from_favorites) + self.remove_item = Gtk.MenuItem(label="Remove from favorites") + self.remove_item.connect("activate", self._remove_from_favorites) self.menu.add(self.remove_item) self.menu.show_all() @@ -241,16 +248,17 @@ def _remove_from_favorites(self, *_args, **_kwargs): feature""" if not self.app_info.entry_name: return # there is nothing to remove - vm = self.app_info.vm or self.app_info.qapp.domains[ - self.app_info.qapp.local_name] - remove_from_feature(vm, constants.FAVORITES_FEATURE, - self.app_info.entry_name) + vm = ( + self.app_info.vm + or self.app_info.qapp.domains[self.app_info.qapp.local_name] + ) + remove_from_feature(vm, constants.FAVORITES_FEATURE, self.app_info.entry_name) class SearchAppEntry(AppEntryWithVM): """Entry for apps listed on the Search tab.""" - def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, - **properties): + + def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, **properties): super().__init__(app_info, vm_manager, **properties) self.menu = FavoritesMenu(lambda: self.app_info) @@ -269,17 +277,21 @@ def __init__(self, app_info: ApplicationInfo, vm_manager: VMManager, if self.app_info.vm: self.search_words.extend( - self.app_info.vm.name.lower().replace('_', '-').split('-')) + self.app_info.vm.name.lower().replace("_", "-").split("-") + ) else: - self.search_words.append('dom0') + self.search_words.append("dom0") if self.app_info.disposable: self.search_words.extend(DISP_TEXT.lower().split()) if self.app_info.app_name: self.search_words.extend( - self.app_info.app_name.lower().replace( - '_', ' ').replace('-', ' ').split()) + self.app_info.app_name.lower() + .replace("_", " ") + .replace("-", " ") + .split() + ) if self.app_info.keywords: self.search_words.extend(k.lower() for k in self.app_info.keywords) @@ -293,9 +305,10 @@ def find_text(self, search_words: List[str]): return self.last_search_result if search_words: - result = reduce(lambda x, y: x*y, - [text_search(word, self.search_words) - for word in search_words]) + result = reduce( + lambda x, y: x * y, + [text_search(word, self.search_words) for word in search_words], + ) else: result = 0 diff --git a/qubes_menu/application_page.py b/qubes_menu/application_page.py index 6c9ba5b..c65b5c2 100644 --- a/qubes_menu/application_page.py +++ b/qubes_menu/application_page.py @@ -23,15 +23,20 @@ from typing import Optional from .desktop_file_manager import DesktopFileManager -from .custom_widgets import NetworkIndicator, \ - VMRow, ControlList, KeynavController +from .custom_widgets import ( + NetworkIndicator, + VMRow, + ControlList, + KeynavController, +) from .app_widgets import AppEntry, BaseAppEntry from .vm_manager import VMEntry, VMManager from .page_handler import MenuPage from .utils import get_visible_child import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk @@ -40,20 +45,22 @@ class VMTypeToggle: A class controlling a set of radio buttons for toggling which VMs are shown. """ + def __init__(self, builder: Gtk.Builder): """ :param builder: Gtk.Builder, containing loaded glade data """ - self.apps_toggle: Gtk.RadioButton = builder.get_object('apps_toggle') - self.templates_toggle: Gtk.RadioButton = \ - builder.get_object('templates_toggle') - self.system_toggle: Gtk.RadioButton = \ - builder.get_object('system_toggle') - self.vm_list: Gtk.ListBox = builder.get_object('vm_list') - self.app_list: Gtk.ListBox = builder.get_object('app_list') - - self.buttons = [self.apps_toggle, self.templates_toggle, - self.system_toggle] + self.apps_toggle: Gtk.RadioButton = builder.get_object("apps_toggle") + self.templates_toggle: Gtk.RadioButton = builder.get_object("templates_toggle") + self.system_toggle: Gtk.RadioButton = builder.get_object("system_toggle") + self.vm_list: Gtk.ListBox = builder.get_object("vm_list") + self.app_list: Gtk.ListBox = builder.get_object("app_list") + + self.buttons = [ + self.apps_toggle, + self.templates_toggle, + self.system_toggle, + ] for button in self.buttons: button.set_relief(Gtk.ReliefStyle.NONE) @@ -61,7 +68,7 @@ def __init__(self, builder: Gtk.Builder): button.set_can_focus(True) # the below is necessary to make sure keyboard navigation # behaves correctly - button.connect('focus', self._activate_button) + button.connect("focus", self._activate_button) def initialize_state(self): """ @@ -76,7 +83,7 @@ def initialize_state(self): for button in self.buttons: if button.get_size_request() == (-1, -1): - button.set_size_request(button.get_allocated_width()*1.2, -1) + button.set_size_request(button.get_allocated_width() * 1.2, -1) def grab_focus(self): """Simulates other grab_focus type functions: grabs keyboard focus @@ -95,7 +102,7 @@ def _activate_button(widget, _event): def connect_to_toggle(self, func): """Connect a function to toggling of all buttons""" for button in self.buttons: - button.connect('toggled', func) + button.connect("toggled", func) def filter_function(self, row): """Filter function calculated based on currently selected VM toggle @@ -124,7 +131,7 @@ def _filter_templatevms(vm_entry: VMEntry): Filter function for template VMEntries. Returns VMs that are a templateVM or a template for DispVMs. """ - if vm_entry.vm_klass == 'TemplateVM': + if vm_entry.vm_klass == "TemplateVM": return True return vm_entry.is_dispvm_template @@ -141,8 +148,13 @@ class AppPage(MenuPage): """ Helper class for managing the entirety of Applications menu page. """ - def __init__(self, vm_manager: VMManager, builder: Gtk.Builder, - desktop_file_manager: DesktopFileManager): + + def __init__( + self, + vm_manager: VMManager, + builder: Gtk.Builder, + desktop_file_manager: DesktopFileManager, + ): """ :param vm_manager: VM Manager object :param builder: Gtk.Builder with loaded glade object @@ -154,10 +166,10 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder, self.page_widget: Gtk.Box = builder.get_object("app_page") - self.vm_list: Gtk.ListBox = builder.get_object('vm_list') - self.app_list: Gtk.ListBox = builder.get_object('app_list') - self.vm_right_pane: Gtk.Box = builder.get_object('vm_right_pane') - self.separator_bottom = builder.get_object('separator_bottom') + self.vm_list: Gtk.ListBox = builder.get_object("vm_list") + self.app_list: Gtk.ListBox = builder.get_object("app_list") + self.vm_right_pane: Gtk.Box = builder.get_object("vm_right_pane") + self.separator_bottom = builder.get_object("separator_bottom") self.network_indicator = NetworkIndicator() self.vm_right_pane.pack_start(self.network_indicator, False, False, 0) @@ -168,20 +180,20 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder, self.toggle_buttons.connect_to_toggle(self._button_toggled) self.app_list.set_filter_func(self._is_app_fitting) - self.app_list.connect('row-activated', self._app_clicked) + self.app_list.connect("row-activated", self._app_clicked) self.app_list.set_sort_func( - lambda x, y: - x.app_info.sort_name > y.app_info.sort_name) + lambda x, y: x.app_info.sort_name > y.app_info.sort_name + ) self.app_list.invalidate_sort() vm_manager.register_new_vm_callback(self._vm_callback) self.vm_list.set_sort_func(self._sort_vms) self.vm_list.set_filter_func(self.toggle_buttons.filter_function) - self.vm_list.connect('row-selected', self._selection_changed) + self.vm_list.connect("row-selected", self._selection_changed) self.control_list = ControlList(self) - self.control_list.connect('row-activated', self._app_clicked) + self.control_list.connect("row-activated", self._app_clicked) self.vm_right_pane.pack_end(self.control_list, False, False, 0) self.setup_keynav() @@ -190,15 +202,15 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder, self.control_list.set_selection_mode(Gtk.SelectionMode.NONE) self.keynav_manager = KeynavController( - widgets_in_order=[self.app_list, self.control_list]) + widgets_in_order=[self.app_list, self.control_list] + ) - self.widget_order = [self.app_list, - self.control_list] + self.widget_order = [self.app_list, self.control_list] self.vm_list.select_row(None) self._selection_changed(None, None) - self.vm_list.connect('map', self._on_map_vm_list) + self.vm_list.connect("map", self._on_map_vm_list) def _on_map_vm_list(self, *_args): # workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/4977 @@ -216,11 +228,16 @@ def _sort_vms(self, vmentry: VMRow, other_entry: VMRow): my_sort_name = vmentry.sort_order other_sort_name = other_entry.sort_order if self.sort_running: - my_sort_name = "0" if (vmentry.vm_entry.power_state == "Running") \ + my_sort_name = ( + "0" + if (vmentry.vm_entry.power_state == "Running") else "1" + my_sort_name - other_sort_name = "0" if \ - (other_entry.vm_entry.power_state == "Running") \ + ) + other_sort_name = ( + "0" + if (other_entry.vm_entry.power_state == "Running") else "1" + other_sort_name + ) return my_sort_name > other_sort_name def set_sorting_order(self, sort_running: bool = False): @@ -234,12 +251,12 @@ def set_sorting_order(self, sort_running: bool = False): def setup_keynav(self): """Do all the required faffing about to convince Gtk to have reasonable keyboard nav""" - self.vm_list.connect('keynav-failed', self._vm_keynav_failed) + self.vm_list.connect("keynav-failed", self._vm_keynav_failed) - self.app_list.connect('key-press-event', self._focus_vm_list) - self.control_list.connect('key-press-event', self._focus_vm_list) + self.app_list.connect("key-press-event", self._focus_vm_list) + self.control_list.connect("key-press-event", self._focus_vm_list) - self.vm_list.connect('key-press-event', self._vm_key_pressed) + self.vm_list.connect("key-press-event", self._vm_key_pressed) def _vm_key_pressed(self, _widget, event): if event.keyval == Gdk.KEY_Right: @@ -262,8 +279,7 @@ def _vm_callback(self, vm_entry: VMEntry): Callback to be performed on all newly loaded VMEntry instances. """ if vm_entry: - vm_row = VMRow(vm_entry, - show_dispvm_inheritance=not self.sort_running) + vm_row = VMRow(vm_entry, show_dispvm_inheritance=not self.sort_running) vm_row.show_all() vm_entry.entries.append(vm_row) self.vm_list.add(vm_row) @@ -279,15 +295,19 @@ def _is_app_fitting(self, appentry: BaseAppEntry): """ if not self.selected_vm_entry: return False - if appentry.app_info.vm and \ - appentry.app_info.vm.name != \ - self.selected_vm_entry.vm_entry.vm_name: - return self.selected_vm_entry.vm_entry.parent_vm == \ - appentry.app_info.vm.name and \ - not appentry.app_info.disposable + if ( + appentry.app_info.vm + and appentry.app_info.vm.name != self.selected_vm_entry.vm_entry.vm_name + ): + return ( + self.selected_vm_entry.vm_entry.parent_vm == appentry.app_info.vm.name + and not appentry.app_info.disposable + ) if self.selected_vm_entry.vm_entry.is_dispvm_template: - return appentry.app_info.disposable == \ - self.toggle_buttons.apps_toggle.get_active() + return ( + appentry.app_info.disposable + == self.toggle_buttons.apps_toggle.get_active() + ) return True def _vm_keynav_failed(self, _widget, direction: Gtk.DirectionType): @@ -333,8 +353,7 @@ def _selection_changed(self, _widget, row: Optional[VMRow]): self.network_indicator.set_network_state(row.vm_entry.has_network) self.control_list.update_visibility(row.vm_entry.power_state) self.control_list.unselect_all() - self.app_list.ephemeral_vm = bool( - self.selected_vm_entry.vm_entry.parent_vm) + self.app_list.ephemeral_vm = bool(self.selected_vm_entry.vm_entry.parent_vm) self.app_list.invalidate_filter() def _set_right_visibility(self, visibility: bool): diff --git a/qubes_menu/appmenu.py b/qubes_menu/appmenu.py index 58cbc4b..3a42c43 100644 --- a/qubes_menu/appmenu.py +++ b/qubes_menu/appmenu.py @@ -23,35 +23,43 @@ from .custom_widgets import SelfAwareMenu from .vm_manager import VMManager from .page_handler import MenuPage -from .constants import INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \ - POSITION_FEATURE +from .constants import ( + INITIAL_PAGE_FEATURE, + SORT_RUNNING_FEATURE, + POSITION_FEATURE, +) import gi -gi.require_version('Gtk', '3.0') -gi.require_version('GtkLayerShell', '0.1') + +gi.require_version("Gtk", "3.0") +gi.require_version("GtkLayerShell", "0.1") from gi.repository import Gtk, Gdk, GLib, Gio, GtkLayerShell try: from gi.events import GLibEventLoopPolicy + asyncio.set_event_loop_policy(GLibEventLoopPolicy()) HAS_GBULB = False except ImportError: import gbulb + gbulb.install() HAS_GBULB = True -PAGE_LIST = [ - "search_page", "app_page", "favorites_page", "settings_page" -] +PAGE_LIST = ["search_page", "app_page", "favorites_page", "settings_page"] POSITION_LIST = [ - "mouse", "top-left", "top-right", "bottom-left", "bottom-right" + "mouse", + "top-left", + "top-right", + "bottom-left", + "bottom-right", ] -logger = logging.getLogger('qubes-appmenu') +logger = logging.getLogger("qubes-appmenu") -def load_theme(widget: Gtk.Widget, light_theme_path: str, - dark_theme_path: str): + +def load_theme(widget: Gtk.Widget, light_theme_path: str, dark_theme_path: str): """ Load a dark or light theme to current screen, based on widget's current (system) defaults. @@ -65,18 +73,20 @@ def load_theme(widget: Gtk.Widget, light_theme_path: str, provider = Gtk.CssProvider() provider.load_from_path(path) Gtk.StyleContext.add_provider_for_screen( - screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) def is_theme_light(widget): """Check if current theme is light or dark""" style_context: Gtk.StyleContext = widget.get_style_context() background_color: Gdk.RGBA = style_context.get_background_color( - Gtk.StateType.NORMAL) - text_color: Gdk.RGBA = style_context.get_color( - Gtk.StateType.NORMAL) - background_intensity = background_color.red + \ - background_color.blue + background_color.green + Gtk.StateType.NORMAL + ) + text_color: Gdk.RGBA = style_context.get_color(Gtk.StateType.NORMAL) + background_intensity = ( + background_color.red + background_color.blue + background_color.green + ) text_intensity = text_color.red + text_color.blue + text_color.green return text_intensity < background_intensity @@ -86,13 +96,16 @@ class AppMenu(Gtk.Application): """ Main Gtk.Application for appmenu. """ + def __init__(self, qapp, dispatcher): """ :param qapp: qubesadmin.Qubes object :param dispatcher: qubesadmin.vm.EventsDispatcher """ - super().__init__(application_id='org.qubesos.appmenu', - flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,) + super().__init__( + application_id="org.qubesos.appmenu", + flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, + ) self.qapp = qapp self.dispatcher = dispatcher self.primary = False @@ -122,7 +135,7 @@ def __init__(self, qapp, dispatcher): self.highlight_tag: Optional[str] = None self.tasks = [] - self.appmenu_position: str = 'mouse' + self.appmenu_position: str = "mouse" def _add_cli_options(self): self.add_main_option( @@ -135,12 +148,12 @@ def _add_cli_options(self): ) self.add_main_option( - 'page', - ord('p'), + "page", + ord("p"), GLib.OptionFlags.NONE, GLib.OptionArg.INT, "Open menu at selected page; 1 is the apps page 1 is the favorites " - "page and 2 is the system tools page" + "page and 2 is the system tools page", ) self.add_main_option( @@ -187,20 +200,21 @@ def _do_power_button(self, _widget): dbus = Gio.bus_get_sync(Gio.BusType.SESSION, None) proxy = Gio.DBusProxy.new_sync( dbus, # dbus - Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS | - Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, # flags + Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS + | Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, # flags None, # info "org.kde.LogoutPrompt", # bus name "/LogoutPrompt", # object_path - "org.kde.LogoutPrompt") # interface + "org.kde.LogoutPrompt", + ) # interface proxy.call( - 'promptAll', # method name + "promptAll", # method name None, # parameters 0, # flags - 0 # timeout_msec + 0, # timeout_msec ) else: - subprocess.Popen('xfce4-session-logout', stdin=subprocess.DEVNULL) + subprocess.Popen("xfce4-session-logout", stdin=subprocess.DEVNULL) def reposition(self): """ @@ -208,46 +222,59 @@ def reposition(self): """ assert self.main_window match self.appmenu_position: - case 'top-left': + case "top-left": if self.layer_shell: - GtkLayerShell.set_anchor(self.main_window, - GtkLayerShell.Edge.LEFT, True) - GtkLayerShell.set_anchor(self.main_window, - GtkLayerShell.Edge.TOP, True) + GtkLayerShell.set_anchor( + self.main_window, GtkLayerShell.Edge.LEFT, True + ) + GtkLayerShell.set_anchor( + self.main_window, GtkLayerShell.Edge.TOP, True + ) else: self.main_window.move(0, 0) - case 'top-right': + case "top-right": if self.layer_shell: - GtkLayerShell.set_anchor(self.main_window, - GtkLayerShell.Edge.RIGHT, True) - GtkLayerShell.set_anchor(self.main_window, - GtkLayerShell.Edge.TOP, True) + GtkLayerShell.set_anchor( + self.main_window, GtkLayerShell.Edge.RIGHT, True + ) + GtkLayerShell.set_anchor( + self.main_window, GtkLayerShell.Edge.TOP, True + ) else: self.main_window.move( - self.main_window.get_screen().get_width() - - self.main_window.get_size().width, 0) - case 'bottom-left': + self.main_window.get_screen().get_width() + - self.main_window.get_size().width, + 0, + ) + case "bottom-left": if self.layer_shell: - GtkLayerShell.set_anchor(self.main_window, - GtkLayerShell.Edge.LEFT, True) - GtkLayerShell.set_anchor(self.main_window, - GtkLayerShell.Edge.BOTTOM, True) + GtkLayerShell.set_anchor( + self.main_window, GtkLayerShell.Edge.LEFT, True + ) + GtkLayerShell.set_anchor( + self.main_window, GtkLayerShell.Edge.BOTTOM, True + ) else: - self.main_window.move(0, - self.main_window.get_screen().get_height() - - self.main_window.get_size().height) - case 'bottom-right': + self.main_window.move( + 0, + self.main_window.get_screen().get_height() + - self.main_window.get_size().height, + ) + case "bottom-right": if self.layer_shell: - GtkLayerShell.set_anchor(self.main_window, - GtkLayerShell.Edge.RIGHT, True) - GtkLayerShell.set_anchor(self.main_window, - GtkLayerShell.Edge.BOTTOM, True) + GtkLayerShell.set_anchor( + self.main_window, GtkLayerShell.Edge.RIGHT, True + ) + GtkLayerShell.set_anchor( + self.main_window, GtkLayerShell.Edge.BOTTOM, True + ) else: self.main_window.move( - self.main_window.get_screen().get_width() - - self.main_window.get_size().width, - self.main_window.get_screen().get_height() - - self.main_window.get_size().height) + self.main_window.get_screen().get_width() + - self.main_window.get_size().width, + self.main_window.get_screen().get_height() + - self.main_window.get_size().height, + ) def __present(self) -> None: assert self.main_window is not None @@ -264,12 +291,14 @@ def __present(self) -> None: assert max_height > 0 # The default for layer shell is no keyboard input. # Explicitly request exclusive access to the keyboard. - GtkLayerShell.set_keyboard_mode(self.main_window, - GtkLayerShell.KeyboardMode.EXCLUSIVE) + GtkLayerShell.set_keyboard_mode( + self.main_window, GtkLayerShell.KeyboardMode.EXCLUSIVE + ) # Work around https://github.com/wmww/gtk-layer-shell/issues/167 # by explicitly setting the window size. - self.main_window.set_size_request(current_width, - min(current_height, max_height)) + self.main_window.set_size_request( + current_width, min(current_height, max_height) + ) def do_activate(self, *args, **kwargs): """ @@ -295,13 +324,14 @@ def do_activate(self, *args, **kwargs): if not self.start_in_background: # The default for layer shell is no keyboard input. # Explicitly request exclusive access to the keyboard. - GtkLayerShell.set_keyboard_mode(self.main_window, - GtkLayerShell.KeyboardMode.EXCLUSIVE) + GtkLayerShell.set_keyboard_mode( + self.main_window, GtkLayerShell.KeyboardMode.EXCLUSIVE + ) # Work around https://github.com/wmww/gtk-layer-shell/issues/167 # by explicitly setting the window size. self.main_window.set_size_request( - current_width, - min(current_height, max_height)) + current_width, min(current_height, max_height) + ) elif current_height > max_height: self.main_window.resize(current_height, max_height) @@ -314,8 +344,7 @@ def do_activate(self, *args, **kwargs): ] else: if self.main_notebook: - self.main_notebook.set_current_page( - PAGE_LIST.index(self.initial_page)) + self.main_notebook.set_current_page(PAGE_LIST.index(self.initial_page)) if self.main_window: self.main_window.set_keep_above(True) if self.main_window.is_visible() and not self.keep_visible: @@ -330,7 +359,7 @@ def hide_menu(self): app or clicking outside the menu. """ # reset search tab - self.handlers['search_page'].initialize_page() + self.handlers["search_page"].initialize_page() if not self.keep_visible and self.main_window: self.main_window.hide() @@ -344,7 +373,7 @@ def _key_press(self, _widget, event): if event.keyval == Gdk.KEY_Escape: self.hide_menu() if event.keyval == Gdk.KEY_space: - search_page = self.handlers.get('search_page') + search_page = self.handlers.get("search_page") if isinstance(search_page, SearchPage): p = search_page.search_entry.get_position() search_page.search_entry.insert_text(" ", p) @@ -369,8 +398,7 @@ def initialize_state(self): for page in self.handlers.values(): page.initialize_page() if self.main_notebook: - self.main_notebook.set_current_page( - PAGE_LIST.index(self.initial_page)) + self.main_notebook.set_current_page(PAGE_LIST.index(self.initial_page)) def perform_setup(self): """ @@ -380,59 +408,70 @@ def perform_setup(self): # build the frontend self.builder = Gtk.Builder() - self.fav_app_list = self.builder.get_object('fav_app_list') - self.sys_tools_list = self.builder.get_object('sys_tools_list') + self.fav_app_list = self.builder.get_object("fav_app_list") + self.sys_tools_list = self.builder.get_object("sys_tools_list") - glade_path = (importlib.resources.files('qubes_menu') / - 'qubes-menu.glade') + glade_path = importlib.resources.files("qubes_menu") / "qubes-menu.glade" with importlib.resources.as_file(glade_path) as path: self.builder.add_from_file(str(path)) - self.main_window = self.builder.get_object('main_window') + self.main_window = self.builder.get_object("main_window") self.layer_shell = GtkLayerShell.is_supported() - self.main_notebook = self.builder.get_object('main_notebook') + self.main_notebook = self.builder.get_object("main_notebook") self.main_window.set_events(Gdk.EventMask.FOCUS_CHANGE_MASK) - self.main_window.connect('focus-out-event', self._focus_out) - self.main_window.connect('key_press_event', self._key_press) + self.main_window.connect("focus-out-event", self._focus_out) + self.main_window.connect("key_press_event", self._key_press) self.add_window(self.main_window) self.desktop_file_manager = DesktopFileManager(self.qapp) self.vm_manager = VMManager(self.qapp, self.dispatcher) self.handlers = { - 'search_page': SearchPage(self.vm_manager, self.builder, - self.desktop_file_manager), - 'app_page': AppPage(self.vm_manager, self.builder, - self.desktop_file_manager), - 'favorites_page': FavoritesPage(self.qapp, self.builder, - self.desktop_file_manager, - self.dispatcher, self.vm_manager), - 'settings_page': SettingsPage(self.qapp, self.builder, - self.desktop_file_manager, - self.dispatcher)} - self.power_button = self.builder.get_object('power_button') - self.power_button.connect('clicked', self._do_power_button) - self.main_notebook.connect('switch-page', self._handle_page_switch) - self.connect('shutdown', self.do_shutdown) + "search_page": SearchPage( + self.vm_manager, self.builder, self.desktop_file_manager + ), + "app_page": AppPage( + self.vm_manager, self.builder, self.desktop_file_manager + ), + "favorites_page": FavoritesPage( + self.qapp, + self.builder, + self.desktop_file_manager, + self.dispatcher, + self.vm_manager, + ), + "settings_page": SettingsPage( + self.qapp, + self.builder, + self.desktop_file_manager, + self.dispatcher, + ), + } + self.power_button = self.builder.get_object("power_button") + self.power_button.connect("clicked", self._do_power_button) + self.main_notebook.connect("switch-page", self._handle_page_switch) + self.connect("shutdown", self.do_shutdown) self.main_window.add_events(Gdk.EventMask.KEY_PRESS_MASK) - self.main_window.connect('key_press_event', self._key_pressed) + self.main_window.connect("key_press_event", self._key_pressed) self.load_style() - Gtk.Settings.get_default().connect('notify::gtk-theme-name', - self.load_style) + Gtk.Settings.get_default().connect("notify::gtk-theme-name", self.load_style) self.load_settings() # monitor for settings changes - for feature in [INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \ - POSITION_FEATURE]: + for feature in [ + INITIAL_PAGE_FEATURE, + SORT_RUNNING_FEATURE, + POSITION_FEATURE, + ]: self.dispatcher.add_handler( - 'domain-feature-set:' + feature, - self._update_settings) + "domain-feature-set:" + feature, self._update_settings + ) self.dispatcher.add_handler( - 'domain-feature-delete:' + feature, - self._update_settings) + "domain-feature-delete:" + feature, self._update_settings + ) if self.layer_shell: GtkLayerShell.init_for_window(self.main_window) @@ -440,28 +479,31 @@ def perform_setup(self): def load_style(self, *_args): """Load appropriate CSS stylesheet and associated properties.""" - light_ref = (importlib.resources.files('qubes_menu') / - 'qubes-menu-light.css') - dark_ref = (importlib.resources.files('qubes_menu') / - 'qubes-menu-dark.css') - - with importlib.resources.as_file(light_ref) as light_path, \ - importlib.resources.as_file(dark_ref) as dark_path: - load_theme(self.main_window, - light_theme_path=str(light_path), - dark_theme_path=str(dark_path)) + light_ref = importlib.resources.files("qubes_menu") / "qubes-menu-light.css" + dark_ref = importlib.resources.files("qubes_menu") / "qubes-menu-dark.css" + + with ( + importlib.resources.as_file(light_ref) as light_path, + importlib.resources.as_file(dark_ref) as dark_path, + ): + load_theme( + self.main_window, + light_theme_path=str(light_path), + dark_theme_path=str(dark_path), + ) label = Gtk.Label() style_context: Gtk.StyleContext = label.get_style_context() - style_context.add_class('search_highlight') + style_context.add_class("search_highlight") bg_color = style_context.get_background_color(Gtk.StateFlags.NORMAL) fg_color = style_context.get_color(Gtk.StateFlags.NORMAL) # This converts a Gdk.RGBA color to a hex representation liked by span # tags in Pango - self.highlight_tag = \ - f'' + ) def load_settings(self): """Load settings from dom0 features.""" @@ -472,8 +514,7 @@ def load_settings(self): initial_page = "app_page" self.initial_page = initial_page - self.sort_running = \ - bool(local_vm.features.get(SORT_RUNNING_FEATURE, False)) + self.sort_running = bool(local_vm.features.get(SORT_RUNNING_FEATURE, False)) position = local_vm.features.get(POSITION_FEATURE, "mouse") if position not in POSITION_LIST: @@ -494,14 +535,17 @@ def _update_settings(self, vm, _event, **_kwargs): @staticmethod def _rgba_color_to_hex(color: Gdk.RGBA): - return '#' + ''.join([f'{int(c*255):0>2x}' - for c in (color.red, color.green, color.blue)]) + return "#" + "".join( + [f"{int(c*255):0>2x}" for c in (color.red, color.green, color.blue)] + ) def _key_pressed(self, _widget, event_key: Gdk.EventKey): """If user presses a non-control key, move to search.""" - if Gdk.keyval_to_unicode(event_key.keyval) > 32 or \ - event_key.keyval == Gdk.KEY_BackSpace: - search_page = self.handlers.get('search_page') + if ( + Gdk.keyval_to_unicode(event_key.keyval) > 32 + or event_key.keyval == Gdk.KEY_BackSpace + ): + search_page = self.handlers.get("search_page") if not isinstance(search_page, SearchPage): return False @@ -531,7 +575,8 @@ def get_currently_selected_vm(self): """ assert self.main_notebook current_page_handler = self.handlers[ - PAGE_LIST[self.main_notebook.get_current_page()]] + PAGE_LIST[self.main_notebook.get_current_page()] + ] if hasattr(current_page_handler, "get_selected_vm"): return current_page_handler.get_selected_vm() return None @@ -556,5 +601,5 @@ def main(): app.run(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/qubes_menu/constants.py b/qubes_menu/constants.py index d78c601..d8a5c84 100644 --- a/qubes_menu/constants.py +++ b/qubes_menu/constants.py @@ -23,25 +23,25 @@ """ STATE_DICTIONARY = { - 'domain-pre-start': 'Transient', - 'domain-start': 'Running', - 'domain-start-failed': 'Halted', - 'domain-paused': 'Paused', - 'domain-unpaused': 'Running', - 'domain-shutdown': 'Halted', - 'domain-pre-shutdown': 'Transient', - 'domain-shutdown-failed': 'Running' + "domain-pre-start": "Transient", + "domain-start": "Running", + "domain-start-failed": "Halted", + "domain-paused": "Paused", + "domain-unpaused": "Running", + "domain-shutdown": "Halted", + "domain-pre-shutdown": "Transient", + "domain-shutdown-failed": "Running", } -INITIAL_PAGE_FEATURE = 'menu-initial-page' -SORT_RUNNING_FEATURE = 'menu-sort-running' -POSITION_FEATURE = 'menu-position' +INITIAL_PAGE_FEATURE = "menu-initial-page" +SORT_RUNNING_FEATURE = "menu-sort-running" +POSITION_FEATURE = "menu-position" -FAVORITES_FEATURE = 'menu-favorites' -DISPOSABLE_PREFIX = '@disp:' +FAVORITES_FEATURE = "menu-favorites" +DISPOSABLE_PREFIX = "@disp:" -RESTART_PARAM_LONG = 'restart' -RESTART_PARAM_SHORT = 'r' +RESTART_PARAM_LONG = "restart" +RESTART_PARAM_SHORT = "r" # Timeout for activation change when hovering over a menu item, in microseconds HOVER_TIMEOUT = 15 diff --git a/qubes_menu/custom_widgets.py b/qubes_menu/custom_widgets.py index 802d0f3..2ad3abd 100644 --- a/qubes_menu/custom_widgets.py +++ b/qubes_menu/custom_widgets.py @@ -29,7 +29,8 @@ from .desktop_file_manager import ApplicationInfo import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk, GLib, Pango @@ -38,6 +39,7 @@ class LimitedWidthLabel(Gtk.Label): Gtk.Label, but with ellipsization and capped at 35 characters wide (which is not coincidentally 4 characters more than maximum VM name length) """ + def __init__(self, label_text=None): """ :param label_text: optional text of the newly instantiated label @@ -52,6 +54,7 @@ def __init__(self, label_text=None): class HoverEventBox(Gtk.EventBox): """An EventBox that grabs provided widget on mouse hover.""" + def __init__(self, focus_widget: Gtk.Widget): super().__init__() self.mouse = False @@ -59,8 +62,8 @@ def __init__(self, focus_widget: Gtk.Widget): self.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK) self.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK) - self.connect('enter-notify-event', self._enter_event) - self.connect('leave-notify-event', self._leave_event) + self.connect("enter-notify-event", self._enter_event) + self.connect("leave-notify-event", self._leave_event) def _enter_event(self, *_args): self.mouse = True @@ -81,6 +84,7 @@ class HoverListBox(Gtk.ListBoxRow): Gtk.ListBoxRow, but selects itself on hover (after a timeout specified in constants.py) """ + def __init__(self): super().__init__() self.mouse = False @@ -90,7 +94,7 @@ def __init__(self): self.event_box.add(self.main_box) self.add(self.event_box) - self.connect('focus-in-event', self._on_focus) + self.connect("focus-in-event", self._on_focus) def _on_focus(self, *_args): if self.get_mapped(): @@ -103,13 +107,14 @@ class SelfAwareMenu(Gtk.Menu): Gtk.Menu, but the class has a counter of number of currently opened menus. There can be only one menu open at a time. """ + OPEN_MENUS = 0 def __init__(self, **kwargs): super().__init__(**kwargs) - self.get_style_context().add_class('right_menu') - self.connect('realize', self._add_to_open) - self.connect('unrealize', self._remove_from_open) + self.get_style_context().add_class("right_menu") + self.connect("realize", self._add_to_open) + self.connect("unrealize", self._remove_from_open) @staticmethod def _add_to_open(*_args): @@ -124,12 +129,13 @@ class FavoritesMenu(SelfAwareMenu): """ Menu for showing add to favorites option. """ + def __init__(self, app_info_getter: Callable[[], ApplicationInfo]): super().__init__() self.app_info_getter = app_info_getter - self.add_menu_item = Gtk.CheckMenuItem(label='Add to favorites') - self.add_menu_item.connect('activate', self._add_to_favorites) + self.add_menu_item = Gtk.CheckMenuItem(label="Add to favorites") + self.add_menu_item.connect("activate", self._add_to_favorites) self.add(self.add_menu_item) self.show_all() @@ -140,7 +146,7 @@ def _has_favorite_sibling(self): """ if self.app_info_getter(): for entry in self.app_info_getter().entries: - if type(entry).__name__ == 'FavoritesAppEntry': + if type(entry).__name__ == "FavoritesAppEntry": return True return False @@ -151,10 +157,14 @@ def _add_to_favorites(self, *_args, **_kwargs): target_vm = self.app_info_getter().vm if not target_vm: target_vm = self.app_info_getter().qapp.domains[ - self.app_info_getter().qapp.local_name] + self.app_info_getter().qapp.local_name + ] - add_to_feature(target_vm, constants.FAVORITES_FEATURE, - self.app_info_getter().entry_name) # type: ignore + add_to_feature( + target_vm, + constants.FAVORITES_FEATURE, + self.app_info_getter().entry_name, + ) # type: ignore def set_menu_state(self): """ @@ -165,8 +175,10 @@ def set_menu_state(self): :return: """ - if (getattr(self.get_parent(), 'ephemeral_vm', False) or - not self.app_info_getter()): + if ( + getattr(self.get_parent(), "ephemeral_vm", False) + or not self.app_info_getter() + ): self.add_menu_item.set_active(False) self.add_menu_item.set_sensitive(False) else: @@ -180,14 +192,17 @@ class NetworkIndicator(Gtk.Box): Network Indicator Gtk.Box - changes appearance when set_network_state is called. """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.icon_size = Gtk.IconSize.LARGE_TOOLBAR self.network_on: Gtk.Image = Gtk.Image.new_from_pixbuf( - load_icon('qappmenu-networking-yes', self.icon_size)) + load_icon("qappmenu-networking-yes", self.icon_size) + ) self.network_off: Gtk.Image = Gtk.Image.new_from_pixbuf( - load_icon('qappmenu-networking-no', self.icon_size)) + load_icon("qappmenu-networking-no", self.icon_size) + ) _, height, _ = Gtk.icon_size_lookup(self.icon_size) self.network_on.set_size_request(-1, height * 1.3) @@ -196,13 +211,13 @@ def __init__(self, *args, **kwargs): self.pack_end(self.network_on, False, True, 10) self.pack_end(self.network_off, False, True, 10) - self.network_on.set_tooltip_text('Qube is networked') - self.network_off.set_tooltip_text('Qube is not networked') + self.network_on.set_tooltip_text("Qube is networked") + self.network_off.set_tooltip_text("Qube is not networked") self.network_on.set_no_show_all(True) self.network_off.set_no_show_all(True) - self.get_style_context().add_class('network_indicator') + self.get_style_context().add_class("network_indicator") def set_network_state(self, state: bool): """ @@ -218,20 +233,20 @@ class SettingsEntry(Gtk.ListBoxRow): """ Gtk.ListBoxRow especially for a (run VM) Settings entry. """ + def __init__(self, desktop_file_manager): super().__init__() self.desktop_file_manager = desktop_file_manager self.event_box = HoverEventBox(focus_widget=self) self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) self.event_box.add(self.hbox) - self.settings_icon = Gtk.Image.new_from_pixbuf( - load_icon('settings-black')) + self.settings_icon = Gtk.Image.new_from_pixbuf(load_icon("settings-black")) self.hbox.pack_start(self.settings_icon, False, False, 5) self.settings_label = Gtk.Label(label="Settings", xalign=0) self.hbox.pack_start(self.settings_label, False, False, 5) - self.get_style_context().add_class('app_entry') + self.get_style_context().add_class("app_entry") self.add(self.event_box) - self.event_box.connect('button-press-event', self.show_menu) + self.event_box.connect("button-press-event", self.show_menu) self.menu = FavoritesMenu(self.get_appinfo) @@ -240,16 +255,17 @@ def __init__(self, desktop_file_manager): def run_app(self, vm): """Run settings for specified vm.""" # pylint: disable=consider-using-with - subprocess.Popen( - ['qubes-vm-settings', vm.name], stdin=subprocess.DEVNULL) + subprocess.Popen(["qubes-vm-settings", vm.name], stdin=subprocess.DEVNULL) self.get_toplevel().get_application().hide_menu() def get_appinfo(self) -> ApplicationInfo: """Get relevant app_info for currently selected vm""" - vm_entry: VMEntry = (self.get_toplevel().get_application(). - get_currently_selected_vm()) + vm_entry: VMEntry = ( + self.get_toplevel().get_application().get_currently_selected_vm() + ) return self.desktop_file_manager.get_app_info_by_name( - vm_entry.settings_desktop_file_name) + vm_entry.settings_desktop_file_name + ) def update_state(self, state): # pylint: disable=unused-argument """Update state: should be always visible.""" @@ -270,8 +286,10 @@ class VMRow(HoverListBox): """ Helper widget representing a VM row. """ - def __init__(self, vm_entry: VMEntry, - show_dispvm_inheritance: Optional[bool] = True): + + def __init__( + self, vm_entry: VMEntry, show_dispvm_inheritance: Optional[bool] = True + ): """ :param vm_entry: VMEntry object, stored and managed by VMManager :param show_dispvm_inheritance: bool, should dispvm children be shown @@ -281,15 +299,15 @@ def __init__(self, vm_entry: VMEntry, self.vm_entry = vm_entry self.vm_name = vm_entry.vm_name self.show_dispvm_inheritance = show_dispvm_inheritance - self.get_style_context().add_class('vm_entry') + self.get_style_context().add_class("vm_entry") self.icon_img = Gtk.Image() self.dispvm_icon = Gtk.Image() # add the icon for dispvm parent existing if self.vm_entry.parent_vm: - dispvm_icon_img = load_icon('qappmenu-dispvm-child', None, 15) + dispvm_icon_img = load_icon("qappmenu-dispvm-child", None, 15) self.dispvm_icon.set_from_pixbuf(dispvm_icon_img) - self.dispvm_icon.get_style_context().add_class('dispvm_icon') + self.dispvm_icon.get_style_context().add_class("dispvm_icon") self.dispvm_icon.set_valign(Gtk.Align.START) self.main_box.pack_start(self.dispvm_icon, False, False, 2) self.dispvm_icon.set_no_show_all(True) @@ -298,35 +316,41 @@ def __init__(self, vm_entry: VMEntry, self.label = Gtk.Label(label=self.vm_entry.vm_name) self.main_box.pack_start(self.label, False, False, 2) - self.update_contents(update_power_state=True, update_label=True, - update_has_network=True, update_type=True) + self.update_contents( + update_power_state=True, + update_label=True, + update_has_network=True, + update_type=True, + ) def update_style(self, update_power_state: bool = True): """Update own style, based on whether VM is running or not and what type it has.""" style_context: Gtk.StyleContext = self.get_style_context() if self.vm_entry.is_dispvm_template: - style_context.add_class('dvm_template_entry') + style_context.add_class("dvm_template_entry") elif self.vm_entry.parent_vm: # has a parent VM means that it should have arrow etc. - style_context.add_class('dispvm_entry') + style_context.add_class("dispvm_entry") else: - style_context.remove_class('dispvm_entry') - style_context.remove_class('dvm_template_entry') + style_context.remove_class("dispvm_entry") + style_context.remove_class("dvm_template_entry") if update_power_state: - if self.vm_entry.power_state == 'Running': - style_context.add_class('running_vm') + if self.vm_entry.power_state == "Running": + style_context.add_class("running_vm") else: - style_context.remove_class('running_vm') + style_context.remove_class("running_vm") self.dispvm_icon.set_visible(self.show_dispvm_inheritance) - def update_contents(self, - update_power_state=False, - update_label=False, - update_has_network=False, - update_type=False): + def update_contents( + self, + update_power_state=False, + update_label=False, + update_has_network=False, + update_type=False, + ): """ Update own contents (or related widgets, if applicable) based on state change. @@ -362,35 +386,41 @@ def sort_order(self): class SearchVMRow(VMRow): """VM Row used for the Search tab.""" - def update_contents(self, - update_power_state=False, - update_label=False, - update_has_network=False, - update_type=False): + + def update_contents( + self, + update_power_state=False, + update_label=False, + update_has_network=False, + update_type=False, + ): """ Search rows do not show power state. """ - super().update_contents(update_power_state=False, - update_label=update_label, - update_has_network=False, - update_type=update_type) + super().update_contents( + update_power_state=False, + update_label=update_label, + update_has_network=False, + update_type=update_type, + ) class AnyVMRow(HoverListBox): """Generic Any VM row for search purposes.""" + def __init__(self): super().__init__() self.vm_name = None - self.sort_order = '' - self.get_style_context().add_class('vm_entry') + self.sort_order = "" + self.get_style_context().add_class("vm_entry") icon_img = Gtk.Image() - icon_vm = load_icon('qubes-logo-icon') + icon_vm = load_icon("qubes-logo-icon") icon_img.set_from_pixbuf(icon_vm) self.main_box.pack_start(icon_img, False, False, 2) self.label = Gtk.Label() - self.label.set_markup('Any qube') + self.label.set_markup("Any qube") self.main_box.pack_start(self.label, False, False, 2) self.show_all() @@ -400,10 +430,11 @@ class ControlRow(Gtk.ListBoxRow): Gtk.ListBoxRow representing one of the VM control options: start/shutdown/ pause etc. """ + def __init__(self): super().__init__() self.row_label = LimitedWidthLabel() - self.get_style_context().add_class('app_entry') + self.get_style_context().add_class("app_entry") self.event_box = HoverEventBox(focus_widget=self) self.add(self.event_box) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) @@ -437,20 +468,23 @@ class StartControlItem(ControlRow): shutdown if it's running, unpause if it's paused, and kill if it's transient. """ + def __init__(self, desktop_file_manager): super().__init__() self.desktop_file_manager = desktop_file_manager self.state = None - self.event_box.connect('button-press-event', self.show_menu) + self.event_box.connect("button-press-event", self.show_menu) self.menu = FavoritesMenu(self.get_appinfo) def get_appinfo(self) -> ApplicationInfo: """Get relevant app_info for currently selected vm""" - vm_entry: VMEntry = (self.get_toplevel().get_application(). - get_currently_selected_vm()) + vm_entry: VMEntry = ( + self.get_toplevel().get_application().get_currently_selected_vm() + ) return self.desktop_file_manager.get_app_info_by_name( - vm_entry.settings_desktop_file_name) + vm_entry.settings_desktop_file_name + ) def show_menu(self, _widget, event): """ @@ -467,30 +501,34 @@ def update_state(self, state): state. """ self.state = state - if state == 'Running': - self.row_label.set_label('Shutdown qube') - self.command = 'qvm-shutdown' - self.icon.set_from_pixbuf(load_icon("qappmenu-shutdown", size=None, - pixel_size=15)) + if state == "Running": + self.row_label.set_label("Shutdown qube") + self.command = "qvm-shutdown" + self.icon.set_from_pixbuf( + load_icon("qappmenu-shutdown", size=None, pixel_size=15) + ) return - if state == 'Transient': - self.row_label.set_label('Kill qube') - self.command = 'qvm-kill' - self.icon.set_from_pixbuf(load_icon("qappmenu-shutdown", size=None, - pixel_size=15)) + if state == "Transient": + self.row_label.set_label("Kill qube") + self.command = "qvm-kill" + self.icon.set_from_pixbuf( + load_icon("qappmenu-shutdown", size=None, pixel_size=15) + ) return - if state == 'Halted': - self.row_label.set_label('Start qube') - self.command = 'qvm-start' - self.icon.set_from_pixbuf(load_icon("qappmenu-start", size=None, - pixel_size=15)) + if state == "Halted": + self.row_label.set_label("Start qube") + self.command = "qvm-start" + self.icon.set_from_pixbuf( + load_icon("qappmenu-start", size=None, pixel_size=15) + ) return - if state == 'Paused': - self.row_label.set_label('Unpause qube') - self.command = 'qvm-unpause' - self.icon.set_from_pixbuf(load_icon("qappmenu-start", size=None, - pixel_size=15)) + if state == "Paused": + self.row_label.set_label("Unpause qube") + self.command = "qvm-unpause" + self.icon.set_from_pixbuf( + load_icon("qappmenu-start", size=None, pixel_size=15) + ) return @@ -498,10 +536,10 @@ class PauseControlItem(ControlRow): """ Control Row item representing pausing VM: visible only when it's running. """ + def __init__(self): super().__init__() - self.icon.set_from_pixbuf(load_icon("qappmenu-pause", size=None, - pixel_size=15)) + self.icon.set_from_pixbuf(load_icon("qappmenu-pause", size=None, pixel_size=15)) self.state = None def update_state(self, state): @@ -510,13 +548,13 @@ def update_state(self, state): state. """ self.state = state - if state == 'Running': - self.row_label.set_label('Pause qube') + if state == "Running": + self.row_label.set_label("Pause qube") self.set_sensitive(True) - self.command = 'qvm-pause' + self.command = "qvm-pause" self.icon.show() return - self.row_label.set_label(' ') + self.row_label.set_label(" ") self.set_sensitive(False) self.command = None self.icon.hide() @@ -526,11 +564,12 @@ class ControlList(Gtk.ListBox): """ ListBox containing VM state control items. """ + def __init__(self, app_page): super().__init__() self.app_page = app_page - self.get_style_context().add_class('control_panel') + self.get_style_context().add_class("control_panel") self.settings_item = SettingsEntry(self.app_page.desktop_file_manager) @@ -554,11 +593,12 @@ class KeynavController: not enough, namely, when we have a bunch of ListBoxes stacked on top of each other. """ + def __init__(self, widgets_in_order: List[Gtk.ListBox]): self.widgets_in_order = widgets_in_order for widget in self.widgets_in_order: - widget.connect('keynav-failed', self._keynav_failed) + widget.connect("keynav-failed", self._keynav_failed) def get_neighbor(self, widget: Gtk.ListBox, direction: Gtk.DirectionType): """Get next widget in given direction""" @@ -569,8 +609,7 @@ def get_neighbor(self, widget: Gtk.ListBox, direction: Gtk.DirectionType): return self.widgets_in_order[(i + 1) % len(self.widgets_in_order)] return None - def _keynav_failed(self, widget: Gtk.ListBox, - direction: Gtk.DirectionType): + def _keynav_failed(self, widget: Gtk.ListBox, direction: Gtk.DirectionType): """ Callback to be performed when keyboard nav fails. Attempts to find next widget and move keyboard focus to it. @@ -579,7 +618,8 @@ def _keynav_failed(self, widget: Gtk.ListBox, if not next_widget: return next_focus_widget = get_visible_child( - next_widget, reverse=direction == Gtk.DirectionType.UP) + next_widget, reverse=direction == Gtk.DirectionType.UP + ) if next_focus_widget: widget.select_row(None) next_focus_widget.grab_focus() diff --git a/qubes_menu/desktop_file_manager.py b/qubes_menu/desktop_file_manager.py index 6f03161..9e65cc1 100644 --- a/qubes_menu/desktop_file_manager.py +++ b/qubes_menu/desktop_file_manager.py @@ -36,7 +36,7 @@ from . import constants -logger = logging.getLogger('qubes-appmenu') +logger = logging.getLogger("qubes-appmenu") def exec_parse(desktop_entry: xdg.DesktopEntry.DesktopEntry): @@ -47,13 +47,24 @@ def exec_parse(desktop_entry: xdg.DesktopEntry.DesktopEntry): split_str = shlex.split(desktop_entry.getExec()) result = [] for s in split_str: - if s in ['%f', '%F', '%u', '%U', '%d', '%D', '%n', '%N', '%v', - '%m', '%k']: + if s in [ + "%f", + "%F", + "%u", + "%U", + "%d", + "%D", + "%n", + "%N", + "%v", + "%m", + "%k", + ]: continue - if s == '%i' and desktop_entry.getIcon(): - result.extend(['--icon', desktop_entry.getIcon()]) + if s == "%i" and desktop_entry.getIcon(): + result.extend(["--icon", desktop_entry.getIcon()]) continue - if s == '%c': + if s == "%c": result.append(desktop_entry.getName()) continue result.append(s) @@ -64,6 +75,7 @@ class ApplicationInfo: """ Class representing data within a single .desktop file. """ + def __init__(self, qapp, file_path): self.qapp: qubesadmin.Qubes = qapp self.file_path: PosixPath = file_path @@ -81,20 +93,20 @@ def __init__(self, qapp, file_path): def load_data(self, entry): """Fill own data with information from xdg.DesktopEntry provided.""" - vm_name = entry.get('X-Qubes-VmName') or None + vm_name = entry.get("X-Qubes-VmName") or None try: self.vm = self.qapp.domains[vm_name] except KeyError: self.vm = None - self.app_name = entry.getName() or '' + self.app_name = entry.getName() or "" if self.vm: self.app_name = self.app_name.split(": ", 1)[-1] self.sort_name = str(self.app_name).lower() self.vm_icon = self.vm.icon if self.vm else None self.app_icon = entry.getIcon() - self.disposable = bool(entry.get('X-Qubes-NonDispvmExec')) - self.entry_name = entry.get('X-Qubes-AppName') or self.file_path.name + self.disposable = bool(entry.get("X-Qubes-NonDispvmExec")) + self.entry_name = entry.get("X-Qubes-AppName") or self.file_path.name if self.disposable: self.entry_name = constants.DISPOSABLE_PREFIX + self.entry_name self.exec = exec_parse(entry) @@ -111,35 +123,40 @@ def get_command_for_vm(self, vm=None): their own .desktop files.""" command = self.exec if vm and not self.vm: - logger.warning('Unexpected command: cannot run local' - ' application for a non-local VM: %s', vm) + logger.warning( + "Unexpected command: cannot run local" + " application for a non-local VM: %s", + vm, + ) return command if vm and str(self.vm) != str(vm): # replace name of the old VM - used for opening apps from DVM # template in their child dispvm if len(command) < 6 or command[5] != str(self.vm): - logger.error( - 'Unexpected command for a disposable VM: %s', command) + logger.error("Unexpected command for a disposable VM: %s", command) return [] return command[:5] + [str(vm)] + command[6:] return command def is_qubes_specific(self): """Check if the current file represents a qubes-generated app.""" - return 'X-Qubes-VM' in self.categories + return "X-Qubes-VM" in self.categories class DesktopFileManager: """ Class that loads, caches and observes changes in .desktop files. """ + desktop_dirs = [ - Path(xdg.BaseDirectory.xdg_data_home) / 'applications', - Path('/usr/share/applications')] + Path(xdg.BaseDirectory.xdg_data_home) / "applications", + Path("/usr/share/applications"), + ] # pylint: disable=invalid-name class EventProcessor(pyinotify.ProcessEvent): """pyinotify helper class""" + def __init__(self, parent): self.parent = parent super().__init__() @@ -183,8 +200,7 @@ def __init__(self, qapp): # directories used by Qubes menu tools, not necessarily all possible # XDG directories - self.current_environments = \ - os.environ.get('XDG_CURRENT_DESKTOP', '').split(':') + self.current_environments = os.environ.get("XDG_CURRENT_DESKTOP", "").split(":") self.app_entries: Dict[Path, ApplicationInfo] = {} @@ -250,14 +266,13 @@ def load_file(self, path: Union[str, Path]): self.remove_file(path) return - if not path.name.endswith('.desktop'): + if not path.name.endswith(".desktop"): return try: entry = xdg.DesktopEntry.DesktopEntry(path) except Exception as ex: # pylint: disable=broad-except - logger.warning( - 'Cannot load desktop entry file %s: %s', path, str(ex)) + logger.warning("Cannot load desktop entry file %s: %s", path, str(ex)) self.remove_file(path) return @@ -286,14 +301,12 @@ def _eligibility_check(self, entry: xdg.DesktopEntry.DesktopEntry): if entry.getNoDisplay(): return False if entry.getOnlyShowIn(): - if not set(entry.getOnlyShowIn()).intersection( - self.current_environments): + if not set(entry.getOnlyShowIn()).intersection(self.current_environments): return False if entry.getNotShowIn(): - if set(entry.getNotShowIn()).intersection( - self.current_environments): + if set(entry.getNotShowIn()).intersection(self.current_environments): return False - if entry.get('X-AppStream-Ignore'): + if entry.get("X-AppStream-Ignore"): return False return True @@ -304,17 +317,23 @@ def _initialize_watchers(self): self.watch_manager = pyinotify.WatchManager() # pylint: disable=no-member - mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE | \ - pyinotify.IN_MODIFY | pyinotify.IN_MOVED_FROM | \ - pyinotify.IN_MOVED_TO + mask = ( + pyinotify.IN_CREATE + | pyinotify.IN_DELETE + | pyinotify.IN_MODIFY + | pyinotify.IN_MOVED_FROM + | pyinotify.IN_MOVED_TO + ) loop = asyncio.get_event_loop() self.notifier = pyinotify.AsyncioNotifier( - self.watch_manager, loop, - default_proc_fun=DesktopFileManager.EventProcessor(self)) + self.watch_manager, + loop, + default_proc_fun=DesktopFileManager.EventProcessor(self), + ) for path in self.desktop_dirs: self.watches.append( - self.watch_manager.add_watch( - str(path), mask, rec=True, auto_add=True)) + self.watch_manager.add_watch(str(path), mask, rec=True, auto_add=True) + ) diff --git a/qubes_menu/favorites_page.py b/qubes_menu/favorites_page.py index b31e1ab..e508d2f 100644 --- a/qubes_menu/favorites_page.py +++ b/qubes_menu/favorites_page.py @@ -30,20 +30,26 @@ from . import constants import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk -logger = logging.getLogger('qubes-appmenu') +logger = logging.getLogger("qubes-appmenu") class FavoritesPage(MenuPage): """ Helper class for managing the entirety of Favorites menu page. """ - def __init__(self, qapp: qubesadmin.Qubes, builder: Gtk.Builder, - desktop_file_manager: DesktopFileManager, - dispatcher: qubesadmin.events.EventsDispatcher, - vm_manager: VMManager): + + def __init__( + self, + qapp: qubesadmin.Qubes, + builder: Gtk.Builder, + desktop_file_manager: DesktopFileManager, + dispatcher: qubesadmin.events.EventsDispatcher, + vm_manager: VMManager, + ): self.qapp = qapp self.desktop_file_manager = desktop_file_manager self.dispatcher = dispatcher @@ -51,24 +57,27 @@ def __init__(self, qapp: qubesadmin.Qubes, builder: Gtk.Builder, self.page_widget: Gtk.Box = builder.get_object("favorites_page") - self.app_list: Gtk.ListBox = builder.get_object('fav_app_list') - self.app_list.connect('row-activated', self._app_clicked) + self.app_list: Gtk.ListBox = builder.get_object("fav_app_list") + self.app_list.connect("row-activated", self._app_clicked) self.app_list.set_sort_func( - lambda x, y: x.app_info.sort_name > y.app_info.sort_name) + lambda x, y: x.app_info.sort_name > y.app_info.sort_name + ) self.desktop_file_manager.register_callback(self._app_info_callback) self.app_list.show_all() self.app_list.invalidate_sort() self.app_list.set_selection_mode(Gtk.SelectionMode.NONE) self.dispatcher.add_handler( - f'domain-feature-delete:{constants.FAVORITES_FEATURE}', - self._feature_deleted) + f"domain-feature-delete:{constants.FAVORITES_FEATURE}", + self._feature_deleted, + ) self.dispatcher.add_handler( - f'domain-feature-set:{constants.FAVORITES_FEATURE}', - self._feature_set) - self.dispatcher.add_handler('domain-add', self._domain_added) - self.dispatcher.add_handler('domain-delete', self._domain_deleted) + f"domain-feature-set:{constants.FAVORITES_FEATURE}", + self._feature_set, + ) + self.dispatcher.add_handler("domain-add", self._domain_added) + self.dispatcher.add_handler("domain-delete", self._domain_deleted) def _load_vms_favorites(self, vm): """ @@ -80,14 +89,15 @@ def _load_vms_favorites(self, vm): vm = self.qapp.domains[vm] except KeyError: return - favorites = vm.features.get(constants.FAVORITES_FEATURE, '') - favorites = favorites.split(' ') + favorites = vm.features.get(constants.FAVORITES_FEATURE, "") + favorites = favorites.split(" ") is_local_vm = vm.name == self.qapp.local_name for app_info in self.desktop_file_manager.get_app_infos(): - if (not is_local_vm and app_info.vm == vm)\ - or (is_local_vm and not app_info.vm): + if (not is_local_vm and app_info.vm == vm) or ( + is_local_vm and not app_info.vm + ): if app_info.entry_name in favorites: self._add_from_app_info(app_info) self.app_list.invalidate_sort() @@ -100,7 +110,7 @@ def _app_info_callback(self, app_info): else: vm = app_info.qapp.domains[app_info.qapp.local_name] - feature = vm.features.get(constants.FAVORITES_FEATURE, '').split(' ') + feature = vm.features.get(constants.FAVORITES_FEATURE, "").split(" ") if app_info.entry_name in feature: self._add_from_app_info(app_info) @@ -124,8 +134,7 @@ def _feature_deleted(self, vm, _event, _feature, *_args, **_kwargs): self.app_list.remove(child) self.app_list.invalidate_sort() except Exception as ex: # pylint: disable=broad-except - logger.warning( - 'Encountered problem removing favorite entry: %s', repr(ex)) + logger.warning("Encountered problem removing favorite entry: %s", repr(ex)) def _feature_set(self, vm, event, feature, *_args, **_kwargs): """When VM feature specified in constants.py is changed, all existing @@ -135,8 +144,7 @@ def _feature_set(self, vm, event, feature, *_args, **_kwargs): self._feature_deleted(vm, event, feature) self._load_vms_favorites(vm) except Exception as ex: # pylint: disable=broad-except - logger.warning( - 'Encountered problem adding favorite entry: %s', repr(ex)) + logger.warning("Encountered problem adding favorite entry: %s", repr(ex)) def _domain_added(self, _submitter, _event, vm, **_kwargs): """On a newly created domain, load all favorites from features diff --git a/qubes_menu/page_handler.py b/qubes_menu/page_handler.py index eca31c2..e2d1882 100644 --- a/qubes_menu/page_handler.py +++ b/qubes_menu/page_handler.py @@ -21,12 +21,14 @@ import abc import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk class MenuPage(abc.ABC): """Abstract Menu Page.""" + page_widget: Gtk.Widget @abc.abstractmethod diff --git a/qubes_menu/search_page.py b/qubes_menu/search_page.py index 2a085a2..ab19c25 100644 --- a/qubes_menu/search_page.py +++ b/qubes_menu/search_page.py @@ -28,7 +28,8 @@ from .utils import load_icon, parse_search import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk @@ -36,29 +37,31 @@ class RecentSearchRow(Gtk.ListBoxRow): """ Gtk.ListBoxRow with a recently searched text. """ + def __init__(self, search_text: str): super().__init__() self.search_text = search_text self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - self.recent_icon = Gtk.Image.new_from_pixbuf( - load_icon('qappmenu-search')) + self.recent_icon = Gtk.Image.new_from_pixbuf(load_icon("qappmenu-search")) self.hbox.pack_start(self.recent_icon, False, False, 5) self.search_label = Gtk.Label(label=search_text, xalign=0) self.hbox.pack_start(self.search_label, False, False, 5) - self.get_style_context().add_class('app_entry') + self.get_style_context().add_class("app_entry") self.add(self.hbox) self.show_all() class RecentSearchManager: """Class for managing the list of recent searches.""" + SEARCH_VALUES_TO_KEEP = 10 + def __init__(self, recent_list: Gtk.ListBox, search_box: Gtk.SearchEntry): self.recent_list_box = recent_list self.search_box = search_box self.recent_searches: Dict[str, RecentSearchRow] = {} - self.recent_list_box.connect('row-activated', self._row_clicked) + self.recent_list_box.connect("row-activated", self._row_clicked) def add_new_recent_search(self, text: str): """Add new recent search entry""" @@ -89,8 +92,13 @@ class SearchPage(MenuPage): """ Helper class for managing the Search menu page. """ - def __init__(self, vm_manager: VMManager, builder: Gtk.Builder, - desktop_file_manager: DesktopFileManager): + + def __init__( + self, + vm_manager: VMManager, + builder: Gtk.Builder, + desktop_file_manager: DesktopFileManager, + ): """ :param vm_manager: VM Manager object :param builder: Gtk.Builder with loaded glade object @@ -103,22 +111,22 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder, self.sort_running = False # sort running vms to top - self.vm_list: Gtk.ListBox = builder.get_object('search_vm_list') - self.app_list: Gtk.ListBox = builder.get_object('search_app_list') - self.search_entry: Gtk.SearchEntry = builder.get_object('search_entry') + self.vm_list: Gtk.ListBox = builder.get_object("search_vm_list") + self.app_list: Gtk.ListBox = builder.get_object("search_app_list") + self.search_entry: Gtk.SearchEntry = builder.get_object("search_entry") self.selected_vm_row: Optional[SearchVMRow] = None self.filtered_vms: Set[str] = set() - self.main_notebook = builder.get_object('main_notebook') + self.main_notebook = builder.get_object("main_notebook") - self.search_entry.connect('search-changed', self._do_search) - self.search_entry.connect('key-press-event', self._search_key_press) + self.search_entry.connect("search-changed", self._do_search) + self.search_entry.connect("key-press-event", self._search_key_press) desktop_file_manager.register_callback(self._app_info_callback) self.app_list.set_filter_func(self._is_app_fitting) - self.app_list.connect('row-activated', self._app_clicked) + self.app_list.connect("row-activated", self._app_clicked) self.vm_list.add(AnyVMRow()) vm_manager.register_new_vm_callback(self._vm_callback) @@ -129,37 +137,35 @@ def __init__(self, vm_manager: VMManager, builder: Gtk.Builder, self.app_list.invalidate_sort() self.vm_list.invalidate_sort() - self.recent_list: Gtk.ListBox = builder.get_object('search_recent_list') + self.recent_list: Gtk.ListBox = builder.get_object("search_recent_list") - self.app_view: Gtk.ScrolledWindow = \ - builder.get_object("search_app_view") - self.app_placeholder: Gtk.Label = \ - builder.get_object('search_app_placeholder') + self.app_view: Gtk.ScrolledWindow = builder.get_object("search_app_view") + self.app_placeholder: Gtk.Label = builder.get_object("search_app_placeholder") self.vm_view: Gtk.ScrolledWindow = builder.get_object("search_vm_view") - self.recent_view: Gtk.ScrolledWindow = \ - builder.get_object("search_recent_view") - self.recent_title: Gtk.Label = builder.get_object('search_recent_title') + self.recent_view: Gtk.ScrolledWindow = builder.get_object("search_recent_view") + self.recent_title: Gtk.Label = builder.get_object("search_recent_title") self.recent_search_manager = RecentSearchManager( - self.recent_list, self.search_entry) + self.recent_list, self.search_entry + ) - self.vm_list.connect('row-selected', self._selection_changed) - self.search_entry.connect('activate', self._move_to_first) + self.vm_list.connect("row-selected", self._selection_changed) + self.search_entry.connect("activate", self._move_to_first) self.control_list = ControlList(self) self.page_widget.attach(self.control_list, 1, 4, 1, 1) - self.control_list.connect('row-activated', self._app_clicked) + self.control_list.connect("row-activated", self._app_clicked) self.control_list.set_selection_mode(Gtk.SelectionMode.NONE) self.keynav_manager = KeynavController( - widgets_in_order=[self.app_list, self.control_list]) + widgets_in_order=[self.app_list, self.control_list] + ) def _app_clicked(self, _widget, row): - self.recent_search_manager.add_new_recent_search( - self.search_entry.get_text()) + self.recent_search_manager.add_new_recent_search(self.search_entry.get_text()) if self.selected_vm_row: row.run_app(self.selected_vm_row.vm_entry.vm) - elif hasattr(row, 'app_info'): + elif hasattr(row, "app_info"): row.run_app(row.app_info.vm) def _app_info_callback(self, app_info): @@ -189,8 +195,7 @@ def _do_search(self, *_args): self._filter_lists() - self.vm_view.set_visible(has_search and - not self.app_placeholder.get_mapped()) + self.vm_view.set_visible(has_search and not self.app_placeholder.get_mapped()) if not self.app_placeholder.get_mapped(): for row in self.app_list.get_children(): @@ -297,7 +302,7 @@ def initialize_page(self): """ Initialize own state. """ - self.search_entry.set_text('') + self.search_entry.set_text("") self.app_list.select_row(None) self.vm_list.select_row(None) self.app_view.set_visible(False) diff --git a/qubes_menu/settings_page.py b/qubes_menu/settings_page.py index cd69428..ffc6351 100644 --- a/qubes_menu/settings_page.py +++ b/qubes_menu/settings_page.py @@ -28,7 +28,8 @@ from .page_handler import MenuPage import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -37,13 +38,14 @@ class SettingsCategoryRow(custom_widgets.HoverListBox): A custom widget representing a category of Settings; selects itself on hover. """ + def __init__(self, name, filter_func): super().__init__() self.name = name self.label = custom_widgets.LimitedWidthLabel(self.name) self.main_box.add(self.label) self.filter_func = filter_func - self.get_style_context().add_class('settings_category_row') + self.get_style_context().add_class("settings_category_row") self.show_all() @@ -51,31 +53,37 @@ class SettingsPage(MenuPage): """ Helper class for managing the entirety of Settings menu page. """ - def __init__(self, qapp, builder: Gtk.Builder, - desktop_file_manager: DesktopFileManager, - dispatcher: qubesadmin.events.EventsDispatcher): + + def __init__( + self, + qapp, + builder: Gtk.Builder, + desktop_file_manager: DesktopFileManager, + dispatcher: qubesadmin.events.EventsDispatcher, + ): self.qapp = qapp self.desktop_file_manager = desktop_file_manager self.dispatcher = dispatcher self.page_widget: Gtk.Box = builder.get_object("settings_page") - self.app_list: Gtk.ListBox = builder.get_object('sys_tools_list') - self.app_list.connect('row-activated', self._app_clicked) + self.app_list: Gtk.ListBox = builder.get_object("sys_tools_list") + self.app_list.connect("row-activated", self._app_clicked) self.app_list.set_sort_func( - lambda x, y: x.app_info.sort_name > y.app_info.sort_name) + lambda x, y: x.app_info.sort_name > y.app_info.sort_name + ) self.app_list.set_filter_func(self._filter_apps) - self.category_list: Gtk.ListBox = builder.get_object( - 'settings_categories') + self.category_list: Gtk.ListBox = builder.get_object("settings_categories") - self.category_list.connect('row-selected', self._category_clicked) - self.category_list.add(SettingsCategoryRow('Qubes Tools', - self._filter_qubes_tools)) + self.category_list.connect("row-selected", self._category_clicked) + self.category_list.add( + SettingsCategoryRow("Qubes Tools", self._filter_qubes_tools) + ) self.category_list.add( - SettingsCategoryRow('System Settings', - self._filter_system_settings)) - self.category_list.add(SettingsCategoryRow('Other', self._filter_other)) + SettingsCategoryRow("System Settings", self._filter_system_settings) + ) + self.category_list.add(SettingsCategoryRow("Other", self._filter_other)) self.desktop_file_manager.register_callback(self._app_info_callback) @@ -88,30 +96,32 @@ def initialize_page(self): self.category_list.select_row(None) def _filter_apps(self, row): - filter_func = getattr(self.category_list.get_selected_row(), - 'filter_func', None) + filter_func = getattr( + self.category_list.get_selected_row(), "filter_func", None + ) if not filter_func: return False return filter_func(row) @staticmethod def _filter_qubes_tools(row): - if 'X-XFCE-SettingsDialog' not in row.app_info.categories: + if "X-XFCE-SettingsDialog" not in row.app_info.categories: return False - return 'qubes' in row.app_info.entry_name + return "qubes" in row.app_info.entry_name @staticmethod def _filter_system_settings(row): - if 'X-XFCE-SettingsDialog' in row.app_info.categories: - return 'qubes' not in row.app_info.entry_name - if 'Settings' in row.app_info.categories: + if "X-XFCE-SettingsDialog" in row.app_info.categories: + return "qubes" not in row.app_info.entry_name + if "Settings" in row.app_info.categories: return True return False @staticmethod def _filter_other(row): - return not SettingsPage._filter_qubes_tools(row) and \ - not SettingsPage._filter_system_settings(row) + return not SettingsPage._filter_qubes_tools( + row + ) and not SettingsPage._filter_system_settings(row) def _category_clicked(self, *_args): self.app_list.invalidate_filter() diff --git a/qubes_menu/tests/conftest.py b/qubes_menu/tests/conftest.py index b43c7ff..1d92fe5 100644 --- a/qubes_menu/tests/conftest.py +++ b/qubes_menu/tests/conftest.py @@ -24,7 +24,8 @@ import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -38,8 +39,7 @@ def test_builder(): """Gtk builder with correct menu glade file""" builder = Gtk.Builder() - glade_path = (importlib.resources.files('qubes_menu') / - 'qubes-menu.glade') + glade_path = importlib.resources.files("qubes_menu") / "qubes-menu.glade" with importlib.resources.as_file(glade_path) as path: builder.add_from_file(str(path)) @@ -48,7 +48,7 @@ def test_builder(): @pytest.fixture def test_desktop_file_path(tmp_path): - app_entry1 = b''' + app_entry1 = b""" [Desktop Entry] Version=1.0 Type=Application @@ -61,9 +61,9 @@ def test_desktop_file_path(tmp_path): Categories=System;TerminalEmulator;X-Qubes-VM; Exec=qvm-run -q -a --service -- test-vm qubes.StartApp+xterm X-Qubes-DispvmExec=qvm-run -q -a --service --dispvm=test-vm -- qubes.StartApp+xterm - ''' + """ - app_entry2 = b''' + app_entry2 = b""" [Desktop Entry] Version=1.0 Type=Application @@ -77,9 +77,9 @@ def test_desktop_file_path(tmp_path): Categories=System;X-Qubes-VM; Exec=qvm-run -q -a --service -- test-red qubes.StartApp+firefox X-Qubes-DispvmExec=qvm-run -q -a --service --dispvm=test-red -- qubes.StartApp+firefox - ''' + """ - app_entry3 = b''' + app_entry3 = b""" [Desktop Entry] Version=1.0 Type=Application @@ -90,10 +90,10 @@ def test_desktop_file_path(tmp_path): Keywords=settings;desktop Categories=Gtk;Settings;X-XFCE-SettingsDialog;X-XFCE; Exec=xfce4-appearance-settings - ''' + """ - (tmp_path / 'test1.desktop').write_bytes(app_entry1) - (tmp_path / 'test2.desktop').write_bytes(app_entry2) - (tmp_path / 'test3.desktop').write_bytes(app_entry3) + (tmp_path / "test1.desktop").write_bytes(app_entry1) + (tmp_path / "test2.desktop").write_bytes(app_entry2) + (tmp_path / "test3.desktop").write_bytes(app_entry3) return tmp_path diff --git a/qubes_menu/tests/test_app_page.py b/qubes_menu/tests/test_app_page.py index fd25524..8c22b87 100644 --- a/qubes_menu/tests/test_app_page.py +++ b/qubes_menu/tests/test_app_page.py @@ -30,43 +30,48 @@ def test_app_page_vm_state(test_desktop_file_path, test_qapp, test_builder): dispatcher = MockDispatcher(test_qapp) vm_manager = VMManager(test_qapp, dispatcher) - with mock.patch.object(DesktopFileManager, 'desktop_dirs', - [test_desktop_file_path]): + with mock.patch.object( + DesktopFileManager, "desktop_dirs", [test_desktop_file_path] + ): desktop_file_manager = DesktopFileManager(test_qapp) app_page = AppPage(vm_manager, test_builder, desktop_file_manager) # select a turned off vm - app_page.vm_list.select_row([row for row in app_page.vm_list.get_children() - if row.vm_name == 'test-red'][0]) + app_page.vm_list.select_row( + [row for row in app_page.vm_list.get_children() if row.vm_name == "test-red"][0] + ) - assert app_page.control_list.start_item.row_label.get_label() == \ - "Start qube" - assert app_page.control_list.pause_item.row_label.get_label() == \ - " " + assert app_page.control_list.start_item.row_label.get_label() == "Start qube" + assert app_page.control_list.pause_item.row_label.get_label() == " " # select a turned on vm - app_page.vm_list.select_row([row for row in app_page.vm_list.get_children() - if row.vm_name == 'sys-usb'][0]) + app_page.vm_list.select_row( + [row for row in app_page.vm_list.get_children() if row.vm_name == "sys-usb"][0] + ) - assert app_page.control_list.start_item.row_label.get_label() == \ - "Shutdown qube" - assert app_page.control_list.pause_item.row_label.get_label() == \ - "Pause qube" + assert app_page.control_list.start_item.row_label.get_label() == "Shutdown qube" + assert app_page.control_list.pause_item.row_label.get_label() == "Pause qube" def test_dispvm_parent_sorting(test_desktop_file_path, test_qapp, test_builder): # check if dispvm child is sorted after the parent - test_qapp._qubes['disp1233'] = MockQube( - name="disp1233", qapp=test_qapp, klass='DispVM', - template_for_dispvms='True', template='default-dvm', auto_cleanup=True) + test_qapp._qubes["disp1233"] = MockQube( + name="disp1233", + qapp=test_qapp, + klass="DispVM", + template_for_dispvms="True", + template="default-dvm", + auto_cleanup=True, + ) test_qapp.update_vm_calls() dispatcher = MockDispatcher(test_qapp) vm_manager = VMManager(test_qapp, dispatcher) - with mock.patch.object(DesktopFileManager, 'desktop_dirs', - [test_desktop_file_path]): + with mock.patch.object( + DesktopFileManager, "desktop_dirs", [test_desktop_file_path] + ): desktop_file_manager = DesktopFileManager(test_qapp) app_page = AppPage(vm_manager, test_builder, desktop_file_manager) @@ -75,11 +80,11 @@ def test_dispvm_parent_sorting(test_desktop_file_path, test_qapp, test_builder): for row in app_page.vm_list.get_children(): if found_dvm: - if row.vm_name == 'disp1233' and row.vm_entry.parent_vm: + if row.vm_name == "disp1233" and row.vm_entry.parent_vm: break found_dvm = False continue - if row.vm_name == 'default-dvm' and row.vm_entry._is_dispvm_template: + if row.vm_name == "default-dvm" and row.vm_entry._is_dispvm_template: found_dvm = True continue found_dvm = False @@ -92,12 +97,14 @@ def test_settings_app_page(test_desktop_file_path, test_qapp, test_builder): dispatcher = MockDispatcher(test_qapp) vm_manager = VMManager(test_qapp, dispatcher) - with mock.patch.object(DesktopFileManager, 'desktop_dirs', - [test_desktop_file_path]): + with mock.patch.object( + DesktopFileManager, "desktop_dirs", [test_desktop_file_path] + ): desktop_file_manager = DesktopFileManager(test_qapp) - settings_page = SettingsPage(test_qapp, test_builder, - desktop_file_manager, dispatcher) + settings_page = SettingsPage( + test_qapp, test_builder, desktop_file_manager, dispatcher + ) for row in settings_page.app_list.get_children(): assert not row.app_info.vm diff --git a/qubes_menu/tests/test_appmenu.py b/qubes_menu/tests/test_appmenu.py index 766582e..44548cb 100644 --- a/qubes_menu/tests/test_appmenu.py +++ b/qubes_menu/tests/test_appmenu.py @@ -18,17 +18,22 @@ # You should have received a copy of the GNU Lesser General Public License along # with this program; if not, see . from ..appmenu import AppMenu -from qubesadmin.tests.mock_app import MockQubesComplete, MockDispatcher, MockQube +from qubesadmin.tests.mock_app import ( + MockQubesComplete, + MockDispatcher, + MockQube, +) def test_app_menu_conffeatures(): qapp = MockQubesComplete() - qapp._qubes['test-vm2'] = MockQube(name="test-vm2", qapp=qapp, - features={'menu-favorites': ''}) - qapp._qubes['dom0'].features['menu-initial-page'] = 'favorites_page' - qapp._qubes['dom0'].features['menu-sort-running'] = '1' - qapp._qubes['dom0'].features['menu-position'] = '' + qapp._qubes["test-vm2"] = MockQube( + name="test-vm2", qapp=qapp, features={"menu-favorites": ""} + ) + qapp._qubes["dom0"].features["menu-initial-page"] = "favorites_page" + qapp._qubes["dom0"].features["menu-sort-running"] = "1" + qapp._qubes["dom0"].features["menu-position"] = "" qapp.update_vm_calls() dispatcher = MockDispatcher(qapp) @@ -46,12 +51,16 @@ def test_app_menu_conffeatures_default(): qapp = MockQubesComplete() # make sure the features exist, but should not be shown - qapp._qubes['test-vm2'] = MockQube( - name="test-vm2", qapp=qapp, - features={'menu-favorites': '', - 'menu-initial-page': 'fake', - 'menu-sort-running': 'fake', - 'menu-position': 'fake'}) + qapp._qubes["test-vm2"] = MockQube( + name="test-vm2", + qapp=qapp, + features={ + "menu-favorites": "", + "menu-initial-page": "fake", + "menu-sort-running": "fake", + "menu-position": "fake", + }, + ) qapp.update_vm_calls() dispatcher = MockDispatcher(qapp) @@ -68,11 +77,12 @@ def test_app_menu_conffeatures_default(): def test_appmenu_options(): qapp = MockQubesComplete() - qapp._qubes['test-vm2'] = MockQube(name="test-vm2", qapp=qapp, - features={'menu-favorites': ''}) - qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page' - qapp._qubes['dom0'].features['menu-sort-running'] = '1' - qapp._qubes['dom0'].features['menu-position'] = 'top-left' + qapp._qubes["test-vm2"] = MockQube( + name="test-vm2", qapp=qapp, features={"menu-favorites": ""} + ) + qapp._qubes["dom0"].features["menu-initial-page"] = "app_page" + qapp._qubes["dom0"].features["menu-sort-running"] = "1" + qapp._qubes["dom0"].features["menu-position"] = "top-left" qapp.update_vm_calls() dispatcher = MockDispatcher(qapp) @@ -82,10 +92,7 @@ def test_appmenu_options(): assert app_menu.initial_page == "app_page" assert not app_menu.keep_visible - options = { - "keep-visible": True, - "page": "2" - } + options = {"keep-visible": True, "page": "2"} app_menu.parse_options(options) @@ -93,14 +100,16 @@ def test_appmenu_options(): assert app_menu.keep_visible assert app_menu.appmenu_position == "top-left" + def test_appmenu_positioning(): qapp = MockQubesComplete() - qapp._qubes['test-vm2'] = MockQube(name="test-vm2", qapp=qapp, - features={'menu-favorites': ''}) - qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page' - qapp._qubes['dom0'].features['menu-sort-running'] = '1' - qapp._qubes['dom0'].features['menu-position'] = '' + qapp._qubes["test-vm2"] = MockQube( + name="test-vm2", qapp=qapp, features={"menu-favorites": ""} + ) + qapp._qubes["dom0"].features["menu-initial-page"] = "app_page" + qapp._qubes["dom0"].features["menu-sort-running"] = "1" + qapp._qubes["dom0"].features["menu-position"] = "" qapp.update_vm_calls() dispatcher = MockDispatcher(qapp) @@ -117,18 +126,23 @@ def test_appmenu_positioning(): assert app_menu.main_window.get_position() == (0, 0) app_menu.appmenu_position = "top-right" app_menu.reposition() - assert app_menu.main_window.get_position() == ( \ - app_menu.main_window.get_screen().get_width() - \ - app_menu.main_window.get_size().width, 0) + assert app_menu.main_window.get_position() == ( + app_menu.main_window.get_screen().get_width() + - app_menu.main_window.get_size().width, + 0, + ) app_menu.appmenu_position = "bottom-left" app_menu.reposition() - assert app_menu.main_window.get_position() == (0, \ - app_menu.main_window.get_screen().get_height() - \ - app_menu.main_window.get_size().height) + assert app_menu.main_window.get_position() == ( + 0, + app_menu.main_window.get_screen().get_height() + - app_menu.main_window.get_size().height, + ) app_menu.appmenu_position = "bottom-right" app_menu.reposition() - assert app_menu.main_window.get_position() == ( \ - app_menu.main_window.get_screen().get_width() - \ - app_menu.main_window.get_size().width, \ - app_menu.main_window.get_screen().get_height() - \ - app_menu.main_window.get_size().height) + assert app_menu.main_window.get_position() == ( + app_menu.main_window.get_screen().get_width() + - app_menu.main_window.get_size().width, + app_menu.main_window.get_screen().get_height() + - app_menu.main_window.get_size().height, + ) diff --git a/qubes_menu/tests/test_desktop_file_manager.py b/qubes_menu/tests/test_desktop_file_manager.py index 3187cc4..e7fd07b 100644 --- a/qubes_menu/tests/test_desktop_file_manager.py +++ b/qubes_menu/tests/test_desktop_file_manager.py @@ -26,7 +26,7 @@ from unittest.mock import Mock import asyncio -correct_bytes = b''' +correct_bytes = b""" [Desktop Entry] Version=1.0 Type=Application @@ -39,9 +39,9 @@ Categories=System;TerminalEmulator;X-Qubes-VM; Exec=qvm-run -q -a --service -- test-vm qubes.StartApp+xterm X-Qubes-DispvmExec=qvm-run -q -a --service --dispvm=test-vm -- qubes.StartApp+xterm -''' +""" -correct_bytes_2 = b''' +correct_bytes_2 = b""" [Desktop Entry] Version=1.0 Type=Application @@ -54,9 +54,9 @@ Categories=System;TerminalEmulator;X-Qubes-VM; Exec=qvm-run -q -a --service -- template qubes.StartApp+xterm X-Qubes-DispvmExec=qvm-run -q -a --service --dispvm=template -- qubes.StartApp+xterm -''' +""" -correct_local_qubes = b''' +correct_local_qubes = b""" [Desktop Entry] Type=Application Exec=qubes-backup @@ -66,9 +66,9 @@ GenericName=Backup Qubes StartupNotify=false Categories=Settings;X-XFCE-SettingsDialog -''' +""" -correct_local_non_qubes = b''' +correct_local_non_qubes = b""" [Desktop Entry] Version=1.0 Name=Power Manager @@ -84,9 +84,9 @@ X-XfcePluggable=true X-XfceHelpComponent=xfce4-power-manager X-XfceHelpPage=start -''' +""" -correct_other = b''' +correct_other = b""" [Desktop Entry] Version=1.0 Name=Pinta @@ -103,11 +103,11 @@ Categories=Graphics;2DGraphics;RasterGraphics;GTK; Keywords=draw;drawing;paint;painting;graphics;raster;2d; MimeType=image/bmp;image/gif;image/jpeg;image/jpg;image/pjpeg;image/png;image/svg+xml;image/tiff;image/x-bmp;image/x-gray;image/x-icb;image/x-ico;image/x-png;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-xbitmap;image/x-xpixmap;image/x-pcx;image/x-targa;image/x-tga;image/openraster; -''' +""" def test_appinfo_correct_file(tmp_path, test_qapp): - file_path = tmp_path / 'test.desktop' + file_path = tmp_path / "test.desktop" file_path.write_bytes(correct_bytes) desktop_entry = DesktopEntry(file_path) @@ -115,27 +115,38 @@ def test_appinfo_correct_file(tmp_path, test_qapp): app_info = ApplicationInfo(test_qapp, file_path) app_info.load_data(desktop_entry) - assert app_info.app_name == 'XTerm' - assert str(app_info.vm) == str(TestVM('test-vm')) - assert app_info.app_icon == '/tmp/test.png' - assert app_info.vm_icon == 'appvm-green' + assert app_info.app_name == "XTerm" + assert str(app_info.vm) == str(TestVM("test-vm")) + assert app_info.app_icon == "/tmp/test.png" + assert app_info.vm_icon == "appvm-green" - assert app_info.entry_name == 'XTerm'\ - or app_info.entry_name == 'test.desktop' + assert app_info.entry_name == "XTerm" or app_info.entry_name == "test.desktop" assert not app_info.disposable assert app_info.is_qubes_specific() - assert app_info.get_command_for_vm(TestVM('test-vm')) ==\ - ['qvm-run', '-q', '-a', '--service', '--', 'test-vm', - 'qubes.StartApp+xterm'] - - assert app_info.get_command_for_vm(TestVM('other-vm')) ==\ - ['qvm-run', '-q', '-a', '--service', '--', 'other-vm', - 'qubes.StartApp+xterm'] + assert app_info.get_command_for_vm(TestVM("test-vm")) == [ + "qvm-run", + "-q", + "-a", + "--service", + "--", + "test-vm", + "qubes.StartApp+xterm", + ] + + assert app_info.get_command_for_vm(TestVM("other-vm")) == [ + "qvm-run", + "-q", + "-a", + "--service", + "--", + "other-vm", + "qubes.StartApp+xterm", + ] def test_file_dvmtemplate(tmp_path, test_qapp): - correct_dvm_template = b''' + correct_dvm_template = b""" [Desktop Entry] Version=1.0 Type=Application @@ -148,9 +159,9 @@ def test_file_dvmtemplate(tmp_path, test_qapp): Categories=Network;WebBrowser;X-Qubes-VM; X-Qubes-NonDispvmExec=qvm-run -q -a --service -- default-dvm qubes.StartApp+firefox Exec=qvm-run -q -a --service --dispvm=default-dvm -- qubes.StartApp+firefox - ''' + """ - file_path = tmp_path / 'test.desktop' + file_path = tmp_path / "test.desktop" file_path.write_bytes(correct_dvm_template) desktop_entry = DesktopEntry(file_path) @@ -158,20 +169,26 @@ def test_file_dvmtemplate(tmp_path, test_qapp): app_info = ApplicationInfo(test_qapp, file_path) app_info.load_data(desktop_entry) - assert app_info.app_name == 'Firefox' - assert str(app_info.vm) == str(TestVM('default-dvm')) - assert app_info.app_icon == '/test/firefox.png' - assert app_info.vm_icon == 'templatevm-green' + assert app_info.app_name == "Firefox" + assert str(app_info.vm) == str(TestVM("default-dvm")) + assert app_info.app_icon == "/test/firefox.png" + assert app_info.vm_icon == "templatevm-green" assert app_info.disposable assert app_info.is_qubes_specific() - assert app_info.get_command_for_vm('default-dvm') == [ - 'qvm-run', '-q', '-a', '--service', '--dispvm=default-dvm', '--', - 'qubes.StartApp+firefox'] + assert app_info.get_command_for_vm("default-dvm") == [ + "qvm-run", + "-q", + "-a", + "--service", + "--dispvm=default-dvm", + "--", + "qubes.StartApp+firefox", + ] def test_appinfo_local(tmp_path, test_qapp): - file_path_qubes = tmp_path / 'test.desktop' - file_path_non_qubes = tmp_path / 'test2.desktop' + file_path_qubes = tmp_path / "test.desktop" + file_path_non_qubes = tmp_path / "test2.desktop" file_path_qubes.write_bytes(correct_local_qubes) file_path_non_qubes.write_bytes(correct_local_non_qubes) @@ -183,32 +200,34 @@ def test_appinfo_local(tmp_path, test_qapp): app_info_qubes.load_data(desktop_entry_qubes) app_info_non_qubes.load_data(desktop_entry_non_qubes) - assert app_info_qubes.app_name == 'Backup Qubes' - assert app_info_non_qubes.app_name == 'Power Manager' + assert app_info_qubes.app_name == "Backup Qubes" + assert app_info_non_qubes.app_name == "Power Manager" assert app_info_qubes.vm is None assert app_info_non_qubes.vm is None assert app_info_qubes.vm_icon is None assert app_info_non_qubes.vm_icon is None - assert app_info_qubes.app_icon == 'qubes-manager' - assert app_info_non_qubes.app_icon == 'xfce4-power-manager-settings' + assert app_info_qubes.app_icon == "qubes-manager" + assert app_info_non_qubes.app_icon == "xfce4-power-manager-settings" assert not app_info_non_qubes.disposable assert not app_info_qubes.disposable assert not app_info_qubes.is_qubes_specific() assert not app_info_non_qubes.is_qubes_specific() - assert app_info_qubes.get_command_for_vm(None) == ['qubes-backup'] - assert app_info_non_qubes.get_command_for_vm(None) == \ - ['xfce4-power-manager-settings'] + assert app_info_qubes.get_command_for_vm(None) == ["qubes-backup"] + assert app_info_non_qubes.get_command_for_vm(None) == [ + "xfce4-power-manager-settings" + ] - assert app_info_qubes.get_command_for_vm('dom0') == ['qubes-backup'] - assert app_info_non_qubes.get_command_for_vm('dom0') == \ - ['xfce4-power-manager-settings'] + assert app_info_qubes.get_command_for_vm("dom0") == ["qubes-backup"] + assert app_info_non_qubes.get_command_for_vm("dom0") == [ + "xfce4-power-manager-settings" + ] def test_file_qubes_virtual(tmp_path, test_qapp): - qubes_virtual = b''' + qubes_virtual = b""" [Desktop Entry] Version=1.0 Type=Application @@ -219,9 +238,9 @@ def test_file_qubes_virtual(tmp_path, test_qapp): Name=fedora-32: Qube Settings GenericName=fedora-32: Qube Settings StartupNotify=false -Categories=System;X-Qubes-VM;''' +Categories=System;X-Qubes-VM;""" - file_path = tmp_path / 'test.desktop' + file_path = tmp_path / "test.desktop" file_path.write_bytes(qubes_virtual) desktop_entry = DesktopEntry(file_path) @@ -233,7 +252,7 @@ def test_file_qubes_virtual(tmp_path, test_qapp): def test_space_exec(tmp_path, test_qapp): - qubes_virtual = b''' + qubes_virtual = b""" [Desktop Entry] Version=1.0 Type=Application @@ -243,9 +262,9 @@ def test_space_exec(tmp_path, test_qapp): Name=Generic Name GenericName=Generic Name StartupNotify=false -Categories=System;X-Qubes-VM;''' +Categories=System;X-Qubes-VM;""" - file_path = tmp_path / 'test.desktop' + file_path = tmp_path / "test.desktop" file_path.write_bytes(qubes_virtual) desktop_entry = DesktopEntry(file_path) @@ -253,11 +272,11 @@ def test_space_exec(tmp_path, test_qapp): app_info = ApplicationInfo(test_qapp, file_path) app_info.load_data(desktop_entry) - assert app_info.get_command_for_vm(None) == ['command', 'a vm'] + assert app_info.get_command_for_vm(None) == ["command", "a vm"] def test_special_characters_exec(tmp_path, test_qapp): - qubes_virtual = b''' + qubes_virtual = b""" [Desktop Entry] Version=1.0 Type=Application @@ -267,9 +286,9 @@ def test_special_characters_exec(tmp_path, test_qapp): Name=Generic Name GenericName=Generic Name StartupNotify=false -Categories=System;X-Qubes-VM;''' +Categories=System;X-Qubes-VM;""" - file_path = tmp_path / 'test.desktop' + file_path = tmp_path / "test.desktop" file_path.write_bytes(qubes_virtual) desktop_entry = DesktopEntry(file_path) @@ -277,14 +296,14 @@ def test_special_characters_exec(tmp_path, test_qapp): app_info = ApplicationInfo(test_qapp, file_path) app_info.load_data(desktop_entry) - assert app_info.get_command_for_vm(None) == ['command', "a\\b\\c"] + assert app_info.get_command_for_vm(None) == ["command", "a\\b\\c"] @pytest.mark.asyncio async def test_file_manager(tmp_path, test_qapp): DesktopFileManager.desktop_dirs = [tmp_path] - (tmp_path / 'test.desktop').write_bytes(correct_bytes) - (tmp_path / 'wrong.desktop').write_bytes(b'faulty') + (tmp_path / "test.desktop").write_bytes(correct_bytes) + (tmp_path / "wrong.desktop").write_bytes(b"faulty") dfm = DesktopFileManager(test_qapp) assert len(dfm.app_entries) == 1 @@ -300,16 +319,16 @@ def add_entry(en): assert len(entry_list) == 1 - (tmp_path / 'test2.desktop').write_bytes(correct_bytes_2) + (tmp_path / "test2.desktop").write_bytes(correct_bytes_2) # process file events await asyncio.sleep(1) assert len(entry_list) == 2 - (tmp_path / 'test.desktop').write_bytes(correct_bytes) - (tmp_path / 'test2.desktop').write_bytes(correct_bytes_2) - (tmp_path / 'wrong.desktop').write_bytes(b'faulty') + (tmp_path / "test.desktop").write_bytes(correct_bytes) + (tmp_path / "test2.desktop").write_bytes(correct_bytes_2) + (tmp_path / "wrong.desktop").write_bytes(b"faulty") # process file events await asyncio.sleep(1) @@ -321,7 +340,7 @@ def add_entry(en): def test_filter_system(tmp_path, test_qapp): - file_path_non_qubes = tmp_path / 'correct_local_non.desktop' + file_path_non_qubes = tmp_path / "correct_local_non.desktop" file_path_non_qubes.write_bytes(correct_local_non_qubes) desktop_entry_non_qubes = DesktopEntry(file_path_non_qubes) app_info_non_qubes = ApplicationInfo(test_qapp, file_path_non_qubes) @@ -329,7 +348,7 @@ def test_filter_system(tmp_path, test_qapp): row_non_qubes = Mock() row_non_qubes.app_info = app_info_non_qubes - file_path_qubes = tmp_path / 'correct_local_qubes.desktop' + file_path_qubes = tmp_path / "correct_local_qubes.desktop" file_path_qubes.write_bytes(correct_local_qubes) desktop_entry_qubes = DesktopEntry(file_path_qubes) app_info_qubes = ApplicationInfo(test_qapp, file_path_qubes) @@ -337,7 +356,7 @@ def test_filter_system(tmp_path, test_qapp): row_qubes = Mock() row_qubes.app_info = app_info_qubes - file_path_other = tmp_path / 'correct_other.desktop' + file_path_other = tmp_path / "correct_other.desktop" file_path_other.write_bytes(correct_other) desktop_entry_other = DesktopEntry(file_path_other) app_info_other = ApplicationInfo(test_qapp, file_path_other) diff --git a/qubes_menu/tests/test_favorites.py b/qubes_menu/tests/test_favorites.py index c4d0fe6..ea16d1f 100644 --- a/qubes_menu/tests/test_favorites.py +++ b/qubes_menu/tests/test_favorites.py @@ -25,10 +25,10 @@ def test_add_to_favorites(tmp_path, test_qapp): app_info = ApplicationInfo(test_qapp, tmp_path) - vm = test_qapp.domains['test-vm'] + vm = test_qapp.domains["test-vm"] app_info.vm = vm - app_info.app_name = 'Test App' - app_info.entry_name = 'org.test.app' + app_info.app_name = "Test App" + app_info.entry_name = "org.test.app" app_info.app_icon = None app_info.vm_icon = None vm.features = {} # overwrite smart features object with a dumb dict @@ -36,48 +36,48 @@ def test_add_to_favorites(tmp_path, test_qapp): base_entry = BaseAppEntry(app_info) base_entry.menu._add_to_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" base_entry.menu._add_to_favorites() base_entry.menu._add_to_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" mock_manager = Mock() fav_entry = FavoritesAppEntry(app_info, mock_manager) fav_entry._remove_from_favorites() - assert vm.features.get('menu-favorites') == '' + assert vm.features.get("menu-favorites") == "" base_entry.menu._add_to_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" second_app_info = ApplicationInfo(test_qapp, tmp_path) second_app_info.vm = vm - second_app_info.app_name = 'Second App' - second_app_info.entry_name = 'org.second.app' + second_app_info.app_name = "Second App" + second_app_info.entry_name = "org.second.app" second_app_info.app_icon = None second_app_info.vm_icon = None second_base_entry = BaseAppEntry(second_app_info) second_fav_entry = FavoritesAppEntry(second_app_info, mock_manager) - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" second_base_entry.menu._add_to_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app org.second.app' + assert vm.features.get("menu-favorites") == "org.test.app org.second.app" second_fav_entry._remove_from_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" second_base_entry.menu._add_to_favorites() fav_entry._remove_from_favorites() - assert vm.features.get('menu-favorites') == 'org.second.app' + assert vm.features.get("menu-favorites") == "org.second.app" def test_correct_menu_states(tmp_path, test_qapp): app_info = ApplicationInfo(test_qapp, tmp_path) - vm = test_qapp.domains['test-vm'] + vm = test_qapp.domains["test-vm"] app_info.vm = vm - app_info.app_name = 'Test App' - app_info.entry_name = 'org.test.app' + app_info.app_name = "Test App" + app_info.entry_name = "org.test.app" app_info.app_icon = None app_info.vm_icon = None vm.features = {} # overwrite smart features object with a dumb dict @@ -112,47 +112,47 @@ def test_correct_menu_states(tmp_path, test_qapp): def test_add_to_favorites_dom0(tmp_path, test_qapp): app_info = ApplicationInfo(test_qapp, tmp_path) - app_info.app_name = 'Test App' - app_info.entry_name = 'org.test.app' + app_info.app_name = "Test App" + app_info.entry_name = "org.test.app" app_info.app_icon = None app_info.vm_icon = None - vm = test_qapp.domains['dom0'] + vm = test_qapp.domains["dom0"] vm.features = {} # overwrite smart features object with a dumb dict base_entry = BaseAppEntry(app_info) base_entry.menu._add_to_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" base_entry.menu._add_to_favorites() base_entry.menu._add_to_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" mock_manager = Mock() fav_entry = FavoritesAppEntry(app_info, mock_manager) fav_entry._remove_from_favorites() - assert vm.features.get('menu-favorites') == '' + assert vm.features.get("menu-favorites") == "" base_entry.menu._add_to_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" second_app_info = ApplicationInfo(test_qapp, tmp_path) second_app_info.vm = vm - second_app_info.app_name = 'Second App' - second_app_info.entry_name = 'org.second.app' + second_app_info.app_name = "Second App" + second_app_info.entry_name = "org.second.app" second_app_info.app_icon = None second_app_info.vm_icon = None second_base_entry = BaseAppEntry(second_app_info) second_fav_entry = FavoritesAppEntry(second_app_info, mock_manager) - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" second_base_entry.menu._add_to_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app org.second.app' + assert vm.features.get("menu-favorites") == "org.test.app org.second.app" second_fav_entry._remove_from_favorites() - assert vm.features.get('menu-favorites') == 'org.test.app' + assert vm.features.get("menu-favorites") == "org.test.app" second_base_entry.menu._add_to_favorites() fav_entry._remove_from_favorites() - assert vm.features.get('menu-favorites') == 'org.second.app' + assert vm.features.get("menu-favorites") == "org.second.app" diff --git a/qubes_menu/tests/test_search.py b/qubes_menu/tests/test_search.py index 308d61f..6c88c12 100644 --- a/qubes_menu/tests/test_search.py +++ b/qubes_menu/tests/test_search.py @@ -29,8 +29,9 @@ def test_search(test_desktop_file_path, test_qapp, test_builder): dispatcher = MockDispatcher(test_qapp) vm_manager = VMManager(test_qapp, dispatcher) - with mock.patch.object(DesktopFileManager, 'desktop_dirs', - [test_desktop_file_path]): + with mock.patch.object( + DesktopFileManager, "desktop_dirs", [test_desktop_file_path] + ): desktop_file_manager = DesktopFileManager(test_qapp) search_page = SearchPage(vm_manager, test_builder, desktop_file_manager) @@ -38,55 +39,99 @@ def test_search(test_desktop_file_path, test_qapp, test_builder): assert search_page.search_entry.get_sensitive() # nothing should be visible - assert len([row for row in search_page.app_list.get_children() - if search_page._is_app_fitting(row)]) == 0 + assert ( + len( + [ + row + for row in search_page.app_list.get_children() + if search_page._is_app_fitting(row) + ] + ) + == 0 + ) # try to find firefox - search_page.search_entry.set_text('firefox') + search_page.search_entry.set_text("firefox") - found_entries = [row for row in search_page.app_list.get_children() - if search_page._is_app_fitting(row)] + found_entries = [ + row + for row in search_page.app_list.get_children() + if search_page._is_app_fitting(row) + ] assert len(found_entries) == 1 - assert found_entries[0].app_info.app_name == 'Firefox' + assert found_entries[0].app_info.app_name == "Firefox" - search_page.search_entry.set_text('') + search_page.search_entry.set_text("") # nothing should be visible - assert len([row for row in search_page.app_list.get_children() - if search_page._is_app_fitting(row)]) == 0 + assert ( + len( + [ + row + for row in search_page.app_list.get_children() + if search_page._is_app_fitting(row) + ] + ) + == 0 + ) # check for no problems with caps - search_page.search_entry.set_text('xTeRm') + search_page.search_entry.set_text("xTeRm") - found_entries = [row for row in search_page.app_list.get_children() - if search_page._is_app_fitting(row)] + found_entries = [ + row + for row in search_page.app_list.get_children() + if search_page._is_app_fitting(row) + ] assert len(found_entries) == 1 - assert found_entries[0].app_info.app_name == 'XTerm' + assert found_entries[0].app_info.app_name == "XTerm" - search_page.search_entry.set_text('') + search_page.search_entry.set_text("") # nothing should be visible - assert len([row for row in search_page.app_list.get_children() - if search_page._is_app_fitting(row)]) == 0 + assert ( + len( + [ + row + for row in search_page.app_list.get_children() + if search_page._is_app_fitting(row) + ] + ) + == 0 + ) # try to use keywords in searching - search_page.search_entry.set_text('dragons') + search_page.search_entry.set_text("dragons") - found_entries = [row for row in search_page.app_list.get_children() - if search_page._is_app_fitting(row)] + found_entries = [ + row + for row in search_page.app_list.get_children() + if search_page._is_app_fitting(row) + ] assert len(found_entries) == 1 - assert found_entries[0].app_info.app_name == 'Firefox' + assert found_entries[0].app_info.app_name == "Firefox" - search_page.search_entry.set_text('') + search_page.search_entry.set_text("") # nothing should be visible - assert len([row for row in search_page.app_list.get_children() - if search_page._is_app_fitting(row)]) == 0 + assert ( + len( + [ + row + for row in search_page.app_list.get_children() + if search_page._is_app_fitting(row) + ] + ) + == 0 + ) # find a dom0 app - search_page.search_entry.set_text('dom0') + search_page.search_entry.set_text("dom0") - found_entries = [row for row in search_page.app_list.get_children() - if search_page._is_app_fitting(row)] + found_entries = [ + row + for row in search_page.app_list.get_children() + if search_page._is_app_fitting(row) + ] assert len(found_entries) == 1 - assert found_entries[0].app_info.app_name == 'Xfce Appearance Settings' + assert found_entries[0].app_info.app_name == "Xfce Appearance Settings" diff --git a/qubes_menu/tests/test_utils.py b/qubes_menu/tests/test_utils.py index e44236f..f9c1f7c 100644 --- a/qubes_menu/tests/test_utils.py +++ b/qubes_menu/tests/test_utils.py @@ -20,13 +20,14 @@ from ..utils import highlight_words import gi -gi.require_version('Gtk', '3.0') + +gi.require_version("Gtk", "3.0") from gi.repository import Gtk def test_highlight_words(): # make a mock highlight tag - highlight_tag = '' + highlight_tag = "" # create some labels label_1 = Gtk.Label("Come forth my lovely languorous Sphinx") @@ -37,33 +38,26 @@ def test_highlight_words(): highlight_words(labels, ["sphinx"], highlight_tag) - assert label_1.get_label() == \ - "Come forth my lovely languorous Sphinx" - assert label_2.get_label() == \ - "sphinx of black quartz, judge my vow" - assert label_3.get_label() == \ - "A shape with lion body and the head of a man" + assert label_1.get_label() == "Come forth my lovely languorous Sphinx" + assert label_2.get_label() == "sphinx of black quartz, judge my vow" + assert label_3.get_label() == "A shape with lion body and the head of a man" # further highlighting should not break things and should remove # old highlights highlight_words(labels, ["black"], highlight_tag) - assert label_1.get_label() == \ - "Come forth my lovely languorous Sphinx" - assert label_2.get_label() == \ - "sphinx of black quartz, judge my vow" - assert label_3.get_label() == \ - "A shape with lion body and the head of a man" + assert label_1.get_label() == "Come forth my lovely languorous Sphinx" + assert label_2.get_label() == "sphinx of black quartz, judge my vow" + assert label_3.get_label() == "A shape with lion body and the head of a man" # multiple words should work, even when they overlap highlight_words(labels, ["on", "lion", "languorous"], highlight_tag) - assert label_1.get_label() == \ - "Come forth my lovely languorous Sphinx" - assert label_2.get_label() == \ - "sphinx of black quartz, judge my vow" - assert label_3.get_label() == \ - "A shape with lion body and the head of a man" - + assert label_1.get_label() == "Come forth my lovely languorous Sphinx" + assert label_2.get_label() == "sphinx of black quartz, judge my vow" + assert ( + label_3.get_label() + == "A shape with lion body and the head of a man" + ) diff --git a/qubes_menu/tests/test_vmmanager.py b/qubes_menu/tests/test_vmmanager.py index 76dc8f6..d73e9ff 100644 --- a/qubes_menu/tests/test_vmmanager.py +++ b/qubes_menu/tests/test_vmmanager.py @@ -57,12 +57,8 @@ def test_vm_manager(test_qapp): ) assert entry_template.has_network - test_qapp._qubes[vm_name].properties["label"] = Property( - "red", "label", False - ) - test_qapp._qubes[vm_name].properties["icon"] = Property( - "appvm-red", "str", False - ) + test_qapp._qubes[vm_name].properties["label"] = Property("red", "label", False) + test_qapp._qubes[vm_name].properties["icon"] = Property("appvm-red", "str", False) test_qapp._qubes[vm_name].update_calls() vm_manager._update_domain_property( vm_name, diff --git a/qubes_menu/utils.py b/qubes_menu/utils.py index 885e0c0..d0e42d0 100644 --- a/qubes_menu/utils.py +++ b/qubes_menu/utils.py @@ -26,13 +26,15 @@ import qubesadmin.vm -gi.require_version('Gtk', '3.0') +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, GdkPixbuf, GLib -def load_icon(icon_name, - size: Optional[Gtk.IconSize] = Gtk.IconSize.LARGE_TOOLBAR, - pixel_size: Optional[int] = None): +def load_icon( + icon_name, + size: Optional[Gtk.IconSize] = Gtk.IconSize.LARGE_TOOLBAR, + pixel_size: Optional[int] = None, +): """Load icon from provided name, if available. If not, attempt to treat provided name as a path. If icon not found in any of the above ways, load a blank icon of specified size. @@ -49,30 +51,32 @@ def load_icon(icon_name, try: # icon name is a path image: GdkPixbuf.Pixbuf = Gtk.IconTheme.get_default().load_icon( - icon_name, width, Gtk.IconLookupFlags.FORCE_SIZE) + icon_name, width, Gtk.IconLookupFlags.FORCE_SIZE + ) return image except (TypeError, GLib.Error): # icon not found in any way pixbuf: GdkPixbuf.Pixbuf = GdkPixbuf.Pixbuf.new( - GdkPixbuf.Colorspace.RGB, True, 8, width, height) + GdkPixbuf.Colorspace.RGB, True, 8, width, height + ) pixbuf.fill(0x000) return pixbuf + def show_error(title, text): """ Helper function to display error messages. """ - dialog = Gtk.MessageDialog( - None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK) + dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK) dialog.set_title(title) dialog.set_markup(GLib.markup_escape_text(text)) dialog.connect("response", lambda *x: dialog.destroy()) dialog.show() + def parse_search(search_text: str) -> List[str]: """Parse search text into separate words""" - search_words = search_text.lower().replace( - '-', ' ').replace('_', ' ').split(' ') + search_words = search_text.lower().replace("-", " ").replace("_", " ").split(" ") return [w for w in search_words if w] @@ -92,8 +96,11 @@ def text_search(search_word: str, text_words: List[str]): return 0 -def highlight_words(labels: List[Gtk.Label], search_words: List[str], - hl_tag: Optional[str] = None) -> None: +def highlight_words( + labels: List[Gtk.Label], + search_words: List[str], + hl_tag: Optional[str] = None, +) -> None: """Highlight provided search_words in the provided labels.""" if not labels: return @@ -123,13 +130,14 @@ def highlight_words(labels: List[Gtk.Label], search_words: List[str], if not found_intervals: continue - found_intervals.sort(key= lambda x: x[0]) + found_intervals.sort(key=lambda x: x[0]) result_intervals = [found_intervals[0]] for interval in found_intervals[1:]: if interval[0] <= result_intervals[-1][1]: - result_intervals[-1] = \ - (result_intervals[-1][0], - max(result_intervals[-1][1], interval[1])) + result_intervals[-1] = ( + result_intervals[-1][0], + max(result_intervals[-1][1], interval[1]), + ) else: result_intervals.append(interval) @@ -139,7 +147,7 @@ def highlight_words(labels: List[Gtk.Label], search_words: List[str], markup_list.append(GLib.markup_escape_text(text[last_start:start])) markup_list.append(hl_tag) markup_list.append(GLib.markup_escape_text(text[start:end])) - markup_list.append('') + markup_list.append("") last_start = end markup_list.append(GLib.markup_escape_text(text[last_start:])) @@ -166,24 +174,23 @@ def add_to_feature(vm: qubesadmin.vm.QubesVM, feature_name: str, text: str): """ current_feature = vm.features.get(feature_name) if current_feature: - feature_list = current_feature.split(' ') + feature_list = current_feature.split(" ") else: feature_list = [] if text in feature_list: return feature_list.append(text) - vm.features[feature_name] = ' '.join(feature_list) + vm.features[feature_name] = " ".join(feature_list) -def remove_from_feature(vm: qubesadmin.vm.QubesVM, - feature_name: str, text: str): +def remove_from_feature(vm: qubesadmin.vm.QubesVM, feature_name: str, text: str): """ Remove a given string to a feature containing a list of space-separated strings.Can raise ValueError if ext was not found in the feature. """ - current_feature = vm.features.get(feature_name, '').split(' ') + current_feature = vm.features.get(feature_name, "").split(" ") current_feature.remove(text) - vm.features[feature_name] = ' '.join(current_feature) + vm.features[feature_name] = " ".join(current_feature) diff --git a/qubes_menu/vm_manager.py b/qubes_menu/vm_manager.py index 98953bd..7cb25d2 100644 --- a/qubes_menu/vm_manager.py +++ b/qubes_menu/vm_manager.py @@ -57,12 +57,8 @@ def __init__(self, vm: QubesVM): except qubesadmin.exc.QubesDaemonAccessError: self._internal = False self._servicevm = bool(self.vm.features.get("servicevm", False)) - self._is_dispvm_template = getattr( - self.vm, "template_for_dispvms", False - ) - self._has_network = ( - self.vm.is_networked() if vm.klass != "AdminVM" else False - ) + self._is_dispvm_template = getattr(self.vm, "template_for_dispvms", False) + self._has_network = self.vm.is_networked() if vm.klass != "AdminVM" else False self._vm_icon_name = getattr( self.vm, "icon", getattr(self.vm.label, "icon", None) ) @@ -179,20 +175,14 @@ def show_in_apps(self): def _escaped_name(self) -> str: """Name escaped according to rules from desktop-linux-common package""" - return ( - self.vm_name.replace("_", "_u") - .replace("-", "_d") - .replace(".", "_p") - ) + return self.vm_name.replace("_", "_u").replace("-", "_d").replace(".", "_p") @property def settings_desktop_file_name(self) -> str: """ Name of relevant .desktop vm settings file. """ - return ( - "org.qubes-os.qubes-vm-settings._" + self._escaped_name + ".desktop" - ) + return "org.qubes-os.qubes-vm-settings._" + self._escaped_name + ".desktop" @property def start_vm_desktop_file_name(self) -> str: @@ -275,9 +265,7 @@ def _update_domain_state(self, vm_name, event, **_kwargs): state = constants.STATE_DICTIONARY[event] vm_entry.power_state = state - def _update_domain_property( - self, vm_name, event, newvalue, *_args, **_kwargs - ): + def _update_domain_property(self, vm_name, event, newvalue, *_args, **_kwargs): vm_entry = self.load_vm_from_name(vm_name) if not vm_entry: @@ -298,9 +286,7 @@ def _update_domain_property( # it will disable any future event handling pass - def _update_domain_feature( - self, vm, _event, feature=None, value=None, **_kwargs - ): + def _update_domain_feature(self, vm, _event, feature=None, value=None, **_kwargs): vm_entry = self.load_vm_from_name(vm) if not vm_entry: @@ -339,36 +325,20 @@ def _update_domain_feature( def register_events(self): """Register handlers for all relevant VM events.""" - self.dispatcher.add_handler( - "domain-pre-start", self._update_domain_state - ) + self.dispatcher.add_handler("domain-pre-start", self._update_domain_state) self.dispatcher.add_handler("domain-start", self._update_domain_state) - self.dispatcher.add_handler( - "domain-start-failed", self._update_domain_state - ) + self.dispatcher.add_handler("domain-start-failed", self._update_domain_state) self.dispatcher.add_handler("domain-paused", self._update_domain_state) - self.dispatcher.add_handler( - "domain-unpaused", self._update_domain_state - ) - self.dispatcher.add_handler( - "domain-shutdown", self._update_domain_state - ) - self.dispatcher.add_handler( - "domain-pre-shutdown", self._update_domain_state - ) - self.dispatcher.add_handler( - "domain-shutdown-failed", self._update_domain_state - ) + self.dispatcher.add_handler("domain-unpaused", self._update_domain_state) + self.dispatcher.add_handler("domain-shutdown", self._update_domain_state) + self.dispatcher.add_handler("domain-pre-shutdown", self._update_domain_state) + self.dispatcher.add_handler("domain-shutdown-failed", self._update_domain_state) self.dispatcher.add_handler("domain-add", self._add_domain) self.dispatcher.add_handler("domain-delete", self._remove_domain) - self.dispatcher.add_handler( - "property-set:netvm", self._update_domain_property - ) - self.dispatcher.add_handler( - "property-set:label", self._update_domain_property - ) + self.dispatcher.add_handler("property-set:netvm", self._update_domain_property) + self.dispatcher.add_handler("property-set:label", self._update_domain_property) self.dispatcher.add_handler( "property-set:template_for_dispvms", self._update_domain_property ) diff --git a/qubes_menu_settings/menu_settings.py b/qubes_menu_settings/menu_settings.py index cfd07bf..fee0aeb 100644 --- a/qubes_menu_settings/menu_settings.py +++ b/qubes_menu_settings/menu_settings.py @@ -29,30 +29,34 @@ from qubes_config.widgets.gtk_widgets import ImageListModeler -gi.require_version('Gtk', '3.0') +gi.require_version("Gtk", "3.0") from gi.repository import Gtk, Gdk -from qubes_menu.constants import INITIAL_PAGE_FEATURE, SORT_RUNNING_FEATURE, \ - POSITION_FEATURE +from qubes_menu.constants import ( + INITIAL_PAGE_FEATURE, + SORT_RUNNING_FEATURE, + POSITION_FEATURE, +) MENU_PAGES = { "search_page": "Search", "app_page": "Applications", - "favorites_page": "Favorites" + "favorites_page": "Favorites", } MENU_PAGES_DICT = { "Search": {"icon": "qappmenu-search", "object": "search_page"}, "Applications": {"icon": "qappmenu-qube", "object": "app_page"}, - "Favorites": {"icon": "qappmenu-favorites", "object": "favorites_page"}} + "Favorites": {"icon": "qappmenu-favorites", "object": "favorites_page"}, +} MENU_POSITIONS = { "top-left": "Top Left", "top-right": "Top Right", "bottom-left": "Bottom Left", "bottom-right": "Bottom Right", - "mouse": "Mouse" + "mouse": "Mouse", } MENU_POSITIONS_DICT = { @@ -60,17 +64,20 @@ "Top Right": {"icon": "qappmenu-top-right", "object": "top-right"}, "Bottom Left": {"icon": "qappmenu-bottom-left", "object": "bottom-left"}, "Bottom Right": {"icon": "qappmenu-bottom-right", "object": "bottom-right"}, - "Mouse": {"icon": "input-mouse-symbolic", "object": "mouse"}} + "Mouse": {"icon": "input-mouse-symbolic", "object": "mouse"}, +} + class AppMenuSettings(Gtk.Application): """ Qubes Menu Settings app. """ + def __init__(self, qapp: qubesadmin.Qubes): """ :param qapp: qubesadmin.Qubes object """ - super().__init__(application_id='org.qubesos.appmenusettings') + super().__init__(application_id="org.qubesos.appmenusettings") self.qapp = qapp self.vm = self.qapp.domains[self.qapp.local_name] @@ -92,49 +99,54 @@ def perform_setup(self): """ self.builder = Gtk.Builder() - glade_path = (importlib.resources.files('qubes_menu_settings') / - 'menu_settings.glade') + glade_path = ( + importlib.resources.files("qubes_menu_settings") / "menu_settings.glade" + ) with importlib.resources.as_file(glade_path) as path: self.builder.add_from_file(str(path)) - self.main_window : Gtk.ApplicationWindow = \ - self.builder.get_object('main_window') + self.main_window: Gtk.ApplicationWindow = self.builder.get_object("main_window") - self.confirm_button: Gtk.Button = \ - self.builder.get_object('button_confirm') - self.apply_button: Gtk.Button = self.builder.get_object('button_apply') - self.cancel_button: Gtk.Button = \ - self.builder.get_object('button_cancel') + self.confirm_button: Gtk.Button = self.builder.get_object("button_confirm") + self.apply_button: Gtk.Button = self.builder.get_object("button_apply") + self.cancel_button: Gtk.Button = self.builder.get_object("button_cancel") - self.starting_page_combo: Gtk.ComboBox = \ - self.builder.get_object("starting_page_combo") + self.starting_page_combo: Gtk.ComboBox = self.builder.get_object( + "starting_page_combo" + ) - self.menu_position_combo: Gtk.ComboBox = \ - self.builder.get_object("menu_position_combo") + self.menu_position_combo: Gtk.ComboBox = self.builder.get_object( + "menu_position_combo" + ) - self.sort_running_check: Gtk.CheckButton = \ - self.builder.get_object("sort_running_to_top_check") + self.sort_running_check: Gtk.CheckButton = self.builder.get_object( + "sort_running_to_top_check" + ) screen = Gdk.Screen.get_default() provider = Gtk.CssProvider() - css_path = (importlib.resources.files('qubes_menu_settings') / - 'menu_settings.css') + css_path = ( + importlib.resources.files("qubes_menu_settings") / "menu_settings.css" + ) with importlib.resources.as_file(css_path) as path: provider.load_from_path(str(path)) Gtk.StyleContext.add_provider_for_screen( - screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ) self.confirm_button.connect("clicked", self._save_exit) self.apply_button.connect("clicked", self._save) self.cancel_button.connect("clicked", self._quit) self.initial_page_model = ImageListModeler( - self.starting_page_combo, MENU_PAGES_DICT) + self.starting_page_combo, MENU_PAGES_DICT + ) self.menu_position_model = ImageListModeler( - self.menu_position_combo, MENU_POSITIONS_DICT) + self.menu_position_combo, MENU_POSITIONS_DICT + ) self.load_state() @@ -157,8 +169,7 @@ def load_state(self): self.menu_position_model.update_initial() # this can sometimes be None, thus, the "or False) - sort_running = \ - bool(self.vm.features.get(SORT_RUNNING_FEATURE, False)) + sort_running = bool(self.vm.features.get(SORT_RUNNING_FEATURE, False)) self.sort_running_check.set_active(sort_running) def _quit(self, *_args): @@ -175,19 +186,17 @@ def _save(self, *_args): if old_sort_running: del self.vm.features[SORT_RUNNING_FEATURE] - old_initial_page = self.vm.features.get(INITIAL_PAGE_FEATURE, - "app_page") + old_initial_page = self.vm.features.get(INITIAL_PAGE_FEATURE, "app_page") if self.initial_page_model.get_selected() != old_initial_page: - self.vm.features[INITIAL_PAGE_FEATURE] = \ + self.vm.features[INITIAL_PAGE_FEATURE] = ( self.initial_page_model.get_selected() + ) - old_menu_position = self.vm.features.get(POSITION_FEATURE, - "mouse") + old_menu_position = self.vm.features.get(POSITION_FEATURE, "mouse") if self.menu_position_model.get_selected() != old_menu_position: - self.vm.features[POSITION_FEATURE] = \ - self.menu_position_model.get_selected() + self.vm.features[POSITION_FEATURE] = self.menu_position_model.get_selected() def _save_exit(self, *_args): self._save() @@ -203,5 +212,5 @@ def main(): app.run() -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(main()) diff --git a/qubes_menu_settings/test_menu_settings.py b/qubes_menu_settings/test_menu_settings.py index daf5bc0..73bebd6 100644 --- a/qubes_menu_settings/test_menu_settings.py +++ b/qubes_menu_settings/test_menu_settings.py @@ -24,10 +24,10 @@ def test_menu_settings_load(): qapp = MockQubesComplete() - qapp._qubes['dom0'].features['menu-initial-page'] = 'favorites_page' - qapp._qubes['dom0'].features['menu-sort-running'] = '1' - qapp._qubes['dom0'].features['menu-favorites'] = '' - qapp._qubes['dom0'].features['menu-position'] = '' + qapp._qubes["dom0"].features["menu-initial-page"] = "favorites_page" + qapp._qubes["dom0"].features["menu-sort-running"] = "1" + qapp._qubes["dom0"].features["menu-favorites"] = "" + qapp._qubes["dom0"].features["menu-position"] = "" qapp.update_vm_calls() @@ -42,10 +42,10 @@ def test_menu_settings_load(): def test_menu_settings_change(): qapp = MockQubesComplete() - qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page' - qapp._qubes['dom0'].features['menu-sort-running'] = '' - qapp._qubes['dom0'].features['menu-favorites'] = '' - qapp._qubes['dom0'].features['menu-position'] = 'mouse' + qapp._qubes["dom0"].features["menu-initial-page"] = "app_page" + qapp._qubes["dom0"].features["menu-sort-running"] = "" + qapp._qubes["dom0"].features["menu-favorites"] = "" + qapp._qubes["dom0"].features["menu-position"] = "mouse" qapp.update_vm_calls() @@ -61,19 +61,25 @@ def test_menu_settings_change(): app.menu_position_combo.set_active_id("Top Left") # the first option is Top Left app.sort_running_check.set_active(True) - qapp.expected_calls[('dom0', 'admin.vm.feature.Set', 'menu-sort-running', b'1')] = b'0\0' - qapp.expected_calls[('dom0', 'admin.vm.feature.Set', 'menu-initial-page', b'search_page')] = b'0\0' - qapp.expected_calls[('dom0', 'admin.vm.feature.Set', 'menu-position', b'top-left')] = b'0\0' + qapp.expected_calls[("dom0", "admin.vm.feature.Set", "menu-sort-running", b"1")] = ( + b"0\0" + ) + qapp.expected_calls[ + ("dom0", "admin.vm.feature.Set", "menu-initial-page", b"search_page") + ] = b"0\0" + qapp.expected_calls[ + ("dom0", "admin.vm.feature.Set", "menu-position", b"top-left") + ] = b"0\0" app._save() def test_menu_settings_change2(): qapp = MockQubesComplete() - qapp._qubes['dom0'].features['menu-initial-page'] = 'app_page' - qapp._qubes['dom0'].features['menu-sort-running'] = '' - qapp._qubes['dom0'].features['menu-favorites'] = '' - qapp._qubes['dom0'].features['menu-position'] = 'mouse' + qapp._qubes["dom0"].features["menu-initial-page"] = "app_page" + qapp._qubes["dom0"].features["menu-sort-running"] = "" + qapp._qubes["dom0"].features["menu-favorites"] = "" + qapp._qubes["dom0"].features["menu-position"] = "mouse" qapp.update_vm_calls() @@ -86,6 +92,8 @@ def test_menu_settings_change2(): app.starting_page_combo.set_active_id("Favorites") - qapp.expected_calls[('dom0', 'admin.vm.feature.Set', 'menu-initial-page', b'favorites_page')] = b'0\0' + qapp.expected_calls[ + ("dom0", "admin.vm.feature.Set", "menu-initial-page", b"favorites_page") + ] = b"0\0" app._save() diff --git a/setup.py b/setup.py index 05e7c38..46a5c3f 100644 --- a/setup.py +++ b/setup.py @@ -1,28 +1,29 @@ #!/usr/bin/env python3 -''' Setup.py file ''' +"""Setup.py file""" import setuptools.command.install -setuptools.setup(name='qubes_menu', - version='0.1', - author='Invisible Things Lab', - author_email='marmarta@invisiblethingslab.com', - description='Qubes App Menu', - license='GPL2+', - url='https://www.qubes-os.org/', - packages=["qubes_menu", "qubes_menu_settings"], - entry_points={ - 'gui_scripts': [ - 'qubes-app-menu = qubes_menu.appmenu:main', - 'qubes-appmenu-settings = qubes_menu_settings.menu_settings:main', - ] - }, - package_data={ - 'qubes_menu': ["qubes-menu.glade", - "qubes-menu-dark.css", - "qubes-menu-light.css", - "qubes-menu-base.css", - ], - "qubes_menu_settings": ["menu_settings.glade", - "menu_settings.css"] - }, +setuptools.setup( + name="qubes_menu", + version="0.1", + author="Invisible Things Lab", + author_email="marmarta@invisiblethingslab.com", + description="Qubes App Menu", + license="GPL2+", + url="https://www.qubes-os.org/", + packages=["qubes_menu", "qubes_menu_settings"], + entry_points={ + "gui_scripts": [ + "qubes-app-menu = qubes_menu.appmenu:main", + "qubes-appmenu-settings = qubes_menu_settings.menu_settings:main", + ] + }, + package_data={ + "qubes_menu": [ + "qubes-menu.glade", + "qubes-menu-dark.css", + "qubes-menu-light.css", + "qubes-menu-base.css", + ], + "qubes_menu_settings": ["menu_settings.glade", "menu_settings.css"], + }, )