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:
43 files changed
+1227
-637
No files matched your search
@@ -657,12 +657,6 @@ class BluetoothConnections(Box):
|
||||
device_slot = BluetoothDeviceSlot(device)
|
||||
self.other_devices.add(device_slot)
|
||||
|
||||
for slot in existing_paired.values():
|
||||
pass
|
||||
|
||||
for slot in existing_other.values():
|
||||
pass
|
||||
|
||||
if not self._destroyed:
|
||||
has_paired_devices = len(paired_devices) > 0
|
||||
has_other_devices = len(other_devices) > 0
|
||||
@@ -679,6 +673,7 @@ class BluetoothConnections(Box):
|
||||
|
||||
def start_device_monitoring(self):
|
||||
"""Start periodic monitoring for device changes"""
|
||||
self.stop_device_monitoring()
|
||||
self.refresh_timer = GLib.timeout_add_seconds(5, self.periodic_device_refresh)
|
||||
|
||||
def stop_device_monitoring(self):
|
||||
|
||||
@@ -7,7 +7,7 @@ from fabric.widgets.label import Label
|
||||
from fabric.widgets.overlay import Overlay
|
||||
|
||||
from services.brightness import Brightness
|
||||
from services.modus import is_caffeine_active, toggle_caffeine
|
||||
from services.inhibit import get_inhibit_service
|
||||
from services.network import NetworkClient
|
||||
from shared.widgets.flat_scale import FlatScale
|
||||
from shared.window.applet_window import AppletWindow
|
||||
@@ -133,11 +133,8 @@ class ModusControlCenter(AppletWindow):
|
||||
h_expand=True,
|
||||
)
|
||||
|
||||
# Only connect brightness controls if brightness service is available
|
||||
if get_brightness_service().max_screen > 0:
|
||||
pass
|
||||
else:
|
||||
# Disable brightness scale if no backlight device available
|
||||
# Disable brightness scale if no backlight device is available
|
||||
if get_brightness_service().max_screen <= 0:
|
||||
self.brightness_scale.set_sensitive(False)
|
||||
|
||||
self._mpris_manager = get_shared_mpris_manager()
|
||||
@@ -722,12 +719,8 @@ class ModusControlCenter(AppletWindow):
|
||||
self.per_app_volume_center_box.set_size_request(300, -1)
|
||||
|
||||
def _check_initial_states(self):
|
||||
self.caffeine_mode = is_caffeine_active()
|
||||
self.caffeine_mode = get_inhibit_service().active
|
||||
self.flight_mode = False
|
||||
GLib.timeout_add(100, self._update_initial_labels)
|
||||
|
||||
def _update_initial_labels(self):
|
||||
return False
|
||||
|
||||
def set_dont_disturb(self, *_):
|
||||
self.focus_mode = not self.focus_mode
|
||||
@@ -770,7 +763,7 @@ class ModusControlCenter(AppletWindow):
|
||||
|
||||
def toggle_caffeine(self, *_):
|
||||
try:
|
||||
self.caffeine_mode = toggle_caffeine()
|
||||
self.caffeine_mode = get_inhibit_service().toggle()
|
||||
self.caffeine_icon.dynamic_file(
|
||||
"applets/caffeine-on.svg"
|
||||
if self.caffeine_mode
|
||||
@@ -1200,11 +1193,6 @@ class ModusControlCenter(AppletWindow):
|
||||
self._cleanup_processes()
|
||||
self._disconnect_signals_when_hidden()
|
||||
|
||||
# Force garbage collection
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
|
||||
logger.debug("Complete cleanup finished successfully")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -11,6 +11,43 @@ from shared.widgets.flat_scale import FlatScale
|
||||
# Local imports
|
||||
from utils.roam import audio_service
|
||||
|
||||
# Mappings used to resolve an app's icon/display name from its description or
|
||||
# name. Defined once at module level to avoid rebuilding on every refresh.
|
||||
_APP_ICON_MAPPING = {
|
||||
"spotify": "spotify",
|
||||
"firefox": "firefox",
|
||||
"chromium": "chromium-browser",
|
||||
"chrome": "google-chrome",
|
||||
"vlc": "vlc",
|
||||
"discord": "discord",
|
||||
"steam": "steam",
|
||||
"zen": "zen-browser",
|
||||
"code": "vscode",
|
||||
"visual studio code": "vscode",
|
||||
"telegram": "telegram-desktop",
|
||||
"pulse": "audio-card",
|
||||
"pipewire": "audio-card",
|
||||
"alsa": "audio-card",
|
||||
"sink": "audio-speakers",
|
||||
"source": "audio-input-microphone",
|
||||
"youtube": "youtube",
|
||||
"music": "rhythmbox",
|
||||
"media": "multimedia-player",
|
||||
}
|
||||
|
||||
_APP_SPECIAL_NAMES = {
|
||||
"spotify": "Spotify",
|
||||
"firefox": "Firefox",
|
||||
"chromium": "Chromium",
|
||||
"chrome": "Chrome",
|
||||
"vlc": "VLC",
|
||||
"discord": "Discord",
|
||||
"steam": "Steam",
|
||||
"zen": "Zen Browser",
|
||||
"code": "VS Code",
|
||||
"telegram": "Telegram",
|
||||
}
|
||||
|
||||
|
||||
class PerAppVolumeControl(Box):
|
||||
"""Per-application volume control widget"""
|
||||
@@ -125,27 +162,7 @@ class PerAppVolumeControl(Box):
|
||||
# Check description first as it's more reliable
|
||||
search_text = app_description if app_description else app_name
|
||||
|
||||
icon_mapping = {
|
||||
"spotify": "spotify",
|
||||
"firefox": "firefox",
|
||||
"chromium": "chromium-browser",
|
||||
"chrome": "google-chrome",
|
||||
"vlc": "vlc",
|
||||
"discord": "discord",
|
||||
"steam": "steam",
|
||||
"zen": "zen-browser",
|
||||
"code": "vscode",
|
||||
"visual studio code": "vscode",
|
||||
"telegram": "telegram-desktop",
|
||||
"pulse": "audio-card",
|
||||
"pipewire": "audio-card",
|
||||
"alsa": "audio-card",
|
||||
"sink": "audio-speakers",
|
||||
"source": "audio-input-microphone",
|
||||
"youtube": "youtube",
|
||||
"music": "rhythmbox",
|
||||
"media": "multimedia-player",
|
||||
}
|
||||
icon_mapping = _APP_ICON_MAPPING
|
||||
|
||||
# Check if any key in mapping is contained in search text
|
||||
for key, icon in icon_mapping.items():
|
||||
@@ -167,22 +184,9 @@ class PerAppVolumeControl(Box):
|
||||
return "Unknown"
|
||||
|
||||
# Handle common app names specially
|
||||
special_names = {
|
||||
"spotify": "Spotify",
|
||||
"firefox": "Firefox",
|
||||
"chromium": "Chromium",
|
||||
"chrome": "Chrome",
|
||||
"vlc": "VLC",
|
||||
"discord": "Discord",
|
||||
"steam": "Steam",
|
||||
"zen": "Zen Browser",
|
||||
"code": "VS Code",
|
||||
"telegram": "Telegram",
|
||||
}
|
||||
|
||||
name_lower = name.lower()
|
||||
if name_lower in special_names:
|
||||
return special_names[name_lower]
|
||||
if name_lower in _APP_SPECIAL_NAMES:
|
||||
return _APP_SPECIAL_NAMES[name_lower]
|
||||
|
||||
# Default: capitalize first letter
|
||||
return name.capitalize()
|
||||
|
||||
@@ -631,6 +631,7 @@ class WifiConnections(Box):
|
||||
|
||||
def start_network_monitoring(self):
|
||||
"""Start periodic monitoring for network changes"""
|
||||
self.stop_network_monitoring()
|
||||
self.refresh_timer = GLib.timeout_add_seconds(5, self.periodic_network_refresh)
|
||||
|
||||
def stop_network_monitoring(self):
|
||||
|
||||
@@ -56,6 +56,10 @@ from .layout import DockLayout
|
||||
|
||||
_PIXBUF_CACHE_MAX = 64
|
||||
|
||||
# Terminal/interpreter executables whose name alone should not force a match
|
||||
# against unrelated desktop apps during dock item resolution.
|
||||
_COMMON_EXECUTABLES = ("kitty", "bash", "sh", "zsh", "python", "python3")
|
||||
|
||||
|
||||
class DockCanvas(Gtk.DrawingArea):
|
||||
def __init__(self, parent_window):
|
||||
@@ -136,9 +140,6 @@ class DockCanvas(Gtk.DrawingArea):
|
||||
self._desktop_apps = []
|
||||
return self._desktop_apps
|
||||
|
||||
def _refresh_desktop_apps(self) -> None:
|
||||
self._desktop_apps = []
|
||||
|
||||
def _canvas_height(self) -> int:
|
||||
size = self._base_icon_size()
|
||||
return int(
|
||||
@@ -921,7 +922,9 @@ class DockCanvas(Gtk.DrawingArea):
|
||||
if command_line:
|
||||
launch_app(command_line)
|
||||
elif hasattr(app_info, "launch"):
|
||||
app_info.launch()
|
||||
from globalmenu.launch import launch_desktop_app
|
||||
|
||||
launch_desktop_app(app_info)
|
||||
else:
|
||||
logger.error(
|
||||
f"[DockCanvas] Cannot determine launch command: {app_info}"
|
||||
@@ -976,8 +979,7 @@ class DockCanvas(Gtk.DrawingArea):
|
||||
if app.executable:
|
||||
exe = os.path.basename(app.executable).lower()
|
||||
if exe in expanded:
|
||||
common = ["kitty", "bash", "sh", "zsh", "python", "python3"]
|
||||
if exe in common:
|
||||
if exe in _COMMON_EXECUTABLES:
|
||||
app_names = [
|
||||
getattr(app, "name", ""),
|
||||
getattr(app, "display_name", ""),
|
||||
|
||||
+2
-2
@@ -257,12 +257,12 @@ def initialize():
|
||||
),
|
||||
)
|
||||
lockscreen.show()
|
||||
return lock, lockscreen
|
||||
|
||||
|
||||
def main():
|
||||
set_process_name("lockscreen")
|
||||
initialize()
|
||||
lockscreen = LockScreen(GtkSessionLock.Lock())
|
||||
_lock, lockscreen = initialize()
|
||||
|
||||
global app
|
||||
app = Application("lock", lockscreen)
|
||||
|
||||
@@ -21,7 +21,7 @@ from services.modus import notification_service
|
||||
from shared.widgets.clipping_box import ClippingBox
|
||||
from shared.widgets.custom_image import CustomImage
|
||||
from shared.widgets.customrevealer import SlideRevealer
|
||||
from utils.functions import escape_markup_text, parse_timeout_string
|
||||
from utils.functions import escape_markup_text
|
||||
from utils.roam import modus_service
|
||||
from utils.icon_resolver import IconResolver
|
||||
|
||||
@@ -1071,9 +1071,6 @@ class ModusNoti(Window):
|
||||
# Preload assets immediately for optimal caching and display performance
|
||||
preload_notification_assets(notification)
|
||||
|
||||
# Implement smart queue management for smooth transitions
|
||||
GLib.get_monotonic_time() / 1000
|
||||
|
||||
# If queue is getting full, remove oldest notifications smoothly
|
||||
if len(self.notification_queue) >= self.MAX_QUEUE_SIZE:
|
||||
# Remove oldest notification from queue (not current showing one)
|
||||
@@ -1271,21 +1268,6 @@ class ModusNoti(Window):
|
||||
def update_config(self, new_config):
|
||||
"""Update notification configuration dynamically."""
|
||||
try:
|
||||
# Handle notification timeout changes
|
||||
if "notification_timeout" in new_config:
|
||||
timeout_str = new_config["notification_timeout"]
|
||||
# Parse the timeout string to milliseconds
|
||||
|
||||
parse_timeout_string(timeout_str)
|
||||
|
||||
# Handle ignored apps changes
|
||||
if "notification_ignored_apps" in new_config:
|
||||
new_config["notification_ignored_apps"]
|
||||
|
||||
# Handle limited apps history changes
|
||||
if "notification_limited_apps_history" in new_config:
|
||||
new_config["notification_limited_apps_history"]
|
||||
|
||||
# Update the current config
|
||||
self._current_config.update(new_config)
|
||||
|
||||
|
||||
@@ -178,13 +178,9 @@ class ExpandableNotificationGroup(Box):
|
||||
|
||||
self.add(self.collapsed_eventbox)
|
||||
|
||||
# Create expanded state (hidden initially)
|
||||
self.create_expanded_state()
|
||||
|
||||
def _get_notification_pixbuf_for_group(self, cached_notification):
|
||||
"""Get notification pixbuf at 64x64 - matches app icon size for consistency"""
|
||||
notification = cached_notification._notification
|
||||
getattr(notification, "id", None)
|
||||
pixbuf = None
|
||||
|
||||
# First try to get cached notification image using stored cache key
|
||||
@@ -372,10 +368,6 @@ class ExpandableNotificationGroup(Box):
|
||||
self.expanded_container.set_visible(False)
|
||||
return False # Don't repeat timeout
|
||||
|
||||
def _complete_collapse(self):
|
||||
"""Complete the collapse animation - no longer needed but kept for compatibility"""
|
||||
return False # Don't repeat timeout
|
||||
|
||||
def close_all(self, *args):
|
||||
"""Close all notifications in this group with proper cache cleanup"""
|
||||
# Close all notifications in this group
|
||||
@@ -462,7 +454,6 @@ class NotificationCenterWidget(NotificationWidget):
|
||||
|
||||
def _get_notification_pixbuf(self, notification):
|
||||
"""Get notification pixbuf at 64x64 - matches app icon size for consistency"""
|
||||
getattr(notification, "id", None)
|
||||
pixbuf = None
|
||||
|
||||
# First try to get cached notification image using stored cache key
|
||||
@@ -819,8 +810,11 @@ class NotificationCenter(AppletWindow):
|
||||
group_widget.remove(child)
|
||||
child.destroy()
|
||||
|
||||
# Recreate content
|
||||
# Recreate content (collapsed + expanded states)
|
||||
group_widget.create_collapsed_state()
|
||||
group_widget.create_expanded_state()
|
||||
group_widget.collapsed_eventbox.set_visible(True)
|
||||
group_widget.expanded_container.set_visible(False)
|
||||
group_widget.show_all()
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -69,12 +69,6 @@ class Panel(Window):
|
||||
self.tray.connect("add", self._update_tray_visibility)
|
||||
self.tray.connect("remove", self._update_tray_visibility)
|
||||
|
||||
self.indicators = Box(
|
||||
name="indicators",
|
||||
orientation="h",
|
||||
spacing=4,
|
||||
)
|
||||
|
||||
self._spotlight_toggle_cb = None
|
||||
self.search = Button(
|
||||
name="panel-button",
|
||||
|
||||
@@ -334,9 +334,24 @@ class MicrophoneIndicator(Box):
|
||||
self._connect_mic_signals()
|
||||
|
||||
def _connect_mic_signals(self):
|
||||
if self._audio.microphone:
|
||||
self._audio.microphone.connect("changed", self._on_mic_stream_changed)
|
||||
self._sync_state()
|
||||
mic = self._audio.microphone
|
||||
if mic is None:
|
||||
return
|
||||
# Disconnect from a previously-connected device if it changed, and
|
||||
# avoid stacking duplicate handlers on the same device.
|
||||
old = getattr(self, "_mic_device", None)
|
||||
if old is not None and old is not mic:
|
||||
try:
|
||||
old.disconnect_by_func(self._on_mic_stream_changed)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
mic.disconnect_by_func(self._on_mic_stream_changed)
|
||||
except Exception:
|
||||
pass
|
||||
mic.connect("changed", self._on_mic_stream_changed)
|
||||
self._mic_device = mic
|
||||
self._sync_state()
|
||||
|
||||
def _on_mic_device_changed(self, *_):
|
||||
self._connect_mic_signals()
|
||||
@@ -371,8 +386,13 @@ class MicrophoneIndicator(Box):
|
||||
self._hide_timer_id = 0
|
||||
try:
|
||||
self._audio.disconnect_by_func(self._on_mic_device_changed)
|
||||
if self._audio.microphone:
|
||||
self._audio.microphone.disconnect_by_func(self._on_mic_stream_changed)
|
||||
except Exception as e:
|
||||
logger.warning(f"[NotchIndicators] mic disconnect: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
old = getattr(self, "_mic_device", None)
|
||||
target = old if old is not None else self._audio.microphone
|
||||
if target is not None:
|
||||
try:
|
||||
target.disconnect_by_func(self._on_mic_stream_changed)
|
||||
except Exception as e:
|
||||
logger.warning(f"[NotchIndicators] mic disconnect: {e}")
|
||||
super().destroy()
|
||||
@@ -188,6 +188,12 @@ class Notch(Box):
|
||||
def _on_player_vanish(self, _manager, name: str):
|
||||
if name in self._music_services:
|
||||
self._music_services.remove(name)
|
||||
try:
|
||||
svc = self._mpris.get_player_service(name)
|
||||
if svc is not None:
|
||||
svc.disconnect_by_func(self._on_any_artwork)
|
||||
except Exception:
|
||||
pass
|
||||
self._apply_stack_state()
|
||||
|
||||
# ── Scroll ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -257,6 +257,19 @@ class ScreenCaptureWindow(Window):
|
||||
|
||||
_handler_id[0] = screen_capture_service.connect("screenshot-taken", _on_taken)
|
||||
|
||||
def _safe_disconnect():
|
||||
if _handler_id[0] is not None:
|
||||
try:
|
||||
screen_capture_service.disconnect(_handler_id[0])
|
||||
except Exception:
|
||||
pass
|
||||
_handler_id[0] = None
|
||||
return False
|
||||
|
||||
# Safety net: disconnect if the service never emits (e.g. capture failed
|
||||
# silently), otherwise the one-shot handler would leak on every shot.
|
||||
GLib.timeout_add_seconds(10, _safe_disconnect)
|
||||
|
||||
ok = screen_capture_service.screenshot(
|
||||
target,
|
||||
output_dir=output_dir,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
@@ -612,13 +612,6 @@ class ApplicationSwitcher(Window):
|
||||
if addr in self._addr_to_idx:
|
||||
self._capture.capture_by_handle(addr, 300, 168, False)
|
||||
|
||||
def _refill_and_pump(self):
|
||||
# Called when the switcher becomes visible or windows change
|
||||
if not self.get_visible():
|
||||
return False
|
||||
self._pump_capture_queue()
|
||||
return False
|
||||
|
||||
def grab_keyboard(self):
|
||||
try:
|
||||
display = Gdk.Display.get_default()
|
||||
|
||||
Reference in new issue
Block a user