Files
sims/sims/modules/launcher/apps.py
T
2026-09-15 19:53:43 +02:00

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,
)