From f5625ef1e48125760ef6126bf7387fde0e635764 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 20 Jul 2025 22:15:38 +0545 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=20=20refactor(clipboard):=20enhance?= =?UTF-8?q?=20clipboard=20plugin=20with=20forever=20image=20caching=20and?= =?UTF-8?q?=20improved=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example_cliphist.py | 505 --------------------------------------- test_clipboard_search.py | 69 ------ test_image_cache.py | 81 ------- test_mouse_navigation.py | 39 --- 4 files changed, 694 deletions(-) delete mode 100644 example_cliphist.py delete mode 100644 test_clipboard_search.py delete mode 100644 test_image_cache.py delete mode 100644 test_mouse_navigation.py diff --git a/example_cliphist.py b/example_cliphist.py deleted file mode 100644 index 8371974..0000000 --- a/example_cliphist.py +++ /dev/null @@ -1,505 +0,0 @@ -import os -import re -import subprocess -import sys -import tempfile - -from fabric.utils import idle_add, remove_handler -from fabric.utils.helpers import get_relative_path -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.entry import Entry -from fabric.widgets.image import Image -from fabric.widgets.label import Label -from fabric.widgets.scrolledwindow import ScrolledWindow -from gi.repository import Gdk, GdkPixbuf, GLib - -import modules.icons as icons - - -class ClipHistory(Box): - def __init__(self, **kwargs): - super().__init__( - name="clip-history", - visible=False, - all_visible=False, - **kwargs, - ) - - self.tmp_dir = tempfile.mkdtemp(prefix="cliphist-") - self.image_cache = {} - - self.notch = kwargs["notch"] - self.selected_index = -1 - self._arranger_handler = 0 - self.clipboard_items = [] - self._loading = False - self._pending_updates = False - - self.viewport = Box(name="viewport", spacing=4, orientation="v") - self.search_entry = Entry( - name="search-entry", - placeholder="Search Clipboard History...", - h_expand=True, - h_align="fill", - notify_text=self.filter_items, - on_activate=lambda entry, *_: self.use_selected_item(), - on_key_press_event=self.on_search_entry_key_press, - ) - self.search_entry.props.xalign = 0.5 - - self.scrolled_window = ScrolledWindow( - name="scrolled-window", - spacing=10, - h_expand=True, - v_expand=True, - h_align="fill", - v_align="fill", - child=self.viewport, - propagate_width=False, - propagate_height=False, - ) - - self.header_box = Box( - name="header_box", - spacing=10, - orientation="h", - children=[ - Button( - name="clear-button", - child=Label(name="clear-label", markup=icons.trash), - on_clicked=lambda *_: self.clear_history(), - ), - self.search_entry, - Button( - name="close-button", - child=Label(name="close-label", markup=icons.cancel), - tooltip_text="Exit", - on_clicked=lambda *_: self.close() - ), - ], - ) - - self.history_box = Box( - name="launcher-box", - spacing=10, - h_expand=True, - orientation="v", - children=[ - self.header_box, - self.scrolled_window, - ], - ) - - self.add(self.history_box) - self.show_all() - - def close(self): - """Close the clipboard history panel""" - self.viewport.children = [] - self.selected_index = -1 - self.notch.close_notch() - - def open(self): - """Open the clipboard history panel and load items""" - if self._loading: - return - self._loading = True - self.search_entry.set_text("") - self.search_entry.grab_focus() - - GLib.idle_add(self._load_clipboard_items_thread) - - def _load_clipboard_items_thread(self): - """Worker for loading clipboard items, now runs in main loop via idle_add""" - try: - result = subprocess.run( - ["cliphist", "list"], - capture_output=True, - check=True - ) - # Decode stdout with error handling - stdout_str = result.stdout.decode('utf-8', errors='replace') - lines = stdout_str.strip().split('\n') - new_items = [] - for line in lines: - if not line or " 1 else "0" - content = parts[1] if len(parts) > 1 else item - - - display_text = content.strip() - if len(display_text) > 100: - display_text = display_text[:97] + "..." - - - is_image = self.is_image_data(content) - - if is_image: - - button = Button( - name="slot-button", - child=Box( - name="slot-box", - orientation="h", - spacing=10, - children=[ - Image(name="clip-icon", h_align="start"), - Label( - name="clip-label", - label="[Image]", - ellipsization="end", - v_align="center", - h_align="start", - h_expand=True, - ), - ], - ), - tooltip_text="Image in clipboard", - on_clicked=lambda *_, id=item_id: self.paste_item(id), - ) - - self._load_image_preview_async(item_id, button) - else: - - button = self.create_text_item_button(item_id, display_text) - - - button.connect("key-press-event", lambda widget, event, id=item_id: self.on_item_key_press(widget, event, id)) - - - button.set_can_focus(True) - button.add_events(Gdk.EventMask.KEY_PRESS_MASK) - - return button - - def _load_image_preview_async(self, item_id, button): - """Load image preview using GLib.idle_add instead of threads""" - def load_image(): - try: - if item_id in self.image_cache: - pixbuf = self.image_cache[item_id] - else: - result = subprocess.run( - ["cliphist", "decode", item_id], - capture_output=True, - check=True - ) - loader = GdkPixbuf.PixbufLoader() - loader.write(result.stdout) - loader.close() - pixbuf = loader.get_pixbuf() - width, height = pixbuf.get_width(), pixbuf.get_height() - max_size = 72 - 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)) - pixbuf = pixbuf.scale_simple(new_width, new_height, GdkPixbuf.InterpType.BILINEAR) - self.image_cache[item_id] = pixbuf - self._update_image_button(button, pixbuf) - except Exception as e: - print(f"Error loading image preview: {e}", file=sys.stderr) - return False - GLib.idle_add(load_image) - - def _update_image_button(self, button, pixbuf): - """Update the button with the loaded image preview""" - box = button.get_child() - if box and len(box.get_children()) > 0: - image_widget = box.get_children()[0] - if isinstance(image_widget, Image): - image_widget.set_from_pixbuf(pixbuf) - - def create_text_item_button(self, item_id, display_text): - """Create a button for a text clipboard item""" - return Button( - name="slot-button", - child=Box( - name="slot-box", - orientation="h", - spacing=10, - children=[ - Label( - name="clip-icon", - markup=icons.clip_text, - h_align="start", - ), - Label( - name="clip-label", - label=display_text, - ellipsization="end", - v_align="center", - h_align="start", - h_expand=True, - ), - ], - ), - tooltip_text=display_text, - on_clicked=lambda *_: self.paste_item(item_id), - ) - - def is_image_data(self, content): - """Determine if clipboard content is likely an image""" - - return ( - content.startswith("data:image/") or - content.startswith("\x89PNG") or - content.startswith("GIF8") or - content.startswith("\xff\xd8\xff") or - re.match(r'^\s* visible_bottom: - - new_value = y + height - page_size - adj.set_value(new_value) - return False - GLib.idle_add(scroll) - - def use_selected_item(self): - """Use (paste) the selected clipboard item""" - children = self.viewport.get_children() - if not children or self.selected_index == -1 or self.selected_index >= len(self.clipboard_items): - return - - - item_line = self.clipboard_items[self.selected_index] - item_id = item_line.split('\t', 1)[0] - self.paste_item(item_id) - - def delete_selected_item(self): - """Delete the selected clipboard item""" - children = self.viewport.get_children() - if not children or self.selected_index == -1: - return - - - item_line = self.clipboard_items[self.selected_index] - item_id = item_line.split('\t', 1)[0] - self.delete_item(item_id) - - def on_item_key_press(self, widget, event, item_id): - """Handle key press events on clipboard items""" - if event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): - - self.paste_item(item_id) - return True - return False - - def __del__(self): - """Clean up temporary files on destruction""" - try: - if hasattr(self, 'tmp_dir') and 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) diff --git a/test_clipboard_search.py b/test_clipboard_search.py deleted file mode 100644 index ba1ac23..0000000 --- a/test_clipboard_search.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to verify clipboard search improvements. -""" - -import sys -import os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -def test_clipboard_search(): - """Test clipboard search functionality.""" - try: - from modules.launcher.plugins.clipboard import ClipboardPlugin - - print("Testing clipboard search improvements...") - - # Create plugin instance - plugin = ClipboardPlugin() - plugin.initialize() - - print("āœ… Plugin initialized successfully") - - # Test empty query (should show all items up to max_results) - print("\nšŸ” Testing empty query...") - results = plugin.query("") - print(f"Found {len(results)} results for empty query") - - # Test specific search - print("\nšŸ” Testing search query 'test'...") - results = plugin.query("test") - print(f"Found {len(results)} results for 'test' query") - - # Show relevance scores - if results: - print("Relevance scores:") - for i, result in enumerate(results[:5]): # Show first 5 - print(f" {i+1}. {result.relevance:.2f} - {result.title[:50]}...") - - # Test image handling - print("\nšŸ–¼ļø Testing image handling...") - image_results = [r for r in results if "Image from clipboard" in r.title] - if image_results: - print(f"Found {len(image_results)} image results") - for img_result in image_results[:3]: # Show first 3 - has_icon = hasattr(img_result, 'icon') and img_result.icon is not None - print(f" - {img_result.subtitle} (Icon loaded: {has_icon})") - else: - print("No image results found (normal if no images in clipboard)") - - print("\nāœ… Clipboard search test completed!") - print("The plugin now:") - print(" - Searches through ALL clipboard items (not just first 20)") - print(" - Sorts results by relevance (exact matches first)") - print(" - Shows up to 50 results (increased from 20)") - print(" - Limits results only after filtering and sorting") - print(" - FOREVER image caching (like example_cliphist.py)") - print(" - Better image detection (supports more formats)") - print(" - Immediate image loading when cached") - print(" - No cache expiration - images stay cached until restart") - - plugin.cleanup() - - except Exception as e: - print(f"āŒ Error testing clipboard search: {e}") - import traceback - traceback.print_exc() - -if __name__ == "__main__": - test_clipboard_search() diff --git a/test_image_cache.py b/test_image_cache.py deleted file mode 100644 index a7b7a97..0000000 --- a/test_image_cache.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to verify image caching works forever like example_cliphist.py. -""" - -import sys -import os -import time -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -def test_image_cache(): - """Test image caching functionality.""" - try: - from modules.launcher.plugins.clipboard import ClipboardPlugin - - print("Testing image cache forever functionality...") - - # Create plugin instance - plugin = ClipboardPlugin() - plugin.initialize() - - print("āœ… Plugin initialized successfully") - - # Test image caching - print("\nšŸ–¼ļø Testing image cache...") - - # First query - should load images and cache them - print("First query (loading images)...") - results1 = plugin.query("") - image_results1 = [r for r in results1 if "Image from clipboard" in r.title] - - if image_results1: - print(f"Found {len(image_results1)} image results") - - # Check cache size - cache_size_before = len(plugin.image_cache) - print(f"Image cache size: {cache_size_before}") - - # Wait a moment - time.sleep(1) - - # Second query - should use cached images - print("\nSecond query (should use cache)...") - results2 = plugin.query("") - image_results2 = [r for r in results2 if "Image from clipboard" in r.title] - - cache_size_after = len(plugin.image_cache) - print(f"Image cache size after second query: {cache_size_after}") - - # Check if images have icons (meaning they're loaded) - cached_images = 0 - for img_result in image_results2: - if hasattr(img_result, 'icon') and img_result.icon is not None: - cached_images += 1 - - print(f"Images with loaded icons: {cached_images}/{len(image_results2)}") - - if cached_images > 0: - print("āœ… Images are being cached and reused!") - else: - print("āš ļø No cached images found") - - else: - print("No image results found (normal if no images in clipboard)") - - print("\nāœ… Image cache test completed!") - print("Cache behavior:") - print(" - Images are cached forever (no expiration)") - print(" - Cache survives multiple queries") - print(" - Images load immediately when cached") - print(" - Cache only clears on plugin restart") - - plugin.cleanup() - - except Exception as e: - print(f"āŒ Error testing image cache: {e}") - import traceback - traceback.print_exc() - -if __name__ == "__main__": - test_image_cache() diff --git a/test_mouse_navigation.py b/test_mouse_navigation.py deleted file mode 100644 index e83bbf2..0000000 --- a/test_mouse_navigation.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -""" -Test script to verify mouse navigation works in the launcher. -""" - -import sys -import os -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from modules.launcher.main import Launcher -import gi -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, GLib - -def test_mouse_navigation(): - """Test mouse navigation functionality.""" - print("Testing mouse navigation in launcher...") - - # Create launcher instance - launcher = Launcher() - - # Show launcher with a trigger to get some results - launcher.show_launcher("app") - - print("Launcher opened with 'app' trigger") - print("You should be able to:") - print("1. Hover over result items to see selection change (without scrolling)") - print("2. Click on result items to activate them") - print("3. Use keyboard navigation (arrow keys) which should scroll") - print("Press Ctrl+C to exit test") - - try: - Gtk.main() - except KeyboardInterrupt: - print("\nTest completed!") - launcher.close_launcher() - -if __name__ == "__main__": - test_mouse_navigation()