refactor(launcher): remove launcher module files
This commit is contained in:
7 files changed
-2038
No files matched your search
@@ -1,8 +0,0 @@
|
||||
"""
|
||||
Plugin-based launcher module for Fabric.
|
||||
Similar to Albert Launcher with extensible plugin system.
|
||||
"""
|
||||
|
||||
from .main import Launcher
|
||||
|
||||
__all__ = ["Launcher"]
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,172 +0,0 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
from window.launcher.result import Result
|
||||
|
||||
|
||||
class PluginBase(ABC):
|
||||
"""
|
||||
Abstract base class for launcher plugins.
|
||||
All plugins must inherit from this class.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.name = self.__class__.__name__.lower()
|
||||
self.display_name = self.__class__.__name__
|
||||
self.description = "A launcher plugin"
|
||||
self.version = "1.0.0"
|
||||
self.enabled = True
|
||||
self._triggers = [] # List of trigger keywords
|
||||
|
||||
@abstractmethod
|
||||
def initialize(self):
|
||||
"""
|
||||
Initialize the plugin.
|
||||
Called when the plugin is activated.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cleanup(self):
|
||||
"""
|
||||
Cleanup the plugin.
|
||||
Called when the plugin is deactivated.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def query(self, query_string: str) -> List[Result]:
|
||||
"""
|
||||
Process a search query and return results.
|
||||
|
||||
Args:
|
||||
query_string: The search query from the user
|
||||
|
||||
Returns:
|
||||
List of Result objects
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_triggers(self) -> List[str]:
|
||||
"""
|
||||
Get list of trigger keywords for this plugin.
|
||||
If the query starts with any of these, this plugin gets priority.
|
||||
|
||||
Returns:
|
||||
List of trigger strings (e.g., ["calc", "=", "math"])
|
||||
"""
|
||||
return self._triggers
|
||||
|
||||
def set_triggers(self, triggers: List[str]):
|
||||
"""
|
||||
Set the trigger keywords for this plugin.
|
||||
|
||||
Args:
|
||||
triggers: List of trigger keywords
|
||||
"""
|
||||
self._triggers = triggers
|
||||
|
||||
def handles_query(self, query_string: str) -> bool:
|
||||
"""
|
||||
Check if this plugin should handle the given query.
|
||||
|
||||
Args:
|
||||
query_string: The search query
|
||||
|
||||
Returns:
|
||||
True if this plugin should process the query
|
||||
"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
# Check triggers
|
||||
triggers = self.get_triggers()
|
||||
if triggers:
|
||||
query_lower = query_string.lower().strip()
|
||||
return any(query_lower.startswith(trigger.lower()) for trigger in triggers)
|
||||
|
||||
# Default: handle all queries
|
||||
return True
|
||||
|
||||
def get_active_trigger(self, query_string: str) -> str:
|
||||
"""
|
||||
Get the active trigger for the given query.
|
||||
|
||||
Args:
|
||||
query_string: The search query
|
||||
|
||||
Returns:
|
||||
The trigger keyword if found, empty string otherwise
|
||||
"""
|
||||
if not self.enabled:
|
||||
return ""
|
||||
|
||||
triggers = self.get_triggers()
|
||||
if triggers:
|
||||
query_lower = query_string.lower().strip()
|
||||
|
||||
# Sort triggers by length (longest first) to match more specific triggers first
|
||||
sorted_triggers = sorted(triggers, key=len, reverse=True)
|
||||
|
||||
for trigger in sorted_triggers:
|
||||
trigger_lower = trigger.lower()
|
||||
|
||||
# Exact match with trigger (including space if present)
|
||||
if query_lower.startswith(trigger_lower):
|
||||
return trigger
|
||||
|
||||
# Match trigger word followed by space
|
||||
trigger_word = trigger.strip().lower()
|
||||
if (
|
||||
query_lower.startswith(trigger_word + " ")
|
||||
or query_lower == trigger_word
|
||||
):
|
||||
return trigger
|
||||
|
||||
return ""
|
||||
|
||||
def query_triggered(self, query_string: str, trigger: str) -> List[Result]:
|
||||
"""
|
||||
Process a triggered query (when plugin is in sticky mode).
|
||||
Default implementation removes trigger and calls query().
|
||||
|
||||
Args:
|
||||
query_string: The full query string including trigger
|
||||
trigger: The trigger that activated this plugin
|
||||
|
||||
Returns:
|
||||
List of Result objects
|
||||
"""
|
||||
# Remove trigger from query and process remaining text
|
||||
remaining_query = query_string[len(trigger) :].strip()
|
||||
return self.query(remaining_query)
|
||||
|
||||
def get_config(self) -> dict:
|
||||
"""
|
||||
Get plugin configuration.
|
||||
|
||||
Returns:
|
||||
Dictionary of configuration options
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"display_name": self.display_name,
|
||||
"description": self.description,
|
||||
"version": self.version,
|
||||
"enabled": self.enabled,
|
||||
}
|
||||
|
||||
def set_config(self, config: dict):
|
||||
"""
|
||||
Set plugin configuration.
|
||||
|
||||
Args:
|
||||
config: Dictionary of configuration options
|
||||
"""
|
||||
self.enabled = config.get("enabled", self.enabled)
|
||||
|
||||
def __str__(self):
|
||||
return f"Plugin({self.name})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
@@ -1,177 +0,0 @@
|
||||
from fabric.utils import logger
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
from typing import Dict, List, Type
|
||||
|
||||
from window.launcher.plugin_base import PluginBase
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""
|
||||
Manages launcher plugins.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.plugins: Dict[str, PluginBase] = {}
|
||||
self.plugin_classes: Dict[str, Type[PluginBase]] = {}
|
||||
self.active_plugins: List[str] = []
|
||||
|
||||
# Load built-in plugins
|
||||
self._load_builtin_plugins()
|
||||
|
||||
# Load external plugins
|
||||
self._load_external_plugins()
|
||||
|
||||
# Activate default plugins
|
||||
self._activate_default_plugins()
|
||||
|
||||
def _load_builtin_plugins(self):
|
||||
"""Load built-in plugins from the plugins directory."""
|
||||
plugins_dir = os.path.join(os.path.dirname(__file__), "plugins")
|
||||
|
||||
if not os.path.exists(plugins_dir):
|
||||
return
|
||||
|
||||
for filename in os.listdir(plugins_dir):
|
||||
if filename.endswith(".py") and not filename.startswith("_"):
|
||||
plugin_name = filename[:-3] # Remove .py extension
|
||||
self._load_plugin_from_file(plugins_dir, plugin_name)
|
||||
|
||||
def _load_external_plugins(self):
|
||||
"""Load external plugins from user directory."""
|
||||
# Could be implemented to load from ~/.config/launcher/plugins/
|
||||
pass
|
||||
|
||||
def _load_plugin_from_file(self, plugins_dir: str, plugin_name: str):
|
||||
"""Load a plugin from a Python file."""
|
||||
try:
|
||||
plugin_path = os.path.join(plugins_dir, f"{plugin_name}.py")
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
f"window.launcher.plugins.{plugin_name}", plugin_path
|
||||
)
|
||||
|
||||
if spec and spec.loader:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
|
||||
# Add the module to sys.window to support relative imports
|
||||
import sys
|
||||
|
||||
sys.modules[f"window.launcher.plugins.{plugin_name}"] = module
|
||||
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# Look for plugin class
|
||||
for attr_name in dir(module):
|
||||
attr = getattr(module, attr_name)
|
||||
if (
|
||||
isinstance(attr, type)
|
||||
and issubclass(attr, PluginBase)
|
||||
and attr != PluginBase
|
||||
):
|
||||
self.plugin_classes[plugin_name] = attr
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load plugin {plugin_name}: {e}")
|
||||
|
||||
def _activate_default_plugins(self):
|
||||
"""Activate default plugins."""
|
||||
default_plugins = [
|
||||
"applications",
|
||||
"calculator",
|
||||
"clipboard",
|
||||
"power",
|
||||
"caffeine",
|
||||
"screencapture",
|
||||
"emoji",
|
||||
"wallpaper",
|
||||
"websearch",
|
||||
]
|
||||
|
||||
for plugin_name in default_plugins:
|
||||
self.activate_plugin(plugin_name)
|
||||
|
||||
def activate_plugin(self, plugin_name: str) -> bool:
|
||||
"""Activate a plugin by name."""
|
||||
if plugin_name in self.plugins:
|
||||
# Already activated
|
||||
return True
|
||||
|
||||
if plugin_name not in self.plugin_classes:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Instantiate plugin
|
||||
plugin_class = self.plugin_classes[plugin_name]
|
||||
plugin_instance = plugin_class()
|
||||
|
||||
# Initialize plugin
|
||||
plugin_instance.initialize()
|
||||
|
||||
# Store plugin
|
||||
self.plugins[plugin_name] = plugin_instance
|
||||
self.active_plugins.append(plugin_name)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to activate plugin {plugin_name}: {e}")
|
||||
return False
|
||||
|
||||
def deactivate_plugin(self, plugin_name: str) -> bool:
|
||||
"""Deactivate a plugin by name."""
|
||||
if plugin_name not in self.plugins:
|
||||
return False
|
||||
|
||||
try:
|
||||
# Cleanup plugin
|
||||
plugin = self.plugins[plugin_name]
|
||||
plugin.cleanup()
|
||||
|
||||
# Remove from active plugins
|
||||
del self.plugins[plugin_name]
|
||||
if plugin_name in self.active_plugins:
|
||||
self.active_plugins.remove(plugin_name)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to deactivate plugin {plugin_name}: {e}")
|
||||
return False
|
||||
|
||||
def get_active_plugins(self) -> List[PluginBase]:
|
||||
"""Get list of active plugin instances."""
|
||||
return [
|
||||
self.plugins[name] for name in self.active_plugins if name in self.plugins
|
||||
]
|
||||
|
||||
def get_plugin_names(self) -> List[str]:
|
||||
"""Get list of available plugin names."""
|
||||
return list(self.plugin_classes.keys())
|
||||
|
||||
def get_active_plugin_names(self) -> List[str]:
|
||||
"""Get list of active plugin names."""
|
||||
return self.active_plugins.copy()
|
||||
|
||||
def reload_plugin(self, plugin_name: str) -> bool:
|
||||
"""Reload a plugin."""
|
||||
was_active = plugin_name in self.active_plugins
|
||||
|
||||
# Deactivate if active
|
||||
if was_active:
|
||||
self.deactivate_plugin(plugin_name)
|
||||
|
||||
# Remove from classes
|
||||
if plugin_name in self.plugin_classes:
|
||||
del self.plugin_classes[plugin_name]
|
||||
|
||||
# Reload from file
|
||||
plugins_dir = os.path.join(os.path.dirname(__file__), "plugins")
|
||||
self._load_plugin_from_file(plugins_dir, plugin_name)
|
||||
|
||||
# Reactivate if it was active
|
||||
if was_active and plugin_name in self.plugin_classes:
|
||||
return self.activate_plugin(plugin_name)
|
||||
|
||||
return plugin_name in self.plugin_classes
|
||||
@@ -1,52 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from fabric.utils import GdkPixbuf, Gtk
|
||||
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
"""
|
||||
Represents a search result that can be displayed and activated.
|
||||
"""
|
||||
|
||||
# Display information
|
||||
title: str
|
||||
subtitle: str = ""
|
||||
subtitle_markup: Optional[str] = None # Pango markup for subtitle
|
||||
description: str = ""
|
||||
icon: Optional[GdkPixbuf.Pixbuf] = None
|
||||
icon_name: Optional[str] = None
|
||||
icon_markup: Optional[str] = None
|
||||
# Behavior
|
||||
action: Optional[Callable[[], Any]] = None
|
||||
relevance: float = 0.0
|
||||
|
||||
# Custom widget support
|
||||
custom_widget: Optional[Gtk.Widget] = None
|
||||
|
||||
# Metadata
|
||||
plugin_name: str = ""
|
||||
data: Optional[dict] = None
|
||||
|
||||
def activate(self):
|
||||
"""Activate this result (execute its action)."""
|
||||
if self.action:
|
||||
return self.action()
|
||||
else:
|
||||
raise NotImplementedError("No action defined for this result")
|
||||
|
||||
def __post_init__(self):
|
||||
"""Post-initialization processing."""
|
||||
# Ensure relevance is within valid range
|
||||
self.relevance = max(0.0, min(1.0, self.relevance))
|
||||
|
||||
# Set default data if None
|
||||
if self.data is None:
|
||||
self.data = {}
|
||||
|
||||
def __str__(self):
|
||||
return f"Result(title='{self.title}', relevance={self.relevance})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
@@ -1,161 +0,0 @@
|
||||
from fabric.core.service import Signal
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.eventbox import EventBox
|
||||
from fabric.widgets.image import Image
|
||||
from fabric.widgets.label import Label
|
||||
|
||||
from window.launcher.result import Result
|
||||
|
||||
|
||||
class ResultItem(EventBox):
|
||||
"""
|
||||
Widget for displaying a single search result.
|
||||
"""
|
||||
|
||||
# Signals
|
||||
@Signal
|
||||
def clicked(self, index: int) -> None:
|
||||
"""Emitted when result is clicked."""
|
||||
pass
|
||||
|
||||
@Signal
|
||||
def hovered(self, index: int) -> None:
|
||||
"""Emitted when result is hovered."""
|
||||
pass
|
||||
|
||||
def __init__(
|
||||
self, result: Result, selected: bool = False, index: int = 0, **kwargs
|
||||
):
|
||||
super().__init__(name="launcher-result-item", **kwargs)
|
||||
|
||||
self.result = result
|
||||
self._selected = selected
|
||||
self.index = index
|
||||
|
||||
# Setup UI
|
||||
self._setup_ui()
|
||||
|
||||
# Connect signals
|
||||
self.connect("button-press-event", self._on_button_press)
|
||||
self.connect("enter-notify-event", self._on_enter)
|
||||
self.connect("leave-notify-event", self._on_leave)
|
||||
|
||||
# Set initial selection state
|
||||
self.set_selected(selected)
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup the result item UI."""
|
||||
# Main container
|
||||
main_box = Box(
|
||||
name="result-item-main",
|
||||
orientation="h",
|
||||
spacing=12,
|
||||
h_align="fill",
|
||||
v_align="center",
|
||||
)
|
||||
self.add(main_box)
|
||||
|
||||
# Icon
|
||||
if self.result.icon:
|
||||
icon_widget = Image(pixbuf=self.result.icon, name="result-item-icon")
|
||||
elif self.result.icon_name:
|
||||
icon_widget = Image(
|
||||
icon_name=self.result.icon_name, icon_size=48, name="result-item-icon"
|
||||
)
|
||||
elif self.result.icon_markup:
|
||||
icon_widget = Label(
|
||||
markup=self.result.icon_markup, name="launcher-icon-label"
|
||||
)
|
||||
else:
|
||||
# Default icon
|
||||
icon_widget = Image(
|
||||
icon_name="application-default-icon",
|
||||
icon_size=48,
|
||||
name="result-item-icon",
|
||||
)
|
||||
|
||||
main_box.add(icon_widget)
|
||||
|
||||
# Text container
|
||||
text_box = Box(
|
||||
name="result-item-text",
|
||||
orientation="v",
|
||||
spacing=2,
|
||||
h_expand=True,
|
||||
v_align="center",
|
||||
)
|
||||
main_box.add(text_box)
|
||||
|
||||
# Title
|
||||
title_label = Label(
|
||||
label=self.result.title,
|
||||
name="result-item-title",
|
||||
h_align="start",
|
||||
v_align="center",
|
||||
ellipsize="end",
|
||||
)
|
||||
text_box.add(title_label)
|
||||
|
||||
# Subtitle (if present)
|
||||
if self.result.subtitle or self.result.subtitle_markup:
|
||||
if self.result.subtitle_markup:
|
||||
# Use markup for subtitle (supports Pango markup)
|
||||
subtitle_label = Label(
|
||||
markup=self.result.subtitle_markup,
|
||||
name="result-item-subtitle",
|
||||
h_align="start",
|
||||
v_align="center",
|
||||
ellipsize="end",
|
||||
)
|
||||
else:
|
||||
# Use plain text for subtitle
|
||||
subtitle_label = Label(
|
||||
label=self.result.subtitle,
|
||||
name="result-item-subtitle",
|
||||
h_align="start",
|
||||
v_align="center",
|
||||
ellipsize="end",
|
||||
)
|
||||
text_box.add(subtitle_label)
|
||||
|
||||
# Plugin name (small text)
|
||||
if self.result.plugin_name:
|
||||
plugin_label = Label(
|
||||
label=f"via {self.result.plugin_name}",
|
||||
name="result-item-plugin",
|
||||
h_align="start",
|
||||
v_align="center",
|
||||
ellipsize="end",
|
||||
)
|
||||
text_box.add(plugin_label)
|
||||
|
||||
def set_selected(self, selected: bool):
|
||||
"""Set the selection state of this result item."""
|
||||
self._selected = selected
|
||||
|
||||
if selected:
|
||||
self.add_style_class("selected")
|
||||
else:
|
||||
self.remove_style_class("selected")
|
||||
|
||||
def get_selected(self) -> bool:
|
||||
"""Get the selection state of this result item."""
|
||||
return self._selected
|
||||
|
||||
def _on_button_press(self, widget, event):
|
||||
"""Handle button press events."""
|
||||
if event.button == 1: # Left click
|
||||
self.clicked.emit(self.index)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _on_enter(self, widget, event):
|
||||
"""Handle mouse enter events."""
|
||||
# Emit hover signal to update selection
|
||||
self.hovered.emit(self.index)
|
||||
return False
|
||||
|
||||
def _on_leave(self, widget, event):
|
||||
"""Handle mouse leave events."""
|
||||
# Could be used for hover effects cleanup
|
||||
return False
|
||||
@@ -1,60 +0,0 @@
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import tomlkit
|
||||
from utils.utils import toml_file
|
||||
|
||||
|
||||
class TriggerConfig:
|
||||
def __init__(self, config_path: str = None):
|
||||
if config_path is None:
|
||||
config_path = toml_file("launcher.toml")
|
||||
|
||||
self.config_path = config_path
|
||||
|
||||
config = {"launcher_config": {}, "settings": {}}
|
||||
try:
|
||||
with open(config_path, "r") as f:
|
||||
data = tomlkit.load(f)
|
||||
config["launcher_config"] = dict(data.get("launcher_config", {}))
|
||||
config["settings"] = dict(data.get("settings", {}))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception as e:
|
||||
from fabric.utils import logger
|
||||
|
||||
logger.error(f"Error loading trigger config: {e}")
|
||||
|
||||
self.config = config
|
||||
self.launcher_config = self.config.get("launcher_config", {})
|
||||
|
||||
# Initialize settings with defaults
|
||||
default_settings = {
|
||||
"max_examples_shown": 2,
|
||||
"default_icon": "application-default-icon",
|
||||
"fallback_example_template": "{trigger} <search>",
|
||||
"config_version": "1.0",
|
||||
}
|
||||
self.settings = {**default_settings, **self.config.get("settings", {})}
|
||||
|
||||
def get_trigger_examples(self, trigger: str) -> List[str]:
|
||||
examples = self.launcher_config.get(trigger, {}).get("examples", [])
|
||||
if not examples:
|
||||
template = self.settings.get(
|
||||
"fallback_example_template", "{trigger} <search>"
|
||||
)
|
||||
examples = [template.format(trigger=trigger)]
|
||||
return examples
|
||||
|
||||
def get_trigger_icon(self, trigger: str) -> str:
|
||||
icon = self.launcher_config.get(trigger, {}).get(
|
||||
"icon", self.settings.get("default_icon", "application-default-icon")
|
||||
)
|
||||
return icon
|
||||
|
||||
def get_trigger_description(self, trigger: str) -> str:
|
||||
return self.launcher_config.get(trigger, {}).get(
|
||||
"description", f"{trigger} - No description available"
|
||||
)
|
||||
|
||||
def get_all_triggers(self) -> Dict[str, Dict[str, Any]]:
|
||||
return self.launcher_config.copy()
|
||||
Reference in new issue
Block a user