1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md) - Complete reference for all @advancedpaste: header tags - Windows and WSL/Linux execution mode protocols - Declaring dependencies, security trust model, error handling - Example scripts for both platforms 2. Custom scripts folder setting in Settings UI - Added ScriptsFolder property to AdvancedPasteViewModel - Added SettingsCard with TextBox + Browse folder dialog in XAML - Added localization strings for the new setting 3. Auto-detect missing Python modules from import statements - Scans script body for import/from-import statements - Filters Python stdlib modules (CPython 3.12 set) - Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.) - Merges auto-detected imports with explicit @advancedpaste:requires entries - Explicit requires always take precedence 4. Better error messages for Python script failures - Parses stderr to extract the final Python exception line - User-friendly summaries for ModuleNotFoundError, SyntaxError, etc. - ModuleNotFoundError includes pip install hint from the mapping table - Full traceback available in Details section of the error UI Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError. Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7.5 KiB
Advanced Paste – Python Scripts
Advanced Paste supports user-defined Python scripts that transform clipboard content. Scripts are discovered automatically from a configurable folder and appear as actions in the Advanced Paste UI.
Quick start
- Open the scripts folder — by default
%LOCALAPPDATA%\Microsoft\PowerToys\AdvancedPaste\Scripts. You can change this in Settings → Advanced Paste → Python scripts → Scripts folder. - Drop a
.pyfile into the folder. - Add the required header comments at the top (see Header format).
- Open the Advanced Paste UI (
Win+Shift+V) — your script will appear in the action list.
Header format
Every script must start with one or more header comment lines. Each line follows the pattern:
# @advancedpaste:<tag> <value>
The parser reads the first 50 lines of each file; only lines beginning with # are inspected.
Supported tags
| Tag | Required | Description |
|---|---|---|
name |
Yes | Display name shown in the Advanced Paste UI. |
desc |
No | Short description / tooltip. |
formats |
No | Comma-separated list of supported clipboard formats. Defaults to all formats when omitted. |
platform |
No | windows (default) or linux. Determines the execution mode (see below). |
version |
No | Free-form version string (reserved for future use). |
requires |
No | Space-separated Python package requirements. See Declaring dependencies. |
Formats
| Value | Clipboard content |
|---|---|
text |
Plain or Unicode text (CF_UNICODETEXT) |
html |
HTML fragment (CF_HTML) |
image |
Bitmap / PNG image |
audio |
Audio file(s) |
video |
Video file(s) |
files or file |
File paths (CF_HDROP / StorageItems) |
any |
All of the above |
Multiple values can be combined with commas:
# @advancedpaste:formats text,html
Execution modes
Windows mode (platform windows)
The script runs directly on Windows via the configured Python interpreter.
It owns the clipboard — use a library like pywin32 (win32clipboard) to read
and write clipboard data.
Invocation:
python.exe "<script.py>" --format <detected_format> --work-dir "<temp_dir>"
Minimal example — reverse text:
# @advancedpaste:name Reverse text
# @advancedpaste:formats text
# @advancedpaste:platform windows
import win32clipboard
win32clipboard.OpenClipboard()
text = win32clipboard.GetClipboardData(win32clipboard.CF_UNICODETEXT)
win32clipboard.EmptyClipboard()
win32clipboard.SetClipboardData(win32clipboard.CF_UNICODETEXT, text[::-1])
win32clipboard.CloseClipboard()
After the script exits with code 0, Advanced Paste re-reads the clipboard and pastes the result. A non-zero exit code signals failure; stderr is shown in the error UI.
WSL / Linux mode (platform linux)
The script runs inside WSL via wsl.exe bash -l -c "python3 -X utf8 <script>".
Instead of direct clipboard access, data is exchanged via JSON:
| Direction | Channel | Schema |
|---|---|---|
| Input (C# → Python) | stdin (JSON) |
See Input payload |
| Output (Python → C#) | stdout (JSON) |
See Output payload |
Input payload
{
"version": 2,
"format": ["text"], // array of detected clipboard format names
"work_dir": "/mnt/c/...", // writable temp directory (WSL path)
"text": "Hello, world!", // present when clipboard has text
"html": "<b>Hello</b>", // present when clipboard has HTML
"image_path": "/mnt/c/.../input.png", // present when clipboard has an image
"file_paths": ["/mnt/c/.../file.txt"] // present when clipboard has files
}
Output payload
{
"result_type": "text", // "text" | "html" | "image" | "file" | "files"
"text": "HELLO, WORLD!", // for result_type "text"
"html": "<b>HELLO</b>", // for result_type "html"
"image_path": "/mnt/c/.../output.png", // for result_type "image"
"file_paths": ["/mnt/c/.../out.txt"] // for result_type "file"/"files"
}
Note: File paths in the output must use
/mnt/<drive>/...format so that Advanced Paste can map them back to Windows paths.
Minimal example — uppercase text (WSL):
# @advancedpaste:name WSL Upper Case
# @advancedpaste:formats text
# @advancedpaste:platform linux
import sys, json
data = json.load(sys.stdin)
text = data.get("text", "")
json.dump({"result_type": "text", "text": text.upper()}, sys.stdout)
Declaring dependencies
Use requires to declare Python packages the script needs:
# @advancedpaste:requires markitdown='markitdown[all]'
# @advancedpaste:requires cv2=opencv-python-headless numpy requests
Each token is either:
import_name— the pip package is assumed to have the same name (e.g.requests).import_name=pip_package— when the import name differs from the pip package (e.g.cv2=opencv-python-headless,PIL=Pillow).
Multiple tokens on one line are space-separated. You can also use multiple requires lines.
Automatic import detection
Advanced Paste also scans the script body for import and from ... import statements
and cross-references them against the Python standard library. Any non-stdlib import
that is not already installed triggers a prompt to install it automatically.
A built-in mapping table handles common mismatches (e.g. win32clipboard → pywin32,
cv2 → opencv-python, PIL → Pillow). For uncommon packages where the import name
differs from the pip name, add an explicit requires entry.
Security — script trust
The first time a script is executed (or after it has been modified), Advanced Paste shows a confirmation dialog. Upon approval the SHA-256 hash of the script is stored. Subsequent runs of the unchanged file skip the dialog.
Error handling
When a script fails, Advanced Paste extracts the Python traceback from stderr and displays a user-friendly summary in the UI:
- ModuleNotFoundError — identifies the missing module and suggests installing it.
- SyntaxError — shows the file and line number.
- Timeout — shows the configured timeout value (default 30 s; configurable in Settings).
- Other errors — shows the last line of the traceback as a summary, with the full traceback available in the expandable Details section.
Settings
The following settings are available under Settings → Advanced Paste → Python scripts:
| Setting | Description | Default |
|---|---|---|
| Python interpreter | Path to the Python executable. Leave blank for auto-detection. | (auto-detect) |
| Scripts folder | Folder to scan for .py scripts. |
%LOCALAPPDATA%\Microsoft\PowerToys\AdvancedPaste\Scripts |
Tips
- Put reusable helper functions in a separate
.pyfile without a# @advancedpaste:nameheader — it will be ignored by the script discovery and can be imported by other scripts. - For complex WSL scripts that need packages not available via
apt, consider using a virtual environment. The script can re-exec itself with the venv interpreter:import os, sys venv = os.path.expanduser("~/my_env/bin/python3") if os.path.exists(venv) and sys.executable != venv: os.execv(venv, [venv] + sys.argv) - The
--work-dirargument (Windows mode) andwork_dirJSON field (WSL mode) point to a temporary directory that is cleaned up after execution. Use it for intermediate files.