feat: launcher recommendation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from fabric.utils.helpers import DesktopApp, get_desktop_applications
|
||||
from fabric.widgets.box import Box
|
||||
@@ -9,6 +10,8 @@ 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
|
||||
@@ -17,14 +20,27 @@ _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]:
|
||||
if not query:
|
||||
return items
|
||||
q = query.lower()
|
||||
return [a for a in items if _matches(a, q)]
|
||||
"""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] = []
|
||||
@@ -45,6 +61,7 @@ class AppProvider:
|
||||
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
|
||||
@@ -70,11 +87,36 @@ class AppProvider:
|
||||
item.launch()
|
||||
|
||||
|
||||
def _matches(app: DesktopApp, q: str) -> bool:
|
||||
for field in (app.name, app.display_name, app.generic_name, app.executable):
|
||||
if field and q in field.lower():
|
||||
return True
|
||||
return False
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user