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)