juc500 정상동작
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user