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()
# Use GLib.Thread for proper async execution
GLib.Thread.new("cliphist-loader", self._load_clipboard_items_thread, None)
def _load_clipboard_items_thread(self, user_data):
"""Background thread worker for loading clipboard items"""
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 asynchronously using background thread"""
GLib.Thread.new("image-preview", self._load_image_preview_thread, (item_id, button))
def _load_image_preview_thread(self, data):
"""Background thread worker for loading image preview"""
item_id, button = data
try:
if item_id in self.image_cache:
pixbuf = self.image_cache[item_id]
GLib.idle_add(self._update_image_button, button, pixbuf)
return
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
GLib.idle_add(self._update_image_button, button, pixbuf)
except Exception as e:
print(f"Error loading image preview: {e}", file=sys.stderr)
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)