fix(services/spotlight): improve MPRIS artwork handling and scope keyword searches to single plugin

- mpris: native MediaArt cache lookup, dedupe play/pause emission, safe
  cleanup, artwork polling for already-playing browser players, drop
  os.system seek fallback
- spotlight: scope plugin keyword and detect() matches to a single plugin
  via search_single instead of global search; register clipboard keywords
  (clip/clipboard)
This commit is contained in:
sankalp committed 2026-07-13 22:20:19 +05:45
1 parent 1fc7edd483
commit 54ff85ffc9
43 files changed
+1227 -637

No files matched your search

-2
View File
@@ -113,8 +113,6 @@ class PluginManager:
if entry.module_name:
self._loader.unload_module(entry.module_name)
from pathlib import Path
fp = Path(entry.path) if entry.path else None
if fp and fp.exists():
source = entry.source
+36 -4
View File
@@ -75,7 +75,7 @@ class SearchPipeline:
self._current_token.cancel()
with self._lock:
for t in self._pending_threads:
t.join(timeout=0.5)
t.join(timeout=0.1)
self._pending_threads.clear()
for source_id in self._pending_idle_ids:
GLib.source_remove(source_id)
@@ -112,13 +112,33 @@ class SearchPipeline:
results = self._safe_search(instance, query, token)
self._stamp_results(results, entry)
if not token.is_cancelled:
source_id = GLib.idle_add(lambda: callback(results))
def _fire():
with self._lock:
try:
self._pending_idle_ids.remove(source_id)
except ValueError:
pass
callback(results)
return False
source_id = GLib.idle_add(_fire)
with self._lock:
self._pending_idle_ids.append(source_id)
except Exception as e:
logger.error(f"[SearchPipeline] Error searching {entry.id}: {e}")
if not token.is_cancelled:
source_id = GLib.idle_add(lambda: callback([]))
def _fire_err():
with self._lock:
try:
self._pending_idle_ids.remove(source_id)
except ValueError:
pass
callback([])
return False
source_id = GLib.idle_add(_fire_err)
with self._lock:
self._pending_idle_ids.append(source_id)
@@ -143,6 +163,8 @@ class SearchPipeline:
remaining = len(searchable)
def _on_all_done():
if token.is_cancelled:
return
merged = self._merge_results(collected)
callback(merged)
@@ -151,7 +173,17 @@ class SearchPipeline:
with results_lock:
remaining -= 1
if remaining <= 0:
source_id = GLib.idle_add(_on_all_done)
def _fire_all_done():
with self._lock:
try:
self._pending_idle_ids.remove(source_id)
except ValueError:
pass
_on_all_done()
return False
source_id = GLib.idle_add(_fire_all_done)
with self._lock:
self._pending_idle_ids.append(source_id)
+58 -35
View File
@@ -1,4 +1,3 @@
import gc
import os
from fabric.core import Signal
@@ -98,7 +97,7 @@ class Spotlight(Box):
self.children = self.header, self.scrolled_clip
# Search
@debounce(200)
@debounce(300)
def on_entry_changed(self, entry: Entry, *_):
if getattr(entry, "_ignore_change", False):
entry._ignore_change = False
@@ -128,7 +127,11 @@ class Spotlight(Box):
self.header_icon.set_from_icon_name(
getattr(inst, "icon", "") or "system-search-symbolic"
)
self.plugin_service.pipeline.search(text, self._on_search_results)
# Scope to the detected plugin only — a global search would
# mix in unrelated plugins' results.
self.plugin_service.pipeline.search_single(
pe, text, self._on_search_results
)
return
self.header_icon.set_from_icon_name("system-search-symbolic")
@@ -225,7 +228,27 @@ class Spotlight(Box):
self._deactivate_keyword_plugin()
def _on_search_results(self, results: list[SearchResult]):
self._clear_viewport()
new_ids = [r.id for r in results]
old_ids = list(self._result_widgets.keys())
if new_ids == old_ids:
self._current_results = results
return
for rid in set(old_ids) - set(new_ids):
self._remove_result_widget(rid)
for child in list(self.viewport.children):
self.viewport.remove(child)
for result in results:
widget = self._result_widgets.get(result.id)
if not widget:
widget = self._render_result(result)
self._result_widgets[result.id] = widget
widget.connect("enter-notify-event", self.on_slot_enter)
self.viewport.add(widget)
self._current_results = results
if not results:
@@ -234,15 +257,19 @@ class Spotlight(Box):
return
self._auto_start_refresh_timer(results)
for result in results:
widget = self._render_result(result)
self._result_widgets[result.id] = widget
self.viewport.add(widget)
widget.connect("enter-notify-event", self.on_slot_enter)
self._post_arrange()
def _remove_result_widget(self, result_id: str):
widget = self._result_widgets.pop(result_id, None)
if not widget:
return
try:
widget.disconnect_by_func(self.on_slot_enter)
except (TypeError, ValueError):
pass
self.viewport.remove(widget)
widget.destroy()
# Rendering
def _render_result(self, result: SearchResult) -> Button:
rt = result.render_type
@@ -298,7 +325,7 @@ class Spotlight(Box):
style_classes="app-slot",
child=Box(orientation="h", spacing=12, children=children),
tooltip_text=r.subtitle,
on_clicked=lambda *_: self._activate_result(r),
on_clicked=lambda *_: self._activate_result_by_id(r.id),
)
def _render_calc(self, r: SearchResult) -> Button:
@@ -330,7 +357,7 @@ class Spotlight(Box):
),
],
),
on_clicked=lambda *_: self._activate_result(r),
on_clicked=lambda *_: self._activate_result_by_id(r.id),
)
def _render_emoji(self, r: SearchResult) -> Button:
@@ -368,7 +395,7 @@ class Spotlight(Box):
],
),
tooltip_text=f"Click to copy {emoji_char}",
on_clicked=lambda *_: self._activate_result(r),
on_clicked=lambda *_: self._activate_result_by_id(r.id),
)
def _render_power(self, r: SearchResult) -> Button:
@@ -399,7 +426,7 @@ class Spotlight(Box):
],
),
tooltip_text=f"Click to {r.title.lower()} the system",
on_clicked=lambda *_: self._activate_result(r),
on_clicked=lambda *_: self._activate_result_by_id(r.id),
)
def _render_wallpaper(self, r: SearchResult) -> Button:
@@ -415,7 +442,7 @@ class Spotlight(Box):
return Button(
style_classes="app-slot wallpaper",
child=child_widget,
on_clicked=lambda *_: self._activate_result(r),
on_clicked=lambda *_: self._activate_result_by_id(r.id),
)
def _render_clipboard_text(self, r: SearchResult) -> Button:
@@ -436,7 +463,7 @@ class Spotlight(Box):
],
),
tooltip_text=r.subtitle,
on_clicked=lambda *_: self._activate_result(r),
on_clicked=lambda *_: self._activate_result_by_id(r.id),
)
def _render_clipboard_image(self, r: SearchResult) -> Button:
@@ -465,7 +492,7 @@ class Spotlight(Box):
return Button(
style_classes="app-slot wallpaper clipboard-image",
child=child_widget,
on_clicked=lambda *_: self._activate_result(r),
on_clicked=lambda *_: self._activate_result_by_id(r.id),
)
def _render_default(self, r: SearchResult) -> Button:
@@ -497,7 +524,7 @@ class Spotlight(Box):
return Button(
style_classes="app-slot",
child=Box(orientation="h", spacing=12, children=children),
on_clicked=lambda *_: self._activate_result(r),
on_clicked=lambda *_: self._activate_result_by_id(r.id),
)
def _activate_result(self, result: SearchResult):
@@ -505,19 +532,23 @@ class Spotlight(Box):
result.action()
self.launched()
def _activate_result_by_id(self, result_id: str):
for r in self._current_results:
if r.id == result_id:
self._activate_result(r)
return
# Viewport management
def _clear_viewport(self):
old_results = self._current_results
self._current_results = []
self._result_widgets.clear()
for child in self.viewport.children:
for child in list(self.viewport.children):
try:
child.disconnect_by_func(self.on_slot_enter)
except (TypeError, ValueError):
pass
self.viewport.remove(child)
child.destroy()
del old_results
self._result_widgets.clear()
def update_result_subtitle(self, result_id: str, text: str) -> None:
"""Update subtitle label of a rendered result in-place (no re-render)."""
@@ -528,9 +559,6 @@ class Spotlight(Box):
if not child:
return
# Walk: Button → main Box → title_box (last Box) → subtitle Label (2nd)
from fabric.widgets.box import Box
from fabric.widgets.label import Label
boxes = [c for c in child.get_children() if isinstance(c, Box)]
if not boxes:
return
@@ -551,12 +579,6 @@ class Spotlight(Box):
self.scrolled_window.show()
self.scrolled_window.animate_size(new_height)
for i, slot in enumerate(self.viewport.children, start=1):
if i > 8:
break
slot.set_style(f"animation-duration: {round(i * 300)}ms;")
slot.add_style_class("shine")
if self.viewport.children:
self.set_selected_index(0)
@@ -660,15 +682,17 @@ class SpotlightWindow(Window):
def close_spotlight(self):
self.spotlight_box.plugin_service.pipeline.cancel()
self.spotlight_box._stop_refresh_timer()
self.spotlight_box.header_entry.set_text("")
debounce_id = getattr(
self.spotlight_box, "_debounce_timer_on_entry_changed", None
)
if debounce_id:
GLib.source_remove(debounce_id)
self.spotlight_box._debounce_timer_on_entry_changed = 0
self.spotlight_box._stop_refresh_timer()
self.spotlight_box.header_entry.set_text("")
self.spotlight_box._clear_viewport()
self.spotlight_box.scrolled_window.animate_size(0)
self.spotlight_box.scrolled_window.hide()
@@ -676,7 +700,6 @@ class SpotlightWindow(Window):
self.spotlight_box.header_icon.set_from_icon_name("system-search-symbolic")
self.spotlight_box._deactivate_keyword_plugin()
self.spotlight_box.plugin_service.manager.release_memory_all()
gc.collect()
self.hide()
def toggle(
+16 -1
View File
@@ -1,3 +1,4 @@
import time
from typing import Any
from fabric.utils import DesktopApp, get_desktop_applications
@@ -17,6 +18,8 @@ class ApplicationPlugin(SpotlightPlugin):
def __init__(self, context):
super().__init__(context)
self._desktop_apps: list[DesktopApp] = []
self._apps_loaded_at: float = 0.0
self._apps_ttl: float = 1.0
def initialize(self) -> None:
self._desktop_apps = get_desktop_applications()
@@ -28,6 +31,16 @@ class ApplicationPlugin(SpotlightPlugin):
if not query:
return []
# Re-scan (throttled) so newly installed .desktop files appear
# without a restart, while avoiding a disk scan on every keystroke.
now = time.monotonic()
if now - self._apps_loaded_at > self._apps_ttl:
try:
self._desktop_apps = get_desktop_applications()
self._apps_loaded_at = now
except Exception:
pass
results: list[SearchResult] = []
for app in self._desktop_apps:
display_name = getattr(app, "display_name", "") or ""
@@ -60,7 +73,9 @@ class ApplicationPlugin(SpotlightPlugin):
@staticmethod
def _launch_app(app: DesktopApp) -> None:
app.launch()
from globalmenu.launch import launch_desktop_app
launch_desktop_app(app)
PLUGIN = ApplicationPlugin
+6 -1
View File
@@ -13,6 +13,7 @@ class ClipboardPlugin(SpotlightPlugin):
id = "clipboard"
name = "Clipboard"
icon = "edit-paste-symbolic"
keywords = ["clip", "clipboard"]
searchable = True
priority = 80
@@ -45,7 +46,11 @@ class ClipboardPlugin(SpotlightPlugin):
def search(self, query: str, token: Any) -> list[SearchResult]:
q = query.strip()
if q.lower().startswith("clip "):
if q.lower().startswith("clipboard "):
q = q[10:].strip()
elif q.lower() == "clipboard":
q = ""
elif q.lower().startswith("clip "):
q = q[5:].strip()
elif q.lower() == "clip":
q = ""
-1
View File
@@ -36,7 +36,6 @@ class EmojiPlugin(SpotlightPlugin):
return
cldr = self._load_cldr_annotations()
self._emoji_data = cldr or {}
logger.info(f"[Emoji] Loaded {len(self._emoji_data)} emojis")
def detect(self, text: str) -> bool:
s = text.strip()
+1 -3
View File
@@ -1,4 +1,3 @@
import gc
import os
import random
from typing import Any
@@ -8,7 +7,7 @@ from shared.data import WALLPAPERS_THUMBNAILS_SIZE
from utils.functions import fuzzy_filter
from window.spotlight.api import SearchResult, SpotlightPlugin
MAX_THUMBS_PER_SEARCH = 5
MAX_THUMBS_PER_SEARCH = 3
class WallpaperPlugin(SpotlightPlugin):
@@ -28,7 +27,6 @@ class WallpaperPlugin(SpotlightPlugin):
def cleanup(self) -> None:
self._wallpaper_service = None
gc.collect()
def release_memory(self) -> None:
if self._wallpaper_service: