♻ refactor(clipboard): enhance clipboard plugin with forever image caching and improved search
This commit is contained in:
4 files changed
+665
-44
No files matched your search
@@ -0,0 +1,505 @@
|
||||
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 "<meta http-equiv" in line:
|
||||
continue
|
||||
new_items.append(line)
|
||||
self._update_items(new_items)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error loading clipboard history: {e}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}", file=sys.stderr)
|
||||
finally:
|
||||
self._loading = False
|
||||
if self._pending_updates:
|
||||
self._pending_updates = False
|
||||
GLib.idle_add(self._load_clipboard_items_thread)
|
||||
return False
|
||||
|
||||
def _update_items(self, new_items):
|
||||
"""Update the items list from main thread"""
|
||||
self.clipboard_items = new_items
|
||||
self.display_clipboard_items()
|
||||
|
||||
def display_clipboard_items(self, filter_text=""):
|
||||
"""Display clipboard items in the viewport"""
|
||||
remove_handler(self._arranger_handler) if self._arranger_handler else None
|
||||
self.viewport.children = []
|
||||
self.selected_index = -1
|
||||
|
||||
|
||||
filtered_items = []
|
||||
for item in self.clipboard_items:
|
||||
|
||||
content = item.split('\t', 1)[1] if '\t' in item else item
|
||||
if filter_text.lower() in content.lower():
|
||||
filtered_items.append(item)
|
||||
|
||||
|
||||
if not filtered_items:
|
||||
|
||||
container = Box(
|
||||
name="no-clip-container",
|
||||
orientation="v",
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
h_expand=True,
|
||||
v_expand=True
|
||||
)
|
||||
|
||||
|
||||
label = Label(
|
||||
name="no-clip",
|
||||
markup=icons.clipboard,
|
||||
h_align="center",
|
||||
v_align="center",
|
||||
)
|
||||
|
||||
container.add(label)
|
||||
self.viewport.add(container)
|
||||
return
|
||||
|
||||
|
||||
self._display_items_batch(filtered_items, 0, 10)
|
||||
|
||||
def _display_items_batch(self, items, start, batch_size):
|
||||
"""Display items in batches to keep UI responsive"""
|
||||
end = min(start + batch_size, len(items))
|
||||
|
||||
for i in range(start, end):
|
||||
item = items[i]
|
||||
self.viewport.add(self.create_clipboard_item(item))
|
||||
|
||||
|
||||
if end < len(items):
|
||||
GLib.idle_add(self._display_items_batch, items, end, batch_size)
|
||||
else:
|
||||
|
||||
if self.search_entry.get_text() and self.viewport.get_children():
|
||||
self.update_selection(0)
|
||||
|
||||
def create_clipboard_item(self, item):
|
||||
"""Create a button for a clipboard item"""
|
||||
|
||||
parts = item.split('\t', 1)
|
||||
item_id = parts[0] if len(parts) > 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*<img\s+', content) is not None or
|
||||
"binary" in content.lower() and any(ext in content.lower() for ext in ["jpg", "jpeg", "png", "bmp", "gif"])
|
||||
)
|
||||
|
||||
def paste_item(self, item_id):
|
||||
"""Copy the selected item to the clipboard and close (GLib.idle_add)"""
|
||||
def paste():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["cliphist", "decode", item_id],
|
||||
capture_output=True,
|
||||
check=True
|
||||
)
|
||||
subprocess.run(
|
||||
["wl-copy"],
|
||||
input=result.stdout,
|
||||
check=True
|
||||
)
|
||||
GLib.idle_add(self.close)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error pasting clipboard item: {e}", file=sys.stderr)
|
||||
return False
|
||||
GLib.idle_add(paste)
|
||||
|
||||
def delete_item(self, item_id):
|
||||
"""Delete the selected clipboard item (GLib.idle_add)"""
|
||||
def delete():
|
||||
try:
|
||||
subprocess.run(
|
||||
["cliphist", "delete", item_id],
|
||||
check=True
|
||||
)
|
||||
self._pending_updates = True
|
||||
if not self._loading:
|
||||
GLib.idle_add(self._load_clipboard_items_thread)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error deleting clipboard item: {e}", file=sys.stderr)
|
||||
return False
|
||||
GLib.idle_add(delete)
|
||||
|
||||
def clear_history(self):
|
||||
"""Clear all clipboard history (GLib.idle_add)"""
|
||||
def clear():
|
||||
try:
|
||||
subprocess.run(["cliphist", "wipe"], check=True)
|
||||
self._pending_updates = True
|
||||
if not self._loading:
|
||||
GLib.idle_add(self._load_clipboard_items_thread)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error clearing clipboard history: {e}", file=sys.stderr)
|
||||
return False
|
||||
GLib.idle_add(clear)
|
||||
|
||||
def filter_items(self, entry, *_):
|
||||
"""Filter clipboard items based on search text"""
|
||||
self.display_clipboard_items(entry.get_text())
|
||||
|
||||
def on_search_entry_key_press(self, widget, event):
|
||||
"""Handle key presses in the search entry"""
|
||||
if event.keyval == Gdk.KEY_Down:
|
||||
self.move_selection(1)
|
||||
return True
|
||||
elif event.keyval == Gdk.KEY_Up:
|
||||
self.move_selection(-1)
|
||||
return True
|
||||
elif event.keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter):
|
||||
self.use_selected_item()
|
||||
return True
|
||||
elif event.keyval == Gdk.KEY_Delete:
|
||||
self.delete_selected_item()
|
||||
return True
|
||||
elif event.keyval == Gdk.KEY_Escape:
|
||||
self.close()
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_selection(self, new_index):
|
||||
"""Update the selected item in the viewport"""
|
||||
children = self.viewport.get_children()
|
||||
|
||||
|
||||
if self.selected_index != -1 and self.selected_index < len(children):
|
||||
current_button = children[self.selected_index]
|
||||
current_button.get_style_context().remove_class("selected")
|
||||
|
||||
|
||||
if new_index != -1 and new_index < len(children):
|
||||
new_button = children[new_index]
|
||||
new_button.get_style_context().add_class("selected")
|
||||
self.selected_index = new_index
|
||||
self.scroll_to_selected(new_button)
|
||||
else:
|
||||
self.selected_index = -1
|
||||
|
||||
def move_selection(self, delta):
|
||||
"""Move the selection up or down"""
|
||||
children = self.viewport.get_children()
|
||||
if not children:
|
||||
return
|
||||
|
||||
|
||||
if self.selected_index == -1 and delta == 1:
|
||||
new_index = 0
|
||||
else:
|
||||
new_index = self.selected_index + delta
|
||||
|
||||
new_index = max(0, min(new_index, len(children) - 1))
|
||||
self.update_selection(new_index)
|
||||
|
||||
def scroll_to_selected(self, button):
|
||||
"""Scroll to ensure the selected item is visible"""
|
||||
def scroll():
|
||||
adj = self.scrolled_window.get_vadjustment()
|
||||
alloc = button.get_allocation()
|
||||
if alloc.height == 0:
|
||||
return False
|
||||
|
||||
y = alloc.y
|
||||
height = alloc.height
|
||||
page_size = adj.get_page_size()
|
||||
current_value = adj.get_value()
|
||||
|
||||
visible_top = current_value
|
||||
visible_bottom = current_value + page_size
|
||||
|
||||
if y < visible_top:
|
||||
|
||||
adj.set_value(y)
|
||||
elif y + height > 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)
|
||||
@@ -20,18 +20,16 @@ class ClipboardPlugin(PluginBase):
|
||||
self.display_name = "Clipboard History"
|
||||
self.description = "Search and manage clipboard history using cliphist"
|
||||
|
||||
# Performance settings
|
||||
self.max_results = 20
|
||||
# Performance settings - show more history like example_cliphist.py
|
||||
self.max_results = 50
|
||||
# Cache clipboard items for 5 seconds (more responsive)
|
||||
self.cache_ttl = 5
|
||||
self.image_cache_ttl = 300 # Cache images for 5 minutes
|
||||
|
||||
# Initialize cache and temp directory
|
||||
self.tmp_dir = tempfile.mkdtemp(prefix="cliphist-")
|
||||
self.image_cache: Dict[str, GdkPixbuf.Pixbuf] = {}
|
||||
self.image_cache: Dict[str, GdkPixbuf.Pixbuf] = {} # Cache images forever like example_cliphist.py
|
||||
self.clipboard_items_cache: List[str] = []
|
||||
self.cache_timestamp = 0
|
||||
self.image_cache_timestamps: Dict[str, float] = {}
|
||||
|
||||
# Threading
|
||||
self.executor = ThreadPoolExecutor(
|
||||
@@ -73,7 +71,6 @@ class ClipboardPlugin(PluginBase):
|
||||
with self.cache_lock:
|
||||
self.image_cache.clear()
|
||||
self.clipboard_items_cache.clear()
|
||||
self.image_cache_timestamps.clear()
|
||||
except Exception as e:
|
||||
print(f"Error cleaning up temporary files: {e}", file=sys.stderr)
|
||||
|
||||
@@ -82,8 +79,7 @@ class ClipboardPlugin(PluginBase):
|
||||
with self.cache_lock:
|
||||
self.clipboard_items_cache.clear()
|
||||
self.cache_timestamp = 0
|
||||
self.image_cache.clear()
|
||||
self.image_cache_timestamps.clear()
|
||||
# Keep image cache forever like example_cliphist.py
|
||||
|
||||
def _force_launcher_refresh(self):
|
||||
"""Force the launcher to refresh its results."""
|
||||
@@ -174,10 +170,7 @@ class ClipboardPlugin(PluginBase):
|
||||
self.clipboard_items_cache = items
|
||||
self.cache_timestamp = current_time
|
||||
|
||||
# If data changed significantly, clear image cache too
|
||||
if data_changed:
|
||||
self.image_cache.clear()
|
||||
self.image_cache_timestamps.clear()
|
||||
# Keep image cache forever like example_cliphist.py - don't clear on data changes
|
||||
|
||||
return items
|
||||
except subprocess.CalledProcessError as e:
|
||||
@@ -217,9 +210,13 @@ class ClipboardPlugin(PluginBase):
|
||||
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"]
|
||||
"""Determine if clipboard content is likely an image (like example_cliphist.py)."""
|
||||
return (
|
||||
content.startswith("data:image/") or
|
||||
content.startswith("\x89PNG") or
|
||||
content.startswith("GIF8") or
|
||||
content.startswith("\xff\xd8\xff") or
|
||||
"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:
|
||||
@@ -229,17 +226,10 @@ class ClipboardPlugin(PluginBase):
|
||||
return content
|
||||
|
||||
def _load_image_preview_cached(self, item_id: str) -> Optional[GdkPixbuf.Pixbuf]:
|
||||
"""Load image preview with caching and timeout."""
|
||||
current_time = time.time()
|
||||
|
||||
# Check cache first
|
||||
"""Load image preview with forever caching (like example_cliphist.py)."""
|
||||
# Check cache first - cache forever like example_cliphist.py
|
||||
with self.cache_lock:
|
||||
if (
|
||||
item_id in self.image_cache
|
||||
and item_id in self.image_cache_timestamps
|
||||
and current_time - self.image_cache_timestamps[item_id]
|
||||
< self.image_cache_ttl
|
||||
):
|
||||
if item_id in self.image_cache:
|
||||
return self.image_cache[item_id]
|
||||
|
||||
try:
|
||||
@@ -253,7 +243,7 @@ class ClipboardPlugin(PluginBase):
|
||||
if pixbuf:
|
||||
with self.cache_lock:
|
||||
self.image_cache[item_id] = pixbuf
|
||||
self.image_cache_timestamps[item_id] = current_time
|
||||
|
||||
return pixbuf
|
||||
except Exception as e:
|
||||
print(f"Error loading image preview: {e}", file=sys.stderr)
|
||||
@@ -312,15 +302,15 @@ class ClipboardPlugin(PluginBase):
|
||||
item_id = parts[0] if len(parts) > 1 else str(i)
|
||||
content = parts[1] if len(parts) > 1 else item
|
||||
|
||||
# Handle image content with lazy loading
|
||||
# Handle image content like example_cliphist.py
|
||||
if self._is_image_data(content):
|
||||
# Check if image is already cached
|
||||
# Check if image is already cached (forever cache like example_cliphist.py)
|
||||
cached_pixbuf = None
|
||||
with self.cache_lock:
|
||||
cached_pixbuf = self.image_cache.get(item_id)
|
||||
|
||||
if cached_pixbuf:
|
||||
# Use cached image
|
||||
# Use cached image immediately
|
||||
result = Result(
|
||||
title="Image from clipboard",
|
||||
subtitle="Click to copy image to clipboard",
|
||||
@@ -332,19 +322,45 @@ class ClipboardPlugin(PluginBase):
|
||||
data={"bypass_max_results": True},
|
||||
)
|
||||
else:
|
||||
# Show placeholder and load image in background
|
||||
result = Result(
|
||||
title="Image from clipboard",
|
||||
subtitle="Loading preview...",
|
||||
description="Image content",
|
||||
icon_name="image-x-generic",
|
||||
relevance=relevance,
|
||||
plugin_name=self.name,
|
||||
action=lambda id=item_id: self._copy_to_clipboard(id),
|
||||
data={"bypass_max_results": True},
|
||||
)
|
||||
# Load image in background (don't wait for it)
|
||||
self.executor.submit(self._load_image_preview_cached, item_id)
|
||||
# Try to load image immediately like example_cliphist.py
|
||||
try:
|
||||
immediate_pixbuf = self._load_image_preview_cached(item_id)
|
||||
if immediate_pixbuf:
|
||||
# Successfully loaded immediately
|
||||
result = Result(
|
||||
title="Image from clipboard",
|
||||
subtitle="Click to copy image to clipboard",
|
||||
description="Image content",
|
||||
icon=immediate_pixbuf,
|
||||
relevance=relevance,
|
||||
plugin_name=self.name,
|
||||
action=lambda id=item_id: self._copy_to_clipboard(id),
|
||||
data={"bypass_max_results": True},
|
||||
)
|
||||
else:
|
||||
# Show placeholder if loading failed
|
||||
result = Result(
|
||||
title="Image from clipboard",
|
||||
subtitle="Click to copy image to clipboard",
|
||||
description="Image content",
|
||||
icon_name="image-x-generic",
|
||||
relevance=relevance,
|
||||
plugin_name=self.name,
|
||||
action=lambda id=item_id: self._copy_to_clipboard(id),
|
||||
data={"bypass_max_results": True},
|
||||
)
|
||||
except Exception:
|
||||
# Fallback to placeholder
|
||||
result = Result(
|
||||
title="Image from clipboard",
|
||||
subtitle="Click to copy image to clipboard",
|
||||
description="Image content",
|
||||
icon_name="image-x-generic",
|
||||
relevance=relevance,
|
||||
plugin_name=self.name,
|
||||
action=lambda id=item_id: self._copy_to_clipboard(id),
|
||||
data={"bypass_max_results": True},
|
||||
)
|
||||
|
||||
results.append(result)
|
||||
continue
|
||||
@@ -400,8 +416,11 @@ class ClipboardPlugin(PluginBase):
|
||||
timeout=3,
|
||||
)
|
||||
|
||||
# Invalidate cache since clipboard content has changed
|
||||
self.invalidate_cache()
|
||||
# Don't invalidate image cache - keep images cached forever like example_cliphist.py
|
||||
# Only invalidate clipboard items cache
|
||||
with self.cache_lock:
|
||||
self.clipboard_items_cache.clear()
|
||||
self.cache_timestamp = 0
|
||||
|
||||
except subprocess.SubprocessError as e:
|
||||
print(f"Error copying to clipboard: {e}", file=sys.stderr)
|
||||
|
||||
@@ -36,11 +36,27 @@ def test_clipboard_search():
|
||||
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()
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user