home/x-wayland-clipboard-daemon/x-wayland-clipboard-daemon.py
2025-04-30 21:23:53 +02:00

138 lines
4.2 KiB
Python

# 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()