Files
juc500/dist-offline/juc500-offline-update-20260721-124606/juc500_xfer/clipboard_files.py
T
2026-07-21 21:04:25 +09:00

299 lines
8.1 KiB
Python

"""Read/write OS clipboard (file paths + plain text)."""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
from typing import Any
from urllib.parse import unquote, urlparse
_IS_WIN = sys.platform == "win32"
_IS_MAC = sys.platform == "darwin"
def list_clipboard_files() -> list[Path]:
"""Return existing file/dir paths currently on the clipboard."""
raw: list[str] = []
if _IS_MAC:
raw = _mac_filenames()
elif _IS_WIN:
raw = _win_filenames()
out: list[Path] = []
seen: set[str] = set()
for item in raw:
p = _normalize_path(item)
if p is None:
continue
key = str(p)
if key in seen:
continue
seen.add(key)
out.append(p)
return out
def clipboard_preview(limit: int = 8) -> dict[str, Any]:
files = list_clipboard_files()
names = [p.name for p in files[:limit]]
return {
"count": len(files),
"names": names,
"truncated": len(files) > limit,
"paths": [str(p) for p in files[:limit]],
}
def get_clipboard_text() -> str:
"""Best-effort plain text from the OS clipboard."""
if _IS_MAC:
return _mac_get_text()
if _IS_WIN:
return _win_get_text()
return ""
def set_clipboard_text(text: str) -> bool:
"""Set peer OS clipboard to plain text (for remote paste)."""
if text is None:
return False
if _IS_MAC:
return _mac_set_text(text)
if _IS_WIN:
return _win_set_text(text)
return False
def _normalize_path(item: str) -> Path | None:
s = (item or "").strip()
if not s:
return None
if s.startswith("file:"):
parsed = urlparse(s)
path = unquote(parsed.path or "")
if _IS_MAC and parsed.netloc and parsed.netloc not in ("", "localhost"):
path = f"/{parsed.netloc}{path}"
s = path
if _IS_WIN and s.startswith("/") and len(s) > 2 and s[2] == ":":
s = s[1:]
try:
p = Path(s)
if p.exists():
return p.resolve()
except OSError:
return None
return None
def _mac_filenames() -> list[str]:
# Prefer AppKit NSFilenamesPboardType
try:
from AppKit import NSPasteboard # type: ignore
pb = NSPasteboard.generalPasteboard()
for typ in (
"NSFilenamesPboardType",
"public.file-url",
):
try:
if typ == "NSFilenamesPboardType":
from AppKit import NSFilenamesPboardType # type: ignore
items = pb.propertyListForType_(NSFilenamesPboardType)
if items:
return [str(x) for x in items]
else:
# public.file-url may be multiple items
classes_out = []
try:
from Foundation import NSURL # type: ignore
objs = pb.readObjectsForClasses_options_([NSURL], None)
if objs:
for u in objs:
try:
if hasattr(u, "isFileURL") and u.isFileURL():
classes_out.append(str(u.path()))
except Exception:
continue
except Exception:
pass
if classes_out:
return classes_out
except Exception:
continue
except Exception:
pass
return _mac_filenames_osascript()
def _mac_filenames_osascript() -> list[str]:
ascript = """
try
set clip to the clipboard as «class furl»
return POSIX path of clip
on error
try
set out to {}
set clip to the clipboard
if class of clip is list then
repeat with i in clip
try
set end of out to POSIX path of (i as alias)
end try
end repeat
end if
set AppleScript's text item delimiters to linefeed
return out as text
on error
return ""
end try
end try
"""
try:
proc = subprocess.run(
["osascript", "-e", ascript],
capture_output=True,
text=True,
timeout=3,
check=False,
)
text = (proc.stdout or "").strip()
if not text:
return []
return [ln.strip() for ln in text.splitlines() if ln.strip()]
except Exception:
return []
def _win_filenames() -> list[str]:
try:
import ctypes
user32 = ctypes.windll.user32
shell32 = ctypes.windll.shell32
CF_HDROP = 15
if not user32.OpenClipboard(None):
return []
try:
handle = user32.GetClipboardData(CF_HDROP)
if not handle:
return []
count = shell32.DragQueryFileW(handle, 0xFFFFFFFF, None, 0)
out: list[str] = []
buf = ctypes.create_unicode_buffer(1024)
for i in range(int(count)):
n = shell32.DragQueryFileW(handle, i, buf, 1024)
if n:
out.append(buf.value)
return out
finally:
user32.CloseClipboard()
except Exception:
return []
def _mac_get_text() -> str:
try:
from AppKit import NSPasteboard, NSPasteboardTypeString # type: ignore
pb = NSPasteboard.generalPasteboard()
s = pb.stringForType_(NSPasteboardTypeString)
if s:
return str(s)
except Exception:
pass
try:
proc = subprocess.run(
["pbpaste"],
capture_output=True,
text=True,
timeout=2,
check=False,
)
return proc.stdout or ""
except Exception:
return ""
def _mac_set_text(text: str) -> bool:
try:
from AppKit import NSPasteboard, NSPasteboardTypeString # type: ignore
pb = NSPasteboard.generalPasteboard()
pb.clearContents()
return bool(pb.setString_forType_(text, NSPasteboardTypeString))
except Exception:
pass
try:
proc = subprocess.run(
["pbcopy"],
input=text,
text=True,
timeout=2,
check=False,
)
return proc.returncode == 0
except Exception:
return False
def _win_get_text() -> str:
try:
import ctypes
from ctypes import wintypes
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
CF_UNICODETEXT = 13
if not user32.OpenClipboard(None):
return ""
try:
handle = user32.GetClipboardData(CF_UNICODETEXT)
if not handle:
return ""
ptr = kernel32.GlobalLock(handle)
if not ptr:
return ""
try:
return ctypes.wstring_at(ptr)
finally:
kernel32.GlobalUnlock(handle)
finally:
user32.CloseClipboard()
except Exception:
return ""
def _win_set_text(text: str) -> bool:
try:
import ctypes
from ctypes import wintypes
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
CF_UNICODETEXT = 13
GMEM_MOVEABLE = 0x0002
data = (text.replace("\n", "\r\n") + "\0").encode("utf-16-le")
if not user32.OpenClipboard(None):
return False
try:
user32.EmptyClipboard()
h = kernel32.GlobalAlloc(GMEM_MOVEABLE, len(data))
if not h:
return False
ptr = kernel32.GlobalLock(h)
if not ptr:
kernel32.GlobalFree(h)
return False
ctypes.memmove(ptr, data, len(data))
kernel32.GlobalUnlock(h)
if not user32.SetClipboardData(CF_UNICODETEXT, h):
kernel32.GlobalFree(h)
return False
return True
finally:
user32.CloseClipboard()
except Exception:
return False