diff options
Diffstat (limited to 'packages/meshbay-cli/src/meshbay_cli/tui/clipboard.py')
| -rw-r--r-- | packages/meshbay-cli/src/meshbay_cli/tui/clipboard.py | 76 |
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 |