from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.image import Image
from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.utils import idle_add, remove_handler
from fabric.utils.helpers import get_relative_path
from gi.repository import GLib, Gdk, GdkPixbuf
import subprocess
import os
import re
import sys
import tempfile
import utils.icons as icons
class ClipHistory(Box):
def __init__(self, **kwargs):
super().__init__(
name="clip-history",
visible=False,
all_visible=False,
**kwargs,
)
# Create a temporary directory for image icons
self.tmp_dir = tempfile.mkdtemp(prefix="cliphist-")
self.image_cache = {} # Cache for image previews
self.launcher = kwargs["launcher"]
self.selected_index = -1 # Track the selected item index
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,
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,
min_content_size=(450, 105),
max_content_size=(450, 105),
child=self.viewport,
)
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,
],
)
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 # Reset selection
self.launcher.close()
def open(self):
"""Open the clipboard history panel and load items"""
self.viewport.set_visible(True)
if self._loading:
return
self._loading = True
self.search_entry.set_text("") # Clear search
self.search_entry.grab_focus()
# Start loading in background using GLib.idle_add
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, text=True, check=True
)
lines = result.stdout.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
# Truncate content for display
display_text = content.strip()
if len(display_text) > 100:
display_text = display_text[:97] + "..."
# Check if this is an image by examining the content
is_image = self.is_image_data(content)
if is_image:
# For images, create item with image preview
button = Button(
name="slot-button",
child=Box(
name="slot-box",
orientation="h",
spacing=10,
children=[
Image(name="clip-icon", h_align="start"), # Placeholder
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),
)
# Load image preview in background
self._load_image_preview_async(item_id, button)
else:
# For text, create regular item
button = self.create_text_item_button(item_id, display_text)
# Add key press event handler for Enter key
button.connect(
"key-press-event",
lambda widget, event, id=item_id: self.on_item_key_press(widget, event, id),
)
# Make sure button can receive focus and key events
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"""
# Check for common image data patterns
return (
content.startswith("data:image/")
or content.startswith("\x89PNG")
or content.startswith("GIF8")
or content.startswith("\xff\xd8\xff") # JPEG
or re.match(r"^\s*
visible_bottom:
# Item below viewport - align to 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
# Get the item ID from the first part before the tab
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
# Get the item ID from the first part before the tab
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):
# Copy item to clipboard and close
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)