""" 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 dataclasses import dataclass 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 import subprocess from time import sleep import threading @dataclass() class CustomApp: name: str generic_name: str | None display_name: str | None description: str | None executable: str | None command_line: str | None hidden: bool def __init__( self, name, display_name=None, executable=None, generic_name=None, description=None, command_line=None, hidden=False, ): self.name = name self.generic_name = generic_name self.display_name = display_name self.description = description self.executable = executable self.command_line = command_line self.hidden = hidden def launch(self): def background(): subprocess.run([self.command_line]) threading.Thread(target=background, daemon=True).start() def get_icon_pixbuf( self, size: int = 48, default_icon: str | None = "image-missing", ) -> None: return None 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._custom_apps = [ CustomApp("Screenshot Clipboard", command_line="grim2clip") ] 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 = [] combined_apps = self._all_apps + self._custom_apps # make a new iterator containing the filtered apps filtered_apps_iter = iter( [ app for app in combined_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 *_: (self.hide(), app.launch()), **kwargs, )