Cross-platform clipboard access module (spclipbd.py) that provides a unified interface for reading from and writing to system clipboard across Windows, macOS, and Linux platforms.
- Reads clipboard content at construction time
get_suffix(): Returns file extension without dot (e.g., "png", "txt", "zip") or "_plaintext"get_raw(): Returns binary content (UTF-8 encoded for text)
- Handle for temporary files created during clipboard operations
path: Property returning the temp file path (None if no file created)delete(): Method to delete the temp file from disk
- Returns
TempFileobject - Behavior by
content_type:"_plaintext"→ Plain text (no temp file)- Image types (
png,jpg,jpeg,bmp,gif,webp,tiff,svg) → Image data (no temp file for most types, except jpg/jpeg on Linux) - Other types (
txt,zip,json, etc.) → File with temp created
- PowerShell (built-in)
- .NET Framework (built-in)
- osascript (built-in)
- Optional but recommended: PyObjC (
pip install pyobjc-core pyobjc-framework-AppKit pyobjc-framework-Cocoa)- Provides NSPasteboard, NSURL, NSData, etc.
- Required for proper file paste to Finder
- Falls back to AppleScript if not available
- xclip (
sudo apt-get install xclipor equivalent) - Background daemon
linux_uri_loopruns automatically
- Read: PowerShell + .NET API
- Check image → Check files (GetFileDropList) → Fallback to text
- Important: PowerShell adds trailing newline to output, must strip it when reading text
- Write: PowerShell + .NET API
- Plain text: SetText (use Clear() for empty text)
- Images: Write to temp file first, then load with Image.FromFile() + SetImage()
- Files: Create temp file + SetFileDropList
- Limitation: Windows clipboard converts all image formats to PNG when reading back
- Read: NSPasteboard (PyObjC) + AppleScript fallback
- Check file types (NSFilenamesPboardType, public.file-url) → Check image (PNGf) → Fallback to text
- UTI mapping for images: public.png, public.jpeg, com.microsoft.bmp, etc.
- Write: NSPasteboard (PyObjC) + AppleScript fallback
- Plain text: AppleScript
set the clipboard to "..." - Images: NSPasteboard.setData_forType_() with UTI types
- Files: NSPasteboard.writeObjects_() with NSURL + bookmark data for Finder
- Plain text: AppleScript
- Read: xclip
- Check TARGETS → text/uri-list (files, checked first) → image → text
- Files: Read URI, verify file exists, read content
- Images: Check for JPEG, PNG, BMP signatures in order
- Write: xclip
- Plain text:
xclip -selection clipboard - All other types (images, files): Create temp file +
xclip -selection clipboard -t text/uri-list
- Plain text:
- Background process:
linux_uri_loopdaemon monitors clipboard and converts file:// URIs to text/uri-list format- Run in background as daemon process (started automatically on Linux)
- Checks clipboard every 0.05 seconds
- When clipboard contains
file://in UTF8_STRING but notext/uri-listformat:- Converts and sets the content as
text/uri-list
- Converts and sets the content as
- Ensures proper file paste operation when using copy_to_clipboard()
- Environment: Windows with PowerShell
- Symptoms:
- Reading clipboard text returns content with extra
\nat the end GetText()output is correct, but PowerShell console adds newline
- Reading clipboard text returns content with extra
- Root cause: PowerShell's Write-Output always appends a newline to string output
- Solution: Strip trailing newline when reading text
text_content = result.stdout if text_content.endswith("\n"): text_content = text_content[:-1] self._raw = text_content.encode("utf-8")
- Environment: Windows with large images (>8KB base64)
- Symptoms:
[WinError 206] 文件名或扩展名太长(The filename or extension is too long)- Image copy fails for files larger than a few KB
- Root cause: Passing base64-encoded image data in command line exceeds Windows limit
- Solution: Write image to temp file, then load with .NET
# Write image to temp file first temp_image_file = os.path.join(_TEMP_DIR, f"{hash_obj}.{content_type}") with open(temp_image_file, "wb") as f: f.write(data) # Load from file and set to clipboard script = ( f'Add-Type -AssemblyName System.Windows.Forms; ' f'Add-Type -AssemblyName System.Drawing; ' f'$img = [System.Drawing.Image]::FromFile("{temp_image_file}"); ' f'[System.Windows.Forms.Clipboard]::SetImage($img); ' f'$img.Dispose()' ) # Clean up temp file after setting clipboard os.unlink(temp_image_file)
- Environment: Windows with empty text
- Symptoms:
CalledProcessErrorwhen trying to set empty text to clipboard
- Root cause:
[System.Windows.Forms.Clipboard]::SetText()throws exception for empty string - Solution: Use
Clear()for empty textif not text: script = ( "Add-Type -AssemblyName System.Windows.Forms; " "[System.Windows.Forms.Clipboard]::Clear()" )
- Environment: Windows Explorer, QQ, etc.
- Symptoms:
- After
copy_to_clipboard('txt', ...), pasting in Explorer shows no response - FileDropList is set correctly but applications can't paste the file
- After
- Root cause: Mixed path format
/tmp/spclipbd_files\...- Git Bash's/tmpmapped to Windows temp, but path separators were inconsistent - Solution: Use Windows-native temp directory path with
tempfile.gettempdir()import tempfile # Use Windows-native temp directory path for proper Explorer compatibility _win_temp_dir = os.path.join(tempfile.gettempdir(), "spclipbd_files") # Results in: C:\Users\xxx\AppData\Local\Temp\spclipbd_files\...
- Environment: Linux with xclip version 0.13
- Symptoms:
- Reading clipboard with
xclip -t image/jpeg -otimes out after copying JPEG - Even checking
TARGETStimes out after copying JPEG with MIME type
- Reading clipboard with
- Root cause: xclip 0.13 has a severe bug when handling image/jpeg MIME type
- Solution: Use file URI method (
text/uri-list) instead ofimage/jpegMIME type# For jpg/jpeg on Linux, create temp file and use text/uri-list os.makedirs(_TEMP_DIR, exist_ok=True) hash_obj = hashlib.md5(data).hexdigest() temp_file = os.path.join(_TEMP_DIR, f"{hash_obj}.jpg") with open(temp_file, "wb") as f: f.write(data) file_uri = f"file://{quote(temp_file)}" subprocess.run( ["xclip", "-selection", "clipboard", "-t", "text/uri-list"], input=file_uri.encode("utf-8"), timeout=30, check=True, )
- Note: This approach works correctly with GNOME's clipboard manager and applications
- Symptoms:
UnboundLocalError: cannot access local variable 'quote' where it is not associated with a value - Root cause: In Python, when a variable is assigned anywhere in a function (including
importstatement), it becomes a local variable for the entire function scope, not just after the assignment - Example of problem:
def _copy_linux(content_type: str, data: bytes) -> TempFile: if content_type in image_mime_types: # This import makes 'quote' local for the entire function! from urllib.parse import quote file_uri = f"file://{quote(temp_file)}" ... else: # This line fails because 'quote' is considered local but not yet assigned file_uri = f"file://{quote(temp_file)}" # UnboundLocalError!
- Solution: Use module-level imports instead of local imports
# At top of file from urllib.parse import quote def _copy_linux(content_type: str, data: bytes) -> TempFile: if condition: file_uri = f"file://{quote(temp_file)}" # Works! else: file_uri = f"file://{quote(temp_file)}" # Also works!
- Bug fixed: Was using
lstrip('file://')which stripped characters individually - Solution: Use
uri.startswith("file://")check anduri[7:]to remove prefix
- Linux/Windows:
/tmp/spclipbd_files/(or platform equivalent) - macOS:
/tmp/directly (Finder may have issues with subdirectories in /tmp) - Naming: MD5 hash of content + extension (macOS uses first 12 chars only)
- No auto-deletion: Files persist until user calls
TempFile.delete()
- Same content creates same temp file path
- Different content creates different temp file paths
- Efficient for repeated operations
- JPEG: Check for
b'\xff\xd8'at start of file - PNG: Check for
b'\x89PNG'at start of file - BMP: Check for
b'BM'at start of file - Always verify signature when reading images to avoid false positives
When testing on Windows, verify:
- Plaintext copy/paste:
_plaintextcontent_type works correctly - Text file copy/paste:
txt,json,zipetc. create temp files correctly - Image copy/paste: Test
png,jpgformats- Note: Windows clipboard converts all image formats to PNG when reading back
- This is a Windows API limitation, not a bug
- ClipboardContent reading: Ensure suffix is correct (no leading dot)
- File drop list: Verify
SetFileDropListworks for pasting files - TempFile cleanup: Verify
delete()works correctly - PowerShell timeout: Verify clipboard operations complete within timeout (10s read, 30s write)
- Empty text: Verify empty text is handled correctly (uses Clear())
- Large images: Verify large images (>100KB) work correctly (uses temp file method)
When testing on Linux, verify:
- Plaintext copy/paste:
_plaintextcontent_type with xclip - Text file copy/paste: Files use
text/uri-listformat correctly - Image copy/paste: Test
png,bmpformats (avoidimage/jpegMIME type on xclip 0.13) - File URI conversion: Background daemon correctly converts
file://totext/uri-list - ClipboardContent reading: Check TARGETS before reading specific format
- TempFile cleanup: Verify
delete()works correctly - xclip version check: Handle xclip 0.13 JPEG bug properly
When testing on macOS, verify:
- Plaintext copy/paste:
_plaintextcontent_type with AppleScript - Text file copy/paste: Files use NSPasteboard with NSURL and bookmark API for Finder
- Image copy/paste: Test
png,jpgformats with UTI types via NSPasteboard - ClipboardContent reading: Check NSFilenamesPboardType, public.file-url, then images
- TempFile cleanup: Verify
delete()works correctly - Finder compatibility: Files pasted to Finder work correctly (bookmark API)
- PyObjC dependency: Ensure
AppKitandFoundationare installed
- Don't use
lstrip()for removing prefixes - usestartswith()+ slicing - Don't use local imports inside functions if the name is used elsewhere in the function
- Always handle empty content gracefully
- Check for file existence before reading from URI
- Problem:
get_suffix()returned.png,.txtinstead ofpng,txt - Solution: Remove dot using
ext[1:].lower()orlstrip(".")
- Problem: Copying files returned file path as text content instead of reading file
- Solution: Check for
text/uri-listin targets BEFORE checking for images
- Problem:
lstrip('file://')removed characters individually instead of prefix - Solution: Use
uri.startswith("file://")check anduri[7:]slicing
- Problem:
copy_to_clipboard('txt', ...)was treated as image - Solution: Only treat known image types as images, everything else as files
- Problem: Reading clipboard after copying JPEG causes timeout
- Solution: Use
text/uri-listmethod instead ofimage/jpegMIME type for JPEG
- Problem: Variable used before assignment when import is in different branch
- Solution: Use module-level imports, not local imports inside functions
spclipbd.py- Main moduletest.py- Comprehensive test suitesptest.py- User's test script (do not modify)prompt.md- Original requirementsREADME.md- Project documentationclient.py,server.py- Network clipboard implementation (separate from spclipbd.py)