import subprocess import urllib.parse from typing import List from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result class WebSearchPlugin(PluginBase): """ Web search plugin that supports multiple search engines. """ def __init__(self): super().__init__() self.display_name = "Web Search" self.description = "Search the web using various search engines" self.current_trigger = "" # Track the current active trigger # Define search engines with their URLs and icons self.search_engines = { "google": { "name": "Google", "url": "https://www.google.com/search?q={}", "icon": "google-symbolic", "description": "Search with Google", }, "duckduckgo": { "name": "DuckDuckGo", "url": "https://duckduckgo.com/?q={}", "icon": "preferences-desktop-search", "description": "Search with DuckDuckGo (privacy-focused)", }, "youtube": { "name": "YouTube", "url": "https://www.youtube.com/results?search_query={}", "icon": "youtube-symbolic", "description": "Search videos on YouTube", }, "github": { "name": "GitHub", "url": "https://github.com/search?q={}", "icon": "github-desktop-symbolic", "description": "Search repositories on GitHub", }, "stackoverflow": { "name": "Stack Overflow", "url": "https://stackoverflow.com/search?q={}", "icon": "stack", "description": "Search programming questions on Stack Overflow", }, "wikipedia": { "name": "Wikipedia", "url": "https://en.wikipedia.org/wiki/Special:Search?search={}", "icon": "search-menus-symbolic", "description": "Search articles on Wikipedia", }, "reddit": { "name": "Reddit", "url": "https://www.reddit.com/search/?q={}", "icon": "reddit-symbolic", "description": "Search discussions on Reddit", }, "linkedin": { "name": "LinkedIn", "url": "https://www.linkedin.com/search/results/all/?keywords={}", "icon": "link", "description": "Search professionals and jobs on LinkedIn", }, } # Default search engine self.default_engine = "google" def initialize(self): """Initialize the web search plugin.""" # Set up triggers for web search - only main triggers, not individual search engines triggers = ["?"] # Don't add individual search engine triggers to avoid cluttering trigger keywords # Search engines can still be used within web search context (e.g., "web google cats") self.set_triggers(triggers) def get_active_trigger(self, query_string: str) -> str: """Override to track which trigger was activated.""" trigger = super().get_active_trigger(query_string) if trigger: self.current_trigger = trigger.strip() return trigger def cleanup(self): """Cleanup the web search plugin.""" pass def query(self, query_string: str) -> List[Result]: """Process web search queries.""" results = [] if not query_string.strip(): # Show available search engines when no query return self._get_search_engine_list() # Check if the query is a URL if self._is_url(query_string): results.append(self._create_url_result(query_string)) return results # Parse query to check if it starts with a search engine name engine_name, search_query = self._parse_engine_query(query_string) if engine_name and engine_name in self.search_engines: # Specific search engine specified in query (e.g., "google cats") if search_query: # Search with specific engine results.append(self._create_search_result(engine_name, search_query)) else: # Show specific engine info results.append(self._create_engine_info_result(engine_name)) else: # General search - offer multiple engines search_query = query_string.strip() # Add default search engine first results.append( self._create_search_result(self.default_engine, search_query) ) # Add other popular search engines popular_engines = ["duckduckgo", "youtube", "github"] for engine in popular_engines: if engine != self.default_engine: results.append(self._create_search_result(engine, search_query)) return results def _parse_engine_query(self, query: str) -> tuple[str, str]: """Parse query to extract search engine and search terms.""" query = query.strip().lower() for engine in self.search_engines.keys(): if query.startswith(f"{engine} "): return engine, query[len(engine) :].strip() elif query == engine: return engine, "" return "", query def _is_url(self, text: str) -> bool: """Check if the text is a URL.""" text = text.strip().lower() return text.startswith(("http://", "https://", "www.")) or ( "." in text and " " not in text and len(text) > 3 ) def _create_url_result(self, url: str) -> Result: """Create a result for opening a URL directly.""" # Add protocol if missing if not url.startswith(("http://", "https://")): if url.startswith("www."): url = "https://" + url else: url = "https://" + url return Result( title=f"Open {url}", subtitle="Open this URL in your default browser", icon_name="link", action=lambda u=url: self._open_url(u), relevance=1.0, plugin_name=self.display_name, data={"type": "url", "url": url}, ) def _open_url(self, url: str): """Open a URL directly in the default browser.""" try: subprocess.Popen( ["xdg-open", url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) except Exception as e: print(f"Failed to open URL: {e}") def _get_search_engine_list(self) -> List[Result]: """Get list of available search engines.""" results = [] for engine_id, engine_info in self.search_engines.items(): result = Result( title=engine_info["name"], subtitle=engine_info["description"], icon_name=engine_info["icon"], action=lambda e=engine_id: self._show_engine_help(e), relevance=1.0 if engine_id == self.default_engine else 0.8, plugin_name=self.display_name, data={"type": "engine_info", "engine": engine_id}, ) results.append(result) return results def _create_search_result(self, engine_id: str, query: str) -> Result: """Create a search result for a specific engine and query.""" engine_info = self.search_engines[engine_id] return Result( title=f"Search '{query}' on {engine_info['name']}", subtitle=f"{engine_info['description']} - {query}", icon_name=engine_info["icon"], action=lambda e=engine_id, q=query: self._perform_search(e, q), relevance=1.0 if engine_id == self.default_engine else 0.9, plugin_name=self.display_name, data={"type": "search", "engine": engine_id, "query": query}, ) def _create_engine_info_result(self, engine_id: str) -> Result: """Create an info result for a specific search engine.""" engine_info = self.search_engines[engine_id] return Result( title=f"{engine_info['name']} Search", subtitle=f"{engine_info['description']} - Type your search query", icon_name=engine_info["icon"], action=lambda: None, # No action for info result relevance=1.0, plugin_name=self.display_name, data={"type": "engine_ready", "engine": engine_id}, ) def _perform_search(self, engine_id: str, query: str): """Perform a web search using the specified engine.""" if not query.strip(): return engine_info = self.search_engines.get(engine_id) if not engine_info: return # URL encode the search query encoded_query = urllib.parse.quote_plus(query) search_url = engine_info["url"].format(encoded_query) try: # Open the search URL in the default browser subprocess.Popen( ["xdg-open", search_url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) except Exception as e: print(f"Failed to open search URL: {e}") def _show_engine_help(self, engine_id: str): """Show help for a specific search engine.""" engine_info = self.search_engines.get(engine_id) if engine_info: print( f"Search engine: {engine_info['name']} - {engine_info['description']}" )