Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add search to current VM Tab #11

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Glade backup file
*.glade~
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be added to git.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not? .gitignore is used to ignore build files and temporary files.


# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
21 changes: 21 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
watchdog = "*"
pyxdg = "*"
pygobject = "*"
gbulb = "*"
qubesadmin = "*" # install this manually, or use the following
# qubesadmin = {git = "https://github.com/QubesOS/qubes-core-admin-client"}
thefuzz = "*"

[dev-packages]
pytest = "*"
# pytest-asyncio = "*"
pylint = "*"

[requires]
python_version = ">=3.8"
313 changes: 313 additions & 0 deletions Pipfile.lock

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ position or via executing `qubes-app-menu` with desired params in CLI.

![](readme_img/menu_howto.png)

## How to test
```
pipenv shell
QUBES_MENU_TEST=1 python qubes_menu
```

You also need to install `qubesadmin` from https://github.com/QubesOS/qubes-core-admin-client. Run `python setup.py` in the dowloaded repo in pipenv shell to install.

## Technical details

### New features
Expand Down
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
asyncio_mode=strict
4 changes: 4 additions & 0 deletions qubes_menu/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .appmenu import main

if __name__ == "__main__":
main()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No newline at the end of file.

16 changes: 8 additions & 8 deletions qubes_menu/appmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,23 +264,23 @@ def exit_app(self):
task.cancel()


from .tests.mock_app import new_mock_qapp
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mocking should not happen in the application code itself, but exclusively in the tests.

Copy link
Author

@iacore iacore Mar 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to run the app in dom0, and I need a way to run the app.

This feature is off by default: https://github.com/QubesOS/qubes-desktop-linux-menu/pull/11/files#diff-609223c61e23f236f844ac3294812b922d4d09f34be6c552c7b6d43dd5c21d4bR41

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, create a file in tests to run it with qubesadmin.Qubes() patched. Production code should not import tests under any circumstances.

import os


def main():
"""
Start the menu app
"""

qapp = qubesadmin.Qubes()
if "QUBES_MENU_TEST" in os.environ:
qapp = new_mock_qapp(qapp)
qapp.domains[qapp.local_name] = qapp.domains['dom0']
dispatcher = qubesadmin.events.EventsDispatcher(qapp)
app = AppMenu(qapp, dispatcher)
app.run(sys.argv)

if f'--{constants.RESTART_PARAM_LONG}' in sys.argv or \
f'-{constants.RESTART_PARAM_SHORT}' in sys.argv:
sys.argv = [x for x in sys.argv if x not in
(f'--{constants.RESTART_PARAM_LONG}',
f'-{constants.RESTART_PARAM_SHORT}')]
app = AppMenu(qapp, dispatcher)
app.run(sys.argv)


if __name__ == '__main__':
sys.exit(main())
87 changes: 43 additions & 44 deletions qubes_menu/desktop_file_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
"""
Helper class that manages all events related to .desktop files.
"""
import pyinotify
import logging
import asyncio
import os
import shlex

from watchdog.observers import Observer
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? In what ways is watchdog better than pyinotify?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pyinotify is using asyncore, which is deprecated

from watchdog.events import FileSystemEventHandler
import xdg.DesktopEntry
import xdg.BaseDirectory
import xdg.Menu
Expand Down Expand Up @@ -125,6 +127,39 @@ def is_qubes_specific(self):
return 'X-Qubes-VM' in self.categories



class FileChangeHandler(FileSystemEventHandler):
def __init__(self, manager: 'DesktopFileManager'):
self.manager = manager
super().__init__()

def try_load(self, filename):
try:
self.manager.load_file(filename)
except FileNotFoundError:
self.manager.remove_file(filename)

def on_created(self, event):
"""On file create, attempt to load it. This can lead to spurious
warnings due to 0-byte files being loaded, but in some cases
is necessary to correctly process files."""
self.try_load(event.src_path)

def on_deleted(self, event):
"""
On file delete, remove the tile and all its children menu entries
"""
self.manager.remove_file(event.src_path)

def on_modified(self, event):
"""On modify, simply attempt to load the file again."""
self.try_load(event.src_path)

def on_moved(self, event):
self.manager.remove_file(event.src_path)
self.try_load(event.dest_path)


