diff --git a/bar/main.py b/bar/main.py index 79df1cf..a865f22 100644 --- a/bar/main.py +++ b/bar/main.py @@ -11,6 +11,7 @@ from fabric.utils import ( ) from .modules.bar import StatusBar from .modules.window_fuzzy import FuzzyWindowFinder +from .modules.app_launcher import AppLauncher tray = SystemTray(name="system-tray", spacing=4) @@ -18,10 +19,11 @@ river = get_river_connection() dummy = Window(visible=False) finder = FuzzyWindowFinder() +launcher = AppLauncher() bar_windows = [] -app = Application("bar", dummy, finder) +app = Application("bar", dummy, finder, launcher) app.set_stylesheet_from_file(get_relative_path("styles/main.css")) diff --git a/bar/modules/app_launcher.py b/bar/modules/app_launcher.py new file mode 100644 index 0000000..84cc8d8 --- /dev/null +++ b/bar/modules/app_launcher.py @@ -0,0 +1,142 @@ +""" +example configuration shows how to make a simple +desktop applications launcher, this example doesn't involve +any styling (except a couple of basic style properties) + + +the purpose of this configuration is to show to to use +the given utils and mainly how using lazy executors might +make the configuration way more faster than it's supposed to be +""" + +import operator +from collections.abc import Iterator +from fabric.widgets.box import Box +from fabric.widgets.label import Label +from fabric.widgets.button import Button +from fabric.widgets.image import Image +from fabric.widgets.entry import Entry +from fabric.widgets.scrolledwindow import ScrolledWindow +from fabric.widgets.wayland import WaylandWindow as Window +from fabric.utils import DesktopApp, get_desktop_applications, idle_add, remove_handler + + +class AppLauncher(Window): + def __init__(self, **kwargs): + super().__init__( + layer="top", + anchor="center", + exclusivity="none", + keyboard_mode="on-demand", + visible=False, + all_visible=False, + **kwargs, + ) + self._arranger_handler: int = 0 + self._all_apps = get_desktop_applications() + + self.viewport = Box(spacing=2, orientation="v") + self.search_entry = Entry( + placeholder="Search Applications...", + h_expand=True, + notify_text=lambda entry, *_: self.arrange_viewport(entry.get_text()), + ) + self.scrolled_window = ScrolledWindow( + min_content_size=(280, 320), + max_content_size=(280 * 2, 320), + child=self.viewport, + ) + + self.add( + Box( + spacing=2, + orientation="v", + style="margin: 2px", + children=[ + # the header with the search entry + Box( + spacing=2, + orientation="h", + children=[ + self.search_entry, + Button( + image=Image(icon_name="window-close"), + tooltip_text="Exit", + on_clicked=lambda *_: self.application.quit(), + ), + ], + ), + # the actual slots holder + self.scrolled_window, + ], + ) + ) + self.show_all() + + def arrange_viewport(self, query: str = ""): + # reset everything so we can filter current viewport's slots... + # remove the old handler so we can avoid race conditions + remove_handler(self._arranger_handler) if self._arranger_handler else None + + # remove all children from the viewport + self.viewport.children = [] + + # make a new iterator containing the filtered apps + filtered_apps_iter = iter( + [ + app + for app in self._all_apps + if query.casefold() + in ( + (app.display_name or "") + + (" " + app.name + " ") + + (app.generic_name or "") + ).casefold() + ] + ) + should_resize = operator.length_hint(filtered_apps_iter) == len(self._all_apps) + + # all aboard... + # start the process of adding slots with a lazy executor + # using this method makes the process of adding slots way more less + # resource expensive without blocking the main thread and resulting in a lock + self._arranger_handler = idle_add( + lambda *args: self.add_next_application(*args) + or (self.resize_viewport() if should_resize else False), + filtered_apps_iter, + pin=True, + ) + + return False + + def add_next_application(self, apps_iter: Iterator[DesktopApp]): + if not (app := next(apps_iter, None)): + return False + + self.viewport.add(self.bake_application_slot(app)) + return True + + def resize_viewport(self): + self.scrolled_window.set_min_content_width( + self.viewport.get_allocation().width # type: ignore + ) + return False + + def bake_application_slot(self, app: DesktopApp, **kwargs) -> Button: + return Button( + child=Box( + orientation="h", + spacing=12, + children=[ + Image(pixbuf=app.get_icon_pixbuf(), h_align="start", size=32), + Label( + label=app.display_name or "Unknown", + v_align="center", + h_align="center", + ), + ], + ), + tooltip_text=app.description, + on_clicked=lambda *_: (app.launch(), self.hide()), + **kwargs, + )