aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/src/meshbay_cli/tui/clipboard.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/tui/clipboard.py')
-rw-r--r--packages/meshbay-cli/src/meshbay_cli/tui/clipboard.py76
1 files changed, 76 insertions, 0 deletions
diff --git a/packages/meshbay-cli/src/meshbay_cli/tui/clipboard.py b/packages/meshbay-cli/src/meshbay_cli/tui/clipboard.py
new file mode 100644
index 0000000..1a1f0d7
--- /dev/null
+++ b/packages/meshbay-cli/src/meshbay_cli/tui/clipboard.py
@@ -0,0 +1,76 @@
+"""
+The system clipboard, for the chat's right-click copy and paste.
+
+A copy goes to the terminal (OSC 52, through Textual), which reaches the
+clipboard of the machine in front of the person even over SSH, and to the
+platform's own tool too, for terminals that ignore OSC 52. A paste has no
+terminal escape to rely on: the text is read with the platform's tool, else it
+is what this program copied last. A tool that is missing, fails or hangs is
+skipped, never an error.
+"""
+
+import asyncio
+import os
+import shutil
+import sys
+
+TIMEOUT = 3.0
+_PS = ["powershell", "-NoProfile", "-NonInteractive", "-Command"]
+
+
+def _tools(read: bool) -> list[list[str]]:
+ if sys.platform == "win32":
+ return [_PS + ["[Console]::OutputEncoding = [Text.Encoding]::UTF8; Get-Clipboard -Raw"]
+ if read else ["clip"]]
+ if sys.platform == "darwin":
+ return [["pbpaste"] if read else ["pbcopy"]]
+ tools = []
+ if os.environ.get("WAYLAND_DISPLAY"):
+ tools.append(["wl-paste", "--no-newline"] if read else ["wl-copy"])
+ if os.environ.get("DISPLAY"):
+ tools.append(["xclip", "-selection", "clipboard", "-o"] if read
+ else ["xclip", "-selection", "clipboard", "-i"])
+ tools.append(["xsel", "--clipboard", "--output"] if read
+ else ["xsel", "--clipboard", "--input"])
+ return tools
+
+
+def _encode(text: str) -> bytes:
+ # clip.exe reads the console's code page unless the text says it is UTF-16.
+ return ("" + text).encode("utf-16-le") if sys.platform == "win32" else text.encode()
+
+
+async def _run(cmd: list[str], data: bytes | None) -> bytes | None:
+ if shutil.which(cmd[0]) is None:
+ return None
+ env = {**os.environ, "LC_CTYPE": "UTF-8"} if sys.platform == "darwin" else None
+ try:
+ proc = await asyncio.create_subprocess_exec(
+ *cmd, stdin=asyncio.subprocess.PIPE if data is not None else asyncio.subprocess.DEVNULL,
+ stdout=asyncio.subprocess.PIPE if data is None else asyncio.subprocess.DEVNULL,
+ stderr=asyncio.subprocess.DEVNULL, env=env)
+ except OSError:
+ return None
+ try:
+ out, _ = await asyncio.wait_for(proc.communicate(data), TIMEOUT)
+ except TimeoutError:
+ proc.kill()
+ await proc.wait()
+ return None
+ return (out or b"") if proc.returncode == 0 else None
+
+
+async def read_text() -> str | None:
+ """The system clipboard's text, or None when no tool could read it."""
+ for cmd in _tools(read=True):
+ out = await _run(cmd, None)
+ if out is not None:
+ return out.decode("utf-8", "replace").lstrip("")
+ return None
+
+
+async def write_text(text: str) -> bool:
+ for cmd in _tools(read=False):
+ if await _run(cmd, _encode(text)) is not None:
+ return True
+ return False