""" Clipboard plugin using cliphist for clipboard history management. """ import os import subprocess import sys import tempfile from typing import List, Dict from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result from gi.repository import GdkPixbuf, GLib class ClipboardPlugin(PluginBase): def __init__(self): super().__init__() self.name = "clipboard" self.display_name = "Clipboard History" self.description = "Search and manage clipboard history using cliphist" # Initialize cache and temp directory self.tmp_dir = tempfile.mkdtemp(prefix="cliphist-") self.image_cache: Dict[str, GdkPixbuf.Pixbuf] = {} self.clipboard_items = [] self._loading = False self._pending_updates = False def initialize(self): self.set_triggers(["clip", "clip "]) """Initialize the plugin.""" try: subprocess.run(["cliphist", "list"], capture_output=True, check=True) except (subprocess.SubprocessError, FileNotFoundError): raise RuntimeError("cliphist is not installed or not working properly") def cleanup(self): """Cleanup the plugin.""" try: if os.path.exists(self.tmp_dir): import shutil shutil.rmtree(self.tmp_dir) self.image_cache.clear() except Exception as e: print(f"Error cleaning up temporary files: {e}", file=sys.stderr) def _load_clipboard_items(self) -> List[str]: """Load clipboard items from cliphist.""" try: result = subprocess.run( ["cliphist", "list"], capture_output=True, check=True ) stdout_str = result.stdout.decode("utf-8", errors="replace") lines = stdout_str.strip().split("\n") return [line for line in lines if line and " GdkPixbuf.Pixbuf: """Create a GdkPixbuf from image bytes with size limit.""" try: loader = GdkPixbuf.PixbufLoader() loader.write(image_data) loader.close() pixbuf = loader.get_pixbuf() # Scale image if needed width, height = pixbuf.get_width(), pixbuf.get_height() if width > height: new_width = max_size new_height = int(height * (max_size / width)) else: new_height = max_size new_width = int(width * (max_size / height)) return pixbuf.scale_simple( new_width, new_height, GdkPixbuf.InterpType.BILINEAR ) except GLib.Error: return None def _is_image_data(self, content: str) -> bool: """Determine if clipboard content is likely an image.""" return "binary" in content.lower() and any( ext in content.lower() for ext in ["jpg", "jpeg", "png", "bmp", "gif"] ) def _get_text_preview(self, content: str) -> str: """Get a text preview of the content.""" if len(content) > 50: return content[:37] + "..." return content def _load_image_preview_async(self, item_id: str) -> GdkPixbuf.Pixbuf: """Load image preview asynchronously.""" if item_id in self.image_cache: return self.image_cache[item_id] try: result = subprocess.run( ["cliphist", "decode", item_id], capture_output=True, check=True ) pixbuf = self._create_pixbuf_from_bytes(result.stdout) if pixbuf: self.image_cache[item_id] = pixbuf return pixbuf except Exception as e: print(f"Error loading image preview: {e}", file=sys.stderr) return None def query(self, query_string: str) -> List[Result]: """Search clipboard history using cliphist.""" results = [] # Handle query string if query_string.lower() == "clip": query_string = "" # Show all items try: # Load clipboard items self.clipboard_items = self._load_clipboard_items() # Filter items based on query filtered_items = [] for item in self.clipboard_items: parts = item.split("\t", 1) content = parts[1] if len(parts) > 1 else item if not query_string or query_string.lower() in content.lower(): filtered_items.append(item) # Process all items without batching for item in filtered_items: parts = item.split("\t", 1) item_id = parts[0] if len(parts) > 1 else "0" content = parts[1] if len(parts) > 1 else item # Handle image content if self._is_image_data(content): pixbuf = self._load_image_preview_async(item_id) if pixbuf: result = Result( title="Image from clipboard", subtitle="Click to copy image to clipboard", description="Image content", icon=pixbuf, relevance=1.0, plugin_name=self.name, action=lambda id=item_id: self._copy_to_clipboard(id), data={"bypass_max_results": True}, ) results.append(result) continue # Handle text content display_text = self._get_text_preview(content) result = Result( title=display_text, subtitle="Text from clipboard", description=content, icon_name="edit-paste", relevance=1.0, plugin_name=self.name, action=lambda id=item_id: self._copy_to_clipboard(id), data={"bypass_max_results": True}, ) results.append(result) except Exception as e: # Handle errors gracefully results.append( Result( title="Error accessing clipboard history", subtitle=str(e), icon_name="dialog-error", relevance=0.0, plugin_name=self.name, data={"bypass_max_results": True}, ) ) return results def _copy_to_clipboard(self, entry_id: str): """Copy entry to clipboard using cliphist.""" try: result = subprocess.run( ["cliphist", "decode", entry_id], capture_output=True, check=True ) # Use wl-copy for Wayland or xclip for X11 try: subprocess.run(["wl-copy"], input=result.stdout, check=True) except subprocess.SubprocessError: subprocess.run( ["xclip", "-selection", "clipboard"], input=result.stdout, check=True, ) except subprocess.SubprocessError as e: print(f"Error copying to clipboard: {e}", file=sys.stderr)