class DesktopFileManager:
"""
Class that loads, caches and observes changes in .desktop files.
Expand All @@ -133,40 +168,10 @@ class DesktopFileManager:
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__()

def process_IN_CREATE(self, event):
"""On file create, attempt to load it. This can lead to spurious
warnings due to 0-byte files being loaded, but in some cases
is necessary to correctly process files."""
try:
self.parent.load_file(event.pathname)
except FileNotFoundError:
self.parent.remove_file(event.pathname)

def process_IN_DELETE(self, event):
"""
On file delete, remove the tile and all its children menu entries
"""
self.parent.remove_file(event.pathname)

def process_IN_MODIFY(self, event):
"""On modify, simply attempt to laod the file again."""
try:
self.parent.load_file(event.pathname)
except FileNotFoundError:
self.parent.remove_file(event.pathname)

def __init__(self, qapp):
self.qapp = qapp
self.watch_manager = None
self.notifier = None
self.watches = []
self.observer = None
self._callbacks: List[Callable] = []

# directories used by Qubes menu tools, not necessarily all possible
Expand Down Expand Up @@ -273,18 +278,12 @@ def _initialize_watchers(self):
"""
Initialize all watcher entities.
"""
self.watch_manager = pyinotify.WatchManager()
event_handler = FileChangeHandler(self)
observer = Observer()

# pylint: disable=no-member
mask = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY

loop = asyncio.get_event_loop()

self.notifier = pyinotify.AsyncioNotifier(
self.watch_manager, loop,
default_proc_fun=DesktopFileManager.EventProcessor(self))
self.observer = observer

for path in self.desktop_dirs:
self.watches.append(
self.watch_manager.add_watch(
str(path), mask, rec=True, auto_add=True))
observer.schedule(event_handler, str(path), recursive=True)

observer.start()
60 changes: 44 additions & 16 deletions qubes_menu/qubes-menu.glade
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<property name="can-focus">True</property>
<property name="tab-pos">left</property>
<child>
<object class="GtkBox">
<object class="GtkBox" id="tab_all">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
Expand Down Expand Up @@ -301,7 +301,7 @@
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkBox" id="tab_current_vm">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
Expand All @@ -319,30 +319,56 @@
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<property name="shadow-type">in</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkViewport">
<object class="GtkSearchEntry" id="current_search">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hscrollbar-policy">never</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkListBox" id="sys_tools_list">
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="selection-mode">none</property>
<style>
<class name="right_pane"/>
</style>
<child>
<object class="GtkListBox" id="sys_tools_list">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="selection-mode">none</property>
<style>
<class name="right_pane"/>
</style>
</object>
</child>
</object>
</child>
<style>
<class name="right_pane"/>
</style>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="right_pane"/>
</style>
</object>
<packing>
<property name="expand">True</property>
Expand All @@ -352,6 +378,7 @@
</child>
</object>
<packing>
<property name="menu-label">Stuff in Current VM</property>
<property name="position">2</property>
</packing>
</child>
Expand Down Expand Up @@ -393,6 +420,7 @@
</style>
</object>
<packing>
<property name="menu-label">Power Button</property>
<property name="tab-fill">False</property>
</packing>
</child>
Expand Down
19 changes: 17 additions & 2 deletions qubes_menu/settings_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,17 @@ class SettingsPage:
def __init__(self, qapp, builder: Gtk.Builder,
desktop_file_manager: DesktopFileManager,
dispatcher: qubesadmin.events.EventsDispatcher):
self.query = ""
self.query_entry: Gtk.SearchEntry = builder.get_object('current_search')
self.query_entry.connect("changed", self._on_query_change)

self.qapp = qapp
self.desktop_file_manager = desktop_file_manager
self.dispatcher = dispatcher

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.app_name > y.app_info.app_name)
self.app_list.set_sort_func(self._sort_apps)
self.app_list.set_filter_func(self._filter_apps)

self.category_list: Gtk.ListBox = builder.get_object(
Expand All @@ -80,9 +83,21 @@ def __init__(self, qapp, builder: Gtk.Builder,
self.app_list.invalidate_filter()
self.app_list.invalidate_sort()


def initialize_state(self):
"""On initialization, no category should be selected."""
self.category_list.select_row(None)

def _on_query_change(self, entry: Gtk.SearchEntry):
self.query = entry.get_text()
self.app_list.invalidate_sort()

def _sort_apps(self, x, y):
from thefuzz import fuzz
edit_distance = fuzz.token_sort_ratio
if not self.query:
return x.app_info.app_name > y.app_info.app_name
return edit_distance(self.query, x.app_info.app_name) < edit_distance(self.query, y.app_info.app_name)

def _filter_apps(self, row):
filter_func = getattr(self.category_list.get_selected_row(),
Expand Down
Loading