130 lines
4.2 KiB
Python
130 lines
4.2 KiB
Python
import re
|
|
import shlex
|
|
import subprocess
|
|
import time
|
|
|
|
from fabric.utils.helpers import DesktopApp, get_desktop_applications
|
|
from fabric.widgets.box import Box
|
|
from fabric.widgets.image import Image
|
|
from fabric.widgets.label import Label
|
|
from gi.repository import Gtk
|
|
|
|
from .base import FuzzyMenu
|
|
from .history import LaunchHistory
|
|
from .ranking import rank_items
|
|
|
|
|
|
ICON_SIZE = 32
|
|
|
|
_FIELD_CODE_RE = re.compile(r"^%[fFuUickdDnNvm]$")
|
|
|
|
|
|
class AppProvider:
|
|
def __init__(self, history: LaunchHistory | None = None):
|
|
self._history = history if history is not None else LaunchHistory()
|
|
|
|
def items(self) -> list[DesktopApp]:
|
|
return get_desktop_applications()
|
|
|
|
def filter(self, items: list[DesktopApp], query: str) -> list[DesktopApp]:
|
|
"""Rank apps against *query*, best match first.
|
|
|
|
With an empty query everything matches equally, so the order is
|
|
purely "what gets launched most" - the launcher opens on your
|
|
most useful apps instead of an arbitrary alphabetical slice.
|
|
"""
|
|
now = time.time()
|
|
return rank_items(
|
|
items,
|
|
query,
|
|
_fields,
|
|
bonus_of=lambda app: self._history.bonus(_app_key(app), now=now),
|
|
tie_break_of=_sort_name,
|
|
)
|
|
|
|
def render(self, item: DesktopApp) -> Gtk.Widget:
|
|
children: list[Gtk.Widget] = []
|
|
|
|
pixbuf = item.get_icon_pixbuf(size=ICON_SIZE)
|
|
if pixbuf is not None:
|
|
children.append(Image(pixbuf=pixbuf, name="app-icon"))
|
|
|
|
primary = item.display_name or item.name or ""
|
|
text_box = Box(name="app-text", orientation="v", spacing=0)
|
|
text_box.add(Label(label=primary, name="app-name", h_align="start"))
|
|
if item.generic_name and item.generic_name != primary:
|
|
text_box.add(
|
|
Label(label=item.generic_name, name="app-generic", h_align="start")
|
|
)
|
|
children.append(text_box)
|
|
|
|
return Box(name="slot-box", orientation="h", spacing=10, children=children)
|
|
|
|
def activate(self, item: DesktopApp) -> None:
|
|
self._history.record(_app_key(item))
|
|
# Launch in a transient systemd --user scope so the app gets its own
|
|
# cgroup instead of inheriting sims.service's. start_new_session alone
|
|
# only changes POSIX session/pgid; systemd tracks units by cgroup and
|
|
# would kill children with sims on stop (default KillMode=control-group).
|
|
if item.command_line:
|
|
argv = [
|
|
t for t in shlex.split(item.command_line) if not _FIELD_CODE_RE.match(t)
|
|
]
|
|
if argv:
|
|
subprocess.Popen(
|
|
[
|
|
"systemd-run",
|
|
"--quiet",
|
|
"--user",
|
|
"--scope",
|
|
"--collect",
|
|
"--",
|
|
*argv,
|
|
],
|
|
start_new_session=True,
|
|
)
|
|
return
|
|
item.launch()
|
|
|
|
|
|
def _fields(app: DesktopApp) -> dict[str, str | None]:
|
|
return {
|
|
"name": app.name,
|
|
"display_name": app.display_name,
|
|
"generic_name": app.generic_name,
|
|
"executable": app.executable,
|
|
}
|
|
|
|
|
|
def _sort_name(app: DesktopApp) -> str:
|
|
return (app.display_name or app.name or "").casefold()
|
|
|
|
|
|
def _app_key(app: DesktopApp) -> str:
|
|
"""Stable identity for the launch history.
|
|
|
|
DesktopApp does not expose the desktop file id, but it wraps the
|
|
GioUnix.DesktopAppInfo which does. Fall back to the command line
|
|
(unique per Steam game) and finally the name.
|
|
"""
|
|
info = getattr(app, "_app", None)
|
|
get_id = getattr(info, "get_id", None) if info is not None else None
|
|
if callable(get_id):
|
|
try:
|
|
app_id = get_id()
|
|
except Exception: # pragma: no cover - defensive
|
|
app_id = None
|
|
if isinstance(app_id, str) and app_id:
|
|
return app_id
|
|
return app.command_line or app.executable or app.name or ""
|
|
|
|
|
|
def AppLauncher(monitor: int = 0) -> FuzzyMenu:
|
|
return FuzzyMenu(
|
|
provider=AppProvider(),
|
|
monitor=monitor,
|
|
placeholder="Search Apps...",
|
|
window_name="app-launcher",
|
|
max_results=8,
|
|
)
|