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)