Files
Modus/modules/launcher/components/cliphist.py
T
2025-02-17 21:27:34 +05:45

206 lines
6.6 KiB
Python

import os
import subprocess
from typing import List
from fabric import Fabricator
from fabric.utils import remove_handler
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GLib
from snippets import MaterialIcon
class Cliphist(Box):
def __init__(self, **kwargs):
super().__init__(
name="cliphist-launcher",
all_visible=False,
visible=False,
**kwargs,
)
self._arranger_handler = 0
self.cliphist_manager = CliphistManager(self)
self.viewport = None
self.launcher = kwargs["launcher"]
self.search_entry = Entry(
name="search-entry",
h_expand=True,
notify_text=lambda entry, *_: self.handle_search_input(entry.get_text()),
on_activate=lambda entry, *_: self.handle_search_input(entry.get_text()),
)
self.toggle_button = Button(
child=MaterialIcon("delete_forever"),
name="clear-launcher-button",
on_clicked=self.on_button_clicked,
)
self.header_box = Box(
name="header-box",
spacing=10,
orientation="h",
children=[self.search_entry, self.toggle_button],
)
self.launcher_box = Box(
name="cliphist-history",
spacing=10,
orientation="v",
h_expand=True,
children=[self.header_box],
)
self.add(self.launcher_box)
def on_button_clicked(self, *_):
GLib.spawn_command_line_async("cliphist wipe")
self.cliphist_manager.update_cliphist_history()
def open_launcher(self):
if not self.viewport:
self.viewport = Box(name="viewport", spacing=4, orientation="v")
self.scrolled_window = ScrolledWindow(
name="scrolled-window",
spacing=10,
h_scrollbar_policy="never",
child=self.viewport,
)
self.launcher_box.add(self.scrolled_window)
self.viewport.children = []
self.cliphist_manager.arrange_viewport()
self.viewport.show()
self.search_entry.grab_focus()
def close_launcher(self):
self.launcher.close()
def handle_search_input(self, text: str):
self.cliphist_manager.arrange_viewport(text)
class CliphistManager:
def __init__(self, launcher):
self.launcher = launcher
self.cliphist_history = []
self.wl_paste_watcher = Fabricator(
poll_from="wl-paste --watch echo", stream=True, interval=-1
)
self.wl_paste_watcher.connect(
"changed", lambda *_: self.update_cliphist_history()
)
def get_clip_history(self) -> List[dict]:
try:
result = subprocess.run(
["cliphist", "list"], capture_output=True, text=True, check=True
)
return self._parse_clip_history(result.stdout)
except subprocess.CalledProcessError as e:
print(f"Error fetching cliphist history: {e}")
return []
def _parse_clip_history(self, output: str) -> List[dict]:
items = []
for line in output.splitlines():
if line.strip():
parts = line.strip().split(maxsplit=1)
if len(parts) == 2:
items.append({"id": parts[0], "content": parts[1]})
return items
def copy_clip(self, clip_id: str):
try:
decode_result = subprocess.run(
["cliphist", "decode", clip_id], capture_output=True, check=True
)
subprocess.run(["wl-copy"], input=decode_result.stdout, check=True)
self.launcher.close_launcher()
except subprocess.CalledProcessError as e:
print(f"Error copying clip {clip_id}: {e}")
def save_image_file(self, clip_id: str) -> str:
output_file = f"/tmp/cliphist-{clip_id}.png"
os.makedirs("/tmp", exist_ok=True)
try:
decode_result = subprocess.run(
["cliphist", "decode", clip_id], capture_output=True, check=True
)
with open(output_file, "wb") as f:
f.write(decode_result.stdout)
return output_file
except (subprocess.CalledProcessError, IOError) as e:
print(f"Error saving image file for clip {clip_id}: {e}")
return ""
def query_clips(self, query: str = "") -> List[dict]:
history = self.get_clip_history()
if not query.strip():
return history
return [item for item in history if query.lower() in item["content"].lower()]
def bake_clip_slot(self, item: dict) -> Button:
if "[[ binary data" in item["content"].lower():
return self._create_image_button(item)
return self._create_text_button(item)
def _create_image_button(self, item: dict) -> Button:
output_file = self.save_image_file(item["id"])
if output_file and os.path.exists(output_file):
button = Button(
h_align="start",
name="clip-img-item",
on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id),
)
button.set_style(
f"background-image: url('{output_file}'); background-size: cover; background-position: center;"
)
return button
return None
def _create_text_button(self, item: dict) -> Button:
return Button(
child=Label(
label=item["content"][:55]
+ ("..." if len(item["content"]) > 55 else ""),
h_expand=True,
v_align="center",
h_align="start",
),
on_clicked=lambda _, clip_id=item["id"]: self.copy_clip(clip_id),
name="clip-item",
)
def update_cliphist_history(self):
self.cliphist_history = self.get_clip_history()
if self.launcher.viewport:
GLib.idle_add(self.arrange_viewport)
def arrange_viewport(self, query: str = ""):
if not self.launcher.viewport:
return
if self.launcher._arranger_handler:
remove_handler(self.launcher._arranger_handler)
self.launcher.viewport.children = []
filtered_clips = self.query_clips(query)
for clip in filtered_clips:
button = self.bake_clip_slot(clip)
if button:
self.launcher.viewport.add(button)