193 lines
6.0 KiB
Python
193 lines
6.0 KiB
Python
"""
|
|
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,
|
|
)
|