Source code for pysnap.terminal.clipboard
"""Read the host clipboard for the ``Ctrl-]`` ``v`` paste of ``pysnap connect``.
The clipboard is read only when the user asks for it. Copying and the usual
paste are left to the terminal of the user; this paste helps where the
terminal offers none from the keyboard, such as the Windows console without
its Ctrl key shortcuts.
- Windows: the Win32 clipboard API through ``ctypes``, no external program;
- macOS: ``pbpaste``;
- Linux and other systems: ``wl-paste`` under Wayland, then ``xclip`` or
``xsel`` for the X11 clipboard.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
READ_TIMEOUT = 3.0
_CF_UNICODETEXT = 13
[docs]
class ClipboardError(Exception):
"""Report that the host clipboard cannot be read."""
[docs]
def read_host_clipboard() -> str:
"""Return the text in the host clipboard.
:returns: Clipboard text; empty when the clipboard holds no text.
:raises ClipboardError: When no way to read the clipboard works.
"""
if sys.platform == "win32":
return _read_windows_clipboard()
commands = _clipboard_commands()
available = [command for command in commands if shutil.which(command[0])]
if not available:
names = ", ".join(command[0] for command in commands)
raise ClipboardError(f"no clipboard program found; install one of: {names}")
errors = []
for command in available:
try:
result = subprocess.run(
command,
stdin=subprocess.DEVNULL,
capture_output=True,
timeout=READ_TIMEOUT,
)
except (OSError, subprocess.SubprocessError) as error:
errors.append(f"{command[0]}: {error}")
continue
if result.returncode == 0:
return result.stdout.decode("utf-8", errors="replace")
message = result.stderr.decode("utf-8", errors="replace").strip()
errors.append(f"{command[0]}: {message or f'exit status {result.returncode}'}")
raise ClipboardError("; ".join(errors))
def _clipboard_commands() -> list[tuple[str, ...]]:
"""Return the programs that print the clipboard, in the order to try."""
if sys.platform == "darwin":
return [("pbpaste",)]
commands = [
("xclip", "-selection", "clipboard", "-o"),
("xsel", "--clipboard", "--output"),
]
if os.environ.get("WAYLAND_DISPLAY"):
commands.insert(0, ("wl-paste", "--no-newline"))
return commands
def _read_windows_clipboard() -> str: # pragma: no cover - Windows only
"""Return the Unicode text of the Windows clipboard."""
import ctypes
from ctypes import wintypes
user32 = ctypes.WinDLL("user32", use_last_error=True)
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
user32.GetClipboardData.restype = wintypes.HANDLE
user32.GetClipboardData.argtypes = [wintypes.UINT]
kernel32.GlobalLock.restype = wintypes.LPVOID
kernel32.GlobalLock.argtypes = [wintypes.HGLOBAL]
kernel32.GlobalUnlock.argtypes = [wintypes.HGLOBAL]
if not user32.OpenClipboard(None):
raise ClipboardError("the clipboard is in use by another program")
try:
handle = user32.GetClipboardData(_CF_UNICODETEXT)
if not handle:
return ""
pointer = kernel32.GlobalLock(handle)
if not pointer:
raise ClipboardError("the clipboard data cannot be locked")
try:
return ctypes.wstring_at(pointer)
finally:
kernel32.GlobalUnlock(handle)
finally:
user32.CloseClipboard()