# very simple daemon that monitors the clipboard for wayland (using wl-clipboard) and X (using xclip) # whenever one changes, clipboard-daemon ensures the other changes as well # # currently supports syncing text/plain and image/png mimetypes between clipboards # # needs wl-paste and xclip in $PATH from typing import Optional import subprocess from abc import ABC from dataclasses import dataclass import time INTERVAL_MS = 100 class ClipboardContents(ABC): pass @dataclass class TextContents(ClipboardContents): text: bytes def __repr__(self): if len(self.text) < 20: return f"TextContents(text={self.text!r})" else: return f"TextContents(text={self.text[:20]!r}..)" def __str__(self): return repr(self) @dataclass class ImageContents(ClipboardContents): png_data: bytes def __repr__(self): return "ImageContents(png_data=...)" def __str__(self): return repr(self) def get_wayland_clipboard() -> Optional[ClipboardContents]: formats = subprocess.run(["wl-paste", "-l"], capture_output=True).stdout.strip(b"\n") if b"image/png" in formats: contents = subprocess.run(["wl-paste", "-t", "image/png"], capture_output=True).stdout return ImageContents(png_data=contents) if b"text/plain" in formats: contents = subprocess.run(["wl-paste", "-n", "-t", "text/plain"], capture_output=True).stdout return TextContents(text=contents) return None def set_wayland_clipboard(data: ClipboardContents): if isinstance(data, TextContents): subprocess.run( ["wl-copy", "-t", "text/plain", "-n"], input=data.text, ) elif isinstance(data, ImageContents): subprocess.run( ["wl-copy", "-t", "image/png"], input=data.png_data, ) else: raise ValueError(f"Unknown clipboard data: {data}") def get_x_clipboard() -> Optional[ClipboardContents]: formats = subprocess.run( ["xclip", "-out", "-selection", "clipboard", "-target", "TARGETS"], capture_output=True, ).stdout.strip(b"\n") if b"image/png" in formats: contents = subprocess.run( ["xclip", "-out", "-selection", "clipboard", "-target", "image/png"], capture_output=True, ).stdout return ImageContents(png_data=contents) if b"text/plain" in formats: contents = subprocess.run( ["xclip", "-out", "-selection", "clipboard", "-target", "text/plain"], capture_output=True, ).stdout return TextContents(text=contents) return None def set_x_clipboard(data: ClipboardContents): if isinstance(data, TextContents): subprocess.run( ["xclip", "-in", "-selection", "clipboard", "-target", "text/plain"], input=data.text, ) elif isinstance(data, ImageContents): subprocess.run( ["xclip", "-in", "-selection", "clipboard", "-target", "image/png"], input=data.png_data, ) else: raise ValueError(f"Unknown clipboard data: {data}") def clipboard_sync(): last_wayland_contents = get_wayland_clipboard() last_x_contents = get_x_clipboard() while True: time.sleep(INTERVAL_MS/1000) wayland_contents = get_wayland_clipboard() x_contents = get_x_clipboard() if wayland_contents != last_wayland_contents: print(f"wayland clipborad updated from {last_wayland_contents} to {wayland_contents}") if wayland_contents is not None: print(" sycning wayland -> x") set_x_clipboard(wayland_contents) x_contents = wayland_contents else: print(" unknown type, not syncing") elif x_contents != last_x_contents: print(f"x clipborad updated from {last_x_contents} to {x_contents}") if x_contents is not None: print(" sycning x -> wayland") set_wayland_clipboard(x_contents) wayland_contents_contents = x_contents else: print(" unknown type, not syncing") last_wayland_contents = wayland_contents last_x_contents = x_contents print("starting x-wayland-clipboard-daemon") clipboard_sync()