mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-02 00:19:16 +02:00
Compare commits
69 Commits
copilot/su
...
user/muyua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f283a568c3 | ||
|
|
873aa42d7f | ||
|
|
32f6b9e18c | ||
|
|
240bf6957d | ||
|
|
b3fe9f608e | ||
|
|
1af0dba08c | ||
|
|
4d7575fcfb | ||
|
|
8c1cf09802 | ||
|
|
796b232035 | ||
|
|
a48efdb6cb | ||
|
|
78d05c9082 | ||
|
|
107c7c4cdc | ||
|
|
4394357013 | ||
|
|
214d80415a | ||
|
|
7418da8806 | ||
|
|
8778af2e32 | ||
|
|
aa3f1165b3 | ||
|
|
df45e56511 | ||
|
|
f8bca48db3 | ||
|
|
5104d0846c | ||
|
|
8ca6c4d2ec | ||
|
|
574aca841b | ||
|
|
0833b68907 | ||
|
|
2051c13bf9 | ||
|
|
4c7bf3df79 | ||
|
|
879163f48e | ||
|
|
4b84c00300 | ||
|
|
6062bdc2f8 | ||
|
|
3e1b07f52c | ||
|
|
96e6542cf1 | ||
|
|
12fac01ee1 | ||
|
|
f2788f2e09 | ||
|
|
5dea1980ad | ||
|
|
e74692815f | ||
|
|
8c1e4f16fe | ||
|
|
e653b4ad37 | ||
|
|
70bf430d9f | ||
|
|
494c14fb88 | ||
|
|
6c806aa08c | ||
|
|
7a0e4ac891 | ||
|
|
cdeae7c854 | ||
|
|
9ae355b963 | ||
|
|
9b7ae9a96a | ||
|
|
169bfe3f04 | ||
|
|
1b4641a158 | ||
|
|
a94d010a8d | ||
|
|
c013122520 | ||
|
|
5d11e8e805 | ||
|
|
83f26d4684 | ||
|
|
07b8915e19 | ||
|
|
4f5837d4e9 | ||
|
|
e8ccb7099e | ||
|
|
6fe4361a20 | ||
|
|
91634922fc | ||
|
|
8d0f8e5b49 | ||
|
|
15cad8ca18 | ||
|
|
65254cec76 | ||
|
|
138c66c328 | ||
|
|
196b9305c3 | ||
|
|
009ee75de0 | ||
|
|
368490ef79 | ||
|
|
fafb582ae2 | ||
|
|
ed76886d98 | ||
|
|
b64afea9f7 | ||
|
|
5e30caa674 | ||
|
|
0f87b61dad | ||
|
|
39bfa86335 | ||
|
|
dcf4c4d16d | ||
|
|
de25059de0 |
12
.github/actions/spell-check/allow/code.txt
vendored
12
.github/actions/spell-check/allow/code.txt
vendored
@@ -315,6 +315,7 @@ xef
|
||||
xes
|
||||
PACKAGEVERSIONNUMBER
|
||||
APPXMANIFESTVERSION
|
||||
PROGMAN
|
||||
|
||||
# MRU lists
|
||||
CACHEWRITE
|
||||
@@ -325,6 +326,14 @@ REGSTR
|
||||
# Misc Win32 APIs and PInvokes
|
||||
INVOKEIDLIST
|
||||
MEMORYSTATUSEX
|
||||
ABE
|
||||
HTCAPTION
|
||||
POSCHANGED
|
||||
QUERYPOS
|
||||
SETAUTOHIDEBAR
|
||||
WINDOWPOS
|
||||
WINEVENTPROC
|
||||
WORKERW
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
@@ -349,3 +358,6 @@ nostdin
|
||||
# Performance counter keys
|
||||
engtype
|
||||
Nonpaged
|
||||
|
||||
# XAML
|
||||
Untargeted
|
||||
|
||||
4
.github/actions/spell-check/excludes.txt
vendored
4
.github/actions/spell-check/excludes.txt
vendored
@@ -110,8 +110,8 @@
|
||||
^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$
|
||||
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
|
||||
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/.*\.TestData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/Text/.*\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/.*\.TestData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/Text/.*\.cs$
|
||||
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
|
||||
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
||||
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
|
||||
|
||||
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -845,6 +845,7 @@ MOUSEINPUT
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
MRM
|
||||
MRT
|
||||
mru
|
||||
msc
|
||||
mscorlib
|
||||
@@ -2037,7 +2038,6 @@ mousehighlighter
|
||||
mousejump
|
||||
mousepointer
|
||||
mouseutils
|
||||
mrt
|
||||
MSAL
|
||||
msdia
|
||||
MSIs
|
||||
|
||||
@@ -211,9 +211,6 @@
|
||||
"PowerToys.PowerAccentModuleInterface.dll",
|
||||
"PowerToys.PowerAccentKeyboardService.dll",
|
||||
|
||||
"PowerToys.PowerDisplayModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
|
||||
"PowerDisplay.Lib.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
|
||||
|
||||
@@ -78,10 +78,10 @@
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.251104000" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.39" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.251106002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260209005" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.260203002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.47" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.260209005" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
|
||||
@@ -196,6 +196,10 @@
|
||||
<Folder Name="/modules/CommandPalette/">
|
||||
<Project Path="src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj" Id="5f63c743-f6ce-4dba-a200-2b3f8a14e8c2" />
|
||||
<Project Path="src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj" Id="0adeb797-c8c7-4ffa-acd5-2af6cad7ecd8" />
|
||||
<Project Path="src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Built-in Extensions/">
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj">
|
||||
@@ -275,16 +279,6 @@
|
||||
<Deploy />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Core/">
|
||||
<Project Path="src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Microsoft.CmdPal.Core.Common.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Extension SDK/">
|
||||
<Project Path="src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Microsoft.CommandPalette.Extensions.Toolkit.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
@@ -305,7 +299,7 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/CommandPalette/Tests/">
|
||||
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Core.Common.UnitTests/Microsoft.CmdPal.Core.Common.UnitTests.csproj">
|
||||
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Common.UnitTests/Microsoft.CmdPal.Common.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
@@ -694,11 +688,13 @@
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<!-- TEMPORARILY_DISABLED: PowerDisplay
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
|
||||
-->
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/Tests/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">
|
||||
|
||||
285
doc/devdocs/modules/advancedpaste-python-scripts.md
Normal file
285
doc/devdocs/modules/advancedpaste-python-scripts.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# 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
|
||||
|
||||
1. Open the scripts folder — by default `%LOCALAPPDATA%\Microsoft\PowerToys\AdvancedPaste\Scripts`.
|
||||
You can change this in **Settings → Advanced Paste → Python scripts → Scripts folder**.
|
||||
2. Drop a `.py` file into the folder.
|
||||
3. Define one `advanced_paste_from_<input>_to_<output>` function (see [Writing a script](#writing-a-script)).
|
||||
4. Open the Advanced Paste UI (`Win+Shift+V`) — your script will appear in the action list.
|
||||
|
||||
> **Important:** Each `.py` file must define exactly one `advanced_paste_from_<input>_to_<output>`
|
||||
> function. Scripts with zero or multiple such functions are ignored.
|
||||
|
||||
## Writing a script
|
||||
|
||||
You write a single Python function whose **name** declares both what clipboard input it accepts
|
||||
and what output type it produces.
|
||||
No imports from PowerToys are needed — zero setup, zero dependencies on our side.
|
||||
|
||||
### Function naming convention
|
||||
|
||||
The function name follows the pattern:
|
||||
|
||||
```
|
||||
advanced_paste_from_<input>_to_<output>(<param>)
|
||||
```
|
||||
|
||||
**Input types** (what the function receives):
|
||||
|
||||
| Input | Parameter | When it runs |
|
||||
|-------|-----------|--------------|
|
||||
| `text` | `str` — clipboard text | Clipboard has text |
|
||||
| `html` | `str` — clipboard HTML | Clipboard has HTML |
|
||||
| `image` | `str` — path to temp image file | Clipboard has an image |
|
||||
| `audio` | `str` — path to audio file | Clipboard has an audio file |
|
||||
| `video` | `str` — path to video file | Clipboard has a video file |
|
||||
| `files` | `list[str]` — file paths | Clipboard has files |
|
||||
|
||||
**Output types** (what the function produces — declared via `_to_` suffix):
|
||||
|
||||
| Output | Effect |
|
||||
|--------|--------|
|
||||
| `text` | Sets clipboard to text |
|
||||
| `html` | Sets clipboard to HTML |
|
||||
| `image` | Sets clipboard to image |
|
||||
| `audio` | Sets clipboard to audio file |
|
||||
| `video` | Sets clipboard to video file |
|
||||
| `file` | Sets clipboard to a file |
|
||||
| `files` | Sets clipboard to multiple files |
|
||||
|
||||
### Return value
|
||||
|
||||
The return value is interpreted according to the declared output type:
|
||||
|
||||
| Output type | Expected return value |
|
||||
|-------------|---------------------|
|
||||
| `text` | `str` (or any value — will be converted via `str()`) |
|
||||
| `html` | `str` containing HTML |
|
||||
| `image` | `str` or `pathlib.Path` pointing to an image file |
|
||||
| `file` | `str` or `pathlib.Path` pointing to a file |
|
||||
| `files` | `list` of `str`/`pathlib.Path` file paths |
|
||||
|
||||
Returning `None` produces an empty result (no-op).
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal — uppercase text
|
||||
|
||||
```python
|
||||
def advanced_paste_from_text_to_text(text):
|
||||
return text.upper()
|
||||
```
|
||||
|
||||
That's it. No headers required, no imports from PowerToys.
|
||||
|
||||
### With optional metadata
|
||||
|
||||
```python
|
||||
# @advancedpaste:name Reverse Text
|
||||
# @advancedpaste:desc Reverses clipboard text character by character
|
||||
|
||||
def advanced_paste_from_text_to_text(text):
|
||||
return text[::-1]
|
||||
```
|
||||
|
||||
### Text to HTML
|
||||
|
||||
```python
|
||||
# @advancedpaste:name Markdown Table to HTML
|
||||
# @advancedpaste:desc Convert a markdown table to an HTML table
|
||||
|
||||
def advanced_paste_from_text_to_html(text):
|
||||
headers = text.splitlines()[0].split("|")
|
||||
return f"<table><tr>{''.join(f'<th>{h.strip()}</th>' for h in headers if h.strip())}</tr></table>"
|
||||
```
|
||||
|
||||
### Image to text (OCR)
|
||||
|
||||
```python
|
||||
# @advancedpaste:requires pytesseract
|
||||
|
||||
def advanced_paste_from_image_to_text(image_path):
|
||||
import pytesseract
|
||||
return pytesseract.image_to_string(image_path).strip()
|
||||
```
|
||||
|
||||
### Save text as file
|
||||
|
||||
```python
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
def advanced_paste_from_text_to_file(text):
|
||||
# Use ADVANCED_PASTE_WORK_DIR for WSL compatibility; falls back to temp dir on Windows.
|
||||
out_dir = os.environ.get("ADVANCED_PASTE_WORK_DIR") or tempfile.gettempdir()
|
||||
out = Path(out_dir) / "clipboard.txt"
|
||||
out.write_text(text, encoding="utf-8")
|
||||
return out
|
||||
```
|
||||
|
||||
### Image processing (image → image)
|
||||
|
||||
```python
|
||||
import os
|
||||
from PIL import Image
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
|
||||
def advanced_paste_from_image_to_image(image_path):
|
||||
"""Convert image to grayscale."""
|
||||
img = Image.open(image_path).convert("L")
|
||||
out_dir = os.environ.get("ADVANCED_PASTE_WORK_DIR") or tempfile.gettempdir()
|
||||
out = Path(out_dir) / "gray.png"
|
||||
img.save(out)
|
||||
return out
|
||||
```
|
||||
|
||||
### File listing (files → text)
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
def advanced_paste_from_files_to_text(file_paths):
|
||||
lines = []
|
||||
for p in file_paths:
|
||||
size = os.path.getsize(p)
|
||||
lines.append(f"{os.path.basename(p)} ({size} bytes)")
|
||||
return "\n".join(lines)
|
||||
```
|
||||
|
||||
## Header tags
|
||||
|
||||
All header tags are **optional**. Tags are placed in comment lines at the top of the script.
|
||||
|
||||
| Tag | Description |
|
||||
|-----|-------------|
|
||||
| `name` | Display name in the Advanced Paste UI. If omitted, the filename is used. |
|
||||
| `desc` | Short description / tooltip. |
|
||||
| `disabled` | Presence of this tag disables the script (it won't appear in the UI). |
|
||||
| `requires` | Declare Python package dependencies (see [Dependencies](#declaring-dependencies)). |
|
||||
|
||||
### Example header
|
||||
|
||||
```python
|
||||
# @advancedpaste:name My Formatter
|
||||
# @advancedpaste:desc Formats clipboard text as markdown table
|
||||
```
|
||||
|
||||
To disable a script without deleting it, add:
|
||||
|
||||
```python
|
||||
# @advancedpaste:disabled
|
||||
```
|
||||
|
||||
Remove the line to re-enable.
|
||||
|
||||
## Declaring dependencies
|
||||
|
||||
Use `requires` to declare Python packages the script needs:
|
||||
|
||||
```python
|
||||
# @advancedpaste:requires PIL=Pillow
|
||||
# @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`).
|
||||
|
||||
### 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.
|
||||
|
||||
## 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 Python scripts feature uses a **mode selector** (dropdown) with three options:
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| **Disabled** | Python scripts are not active. |
|
||||
| **Windows** | Scripts run using a native Windows Python interpreter. |
|
||||
| **WSL** | Scripts run inside Windows Subsystem for Linux. |
|
||||
|
||||
Each mode maintains its own independent settings (scripts folder, interpreter path, etc.),
|
||||
so switching between Windows and WSL does not lose your previous configuration.
|
||||
|
||||
### Windows mode settings
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| Scripts folder | Folder to scan for `.py` scripts. | `%LOCALAPPDATA%\Microsoft\PowerToys\AdvancedPaste\Scripts` |
|
||||
| Python interpreter | Path to the Python executable. Leave blank for auto-detection. | *(auto-detect)* |
|
||||
|
||||
### WSL mode settings
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| Scripts folder | Folder to scan for `.py` scripts (Windows path — auto-translated to `/mnt/...`). | `%LOCALAPPDATA%\Microsoft\PowerToys\AdvancedPaste\Scripts` |
|
||||
| WSL distribution | Which WSL distro to use (e.g. `Ubuntu`). Leave blank for the default distribution. | *(default)* |
|
||||
|
||||
### Scripts list
|
||||
|
||||
The Settings page shows a read-only list of discovered scripts. For each script you can see:
|
||||
|
||||
- **Name** — from `@advancedpaste:name` tag, or the filename if not set.
|
||||
- **Description** — from `@advancedpaste:desc` tag.
|
||||
- **Conversion** — the input → output types detected from the function name (e.g. "text → image").
|
||||
|
||||
The list is **not editable** from Settings. To change a script's name, description, enabled state,
|
||||
or any other metadata, open the script file directly (click the "Open in editor" button) and edit
|
||||
the `# @advancedpaste:...` header tags. After saving, click **Refresh** in Settings to reload.
|
||||
|
||||
### WSL mode details
|
||||
|
||||
When **WSL** mode is selected:
|
||||
|
||||
- Scripts are executed via `wsl.exe bash -l -c "python3 ..."` using the configured distribution.
|
||||
- The scripts folder remains on the Windows filesystem; paths are automatically translated
|
||||
to `/mnt/c/...` format for WSL access.
|
||||
- Package installation uses `pip3 install` inside the WSL environment.
|
||||
- Output files from scripts must be written under `/mnt/` (the Windows-mounted filesystem)
|
||||
so they can be accessed from Windows. The runner sets the `ADVANCED_PASTE_WORK_DIR` environment
|
||||
variable to a temp directory under `/mnt/c/...` — use it instead of `tempfile.gettempdir()`
|
||||
when producing file output for cross-platform compatibility.
|
||||
|
||||
> **Tip:** If you have Python installed only in WSL (not on Windows), select WSL mode
|
||||
> to use your existing WSL Python environment with all its packages.
|
||||
|
||||
## Tips
|
||||
|
||||
- Each `.py` file must contain exactly one `advanced_paste_from_<input>_to_<output>` function.
|
||||
If you need to handle multiple input types, create separate script files for each.
|
||||
- A `.py` file without any matching function is ignored — use this for helper modules
|
||||
that other scripts can import.
|
||||
- Scripts can be tested from the command line:
|
||||
```
|
||||
echo {"format":["text"],"text":"hello"} | python _runner.py my_script.py
|
||||
```
|
||||
- The script's directory is added to `sys.path` at runtime, so you can import sibling `.py`
|
||||
files as helper modules.
|
||||
@@ -96,6 +96,10 @@ See [Debugging](development/debugging.md) for detailed debugging techniques, inc
|
||||
|
||||
See [Creating a New PowerToy](development/new-powertoy.md) for an end-to-end guide covering module architecture, settings integration, installer packaging, and testing.
|
||||
|
||||
### Building Command Palette Extensions
|
||||
|
||||
If you want to build your own extensions for Command Palette, check out the [Command Palette extensibility documentation](https://aka.ms/building-cmdpal-extensions). It covers how to create, package, and distribute custom extensions that integrate with Command Palette.
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
- [Coding Guidelines](development/guidelines.md) - Development guidelines and best practices
|
||||
|
||||
@@ -1565,7 +1565,6 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.PowerRename.exe",
|
||||
L"PowerToys.ImageResizer.exe",
|
||||
L"PowerToys.LightSwitchService.exe",
|
||||
L"PowerToys.PowerDisplay.exe",
|
||||
L"PowerToys.GcodeThumbnailProvider.exe",
|
||||
L"PowerToys.BgcodeThumbnailProvider.exe",
|
||||
L"PowerToys.PdfThumbnailProvider.exe",
|
||||
|
||||
@@ -47,7 +47,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
|
||||
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
|
||||
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
|
||||
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
|
||||
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
|
||||
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
|
||||
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
|
||||
@@ -124,7 +123,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
<Compile Include="KeyboardManager.wxs" />
|
||||
<Compile Include="Peek.wxs" />
|
||||
<Compile Include="PowerRename.wxs" />
|
||||
<Compile Include="PowerDisplay.wxs" />
|
||||
<Compile Include="DscResources.wxs" />
|
||||
<Compile Include="RegistryPreview.wxs" />
|
||||
<Compile Include="Run.wxs" />
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
@@ -176,10 +176,6 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
|
||||
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
|
||||
Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
|
||||
|
||||
#PowerDisplay
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay"
|
||||
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
|
||||
|
||||
#New+
|
||||
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
|
||||
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs
|
||||
|
||||
@@ -117,4 +117,4 @@
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -22,13 +22,13 @@ using System.Diagnostics.CodeAnalysis;
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.SpecialRules", "SA0001:XmlCommentAnalysisDisabled", Justification = "Not enabled as we don't want or need XML documentation.")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1629:DocumentationTextMustEndWithAPeriod", Justification = "Not enabled as we don't want or need XML documentation.")]
|
||||
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly", Scope = "member", Target = "Microsoft.Templates.Core.Locations.TemplatesSynchronization.#SyncStatusChanged", Justification = "Using an Action<object, SyncStatusEventArgs> does not allow the required notation")]
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly", Scope = "member", Target = "Microsoft.Templates.Locations.TemplatesSynchronization.#SyncStatusChanged", Justification = "Using an Action<object, SyncStatusEventArgs> does not allow the required notation")]
|
||||
|
||||
// Non general suppressions
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "This is part of the markdown processing", MessageId = "System.Windows.Documents.Run.#ctor(System.String)", Scope = "member", Target = "Microsoft.Templates.UI.Controls.Markdown.#ImageInlineEvaluator(System.Text.RegularExpressions.Match)")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We need to have the names of these keys in lowercase to be able to compare with the keys becoming form the template json. ContainsKey does not allow StringComparer specification to IgnoreCase", Scope = "member", Target = "Microsoft.Templates.Core.ITemplateInfoExtensions.#GetQueryableProperties(Microsoft.TemplateEngine.Abstractions.ITemplateInfo)")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We need to have the names of these keys in lowercase to be able to compare with the keys becoming form the template json. ContainsKey does not allow StringComparer specification to IgnoreCase", Scope = "member", Target = "Microsoft.Templates.Core.Composition.CompositionQuery.#Match(System.Collections.Generic.IEnumerable`1<Microsoft.Templates.Core.Composition.QueryNode>,Microsoft.Templates.Core.Composition.QueryablePropertyDictionary)")]
|
||||
[assembly: SuppressMessage("Usage", "VSTHRD103:Call async methods when in an async method", Justification = "Resource DictionaryWriter does not implement flush async", Scope = "member", Target = "~M:Microsoft.Templates.Core.PostActions.Catalog.Merge.MergeResourceDictionaryPostAction.ExecuteInternalAsync~System.Threading.Tasks.Task")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We need to have the names of these keys in lowercase to be able to compare with the keys becoming form the template json. ContainsKey does not allow StringComparer specification to IgnoreCase", Scope = "member", Target = "Microsoft.Templates.ITemplateInfoExtensions.#GetQueryableProperties(Microsoft.TemplateEngine.Abstractions.ITemplateInfo)")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We need to have the names of these keys in lowercase to be able to compare with the keys becoming form the template json. ContainsKey does not allow StringComparer specification to IgnoreCase", Scope = "member", Target = "Microsoft.Templates.Composition.CompositionQuery.#Match(System.Collections.Generic.IEnumerable`1<Microsoft.Templates.Composition.QueryNode>,Microsoft.Templates.Composition.QueryablePropertyDictionary)")]
|
||||
[assembly: SuppressMessage("Usage", "VSTHRD103:Call async methods when in an async method", Justification = "Resource DictionaryWriter does not implement flush async", Scope = "member", Target = "~M:Microsoft.Templates.PostActions.Catalog.Merge.MergeResourceDictionaryPostAction.ExecuteInternalAsync~System.Threading.Tasks.Task")]
|
||||
[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Used in a lot of places for meaningful method names")]
|
||||
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Static methods may improve performance but decrease maintainability")]
|
||||
[assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Renaming everything would be a lot of work. It does not do any harm if an EventHandler delegate ends with the suffix EventHandler. Besides this, the Rule causes some false positives.")]
|
||||
@@ -43,10 +43,10 @@ using System.Diagnostics.CodeAnalysis;
|
||||
[assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.ViewModels.Common.SavedTemplateViewModel.OnDelete(Microsoft.Templates.UI.ViewModels.Common.SavedTemplateViewModel)")]
|
||||
|
||||
// Localization suppressions
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#CreateJunction(System.String,System.String,System.Boolean)", Justification = "Only used for local generation")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#DeleteJunction(System.String)", Justification = "Only used for local generation")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#InternalGetTarget(Microsoft.Win32.SafeHandles.SafeFileHandle)", Justification = "Only used for local generation")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Core.Locations.JunctionNativeMethods.#OpenReparsePoint(System.String,Microsoft.Templates.Core.Locations.JunctionNativeMethods+EFileAccess)", Justification = "Only used for local generation")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Locations.JunctionNativeMethods.#CreateJunction(System.String,System.String,System.Boolean)", Justification = "Only used for local generation")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Locations.JunctionNativeMethods.#DeleteJunction(System.String)", Justification = "Only used for local generation")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Locations.JunctionNativeMethods.#InternalGetTarget(Microsoft.Win32.SafeHandles.SafeFileHandle)", Justification = "Only used for local generation")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.Templates.Locations.JunctionNativeMethods.ThrowLastWin32Error(System.String)", Scope = "member", Target = "Microsoft.Templates.Locations.JunctionNativeMethods.#OpenReparsePoint(System.String,Microsoft.Templates.Locations.JunctionNativeMethods+EFileAccess)", Justification = "Only used for local generation")]
|
||||
[assembly: SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Windows.Documents.InlineCollection.Add(System.String)", Scope = "member", Target = "Microsoft.Templates.UI.Extensions.TextBlockExtensions.#OnSequentialFlowStepChanged(System.Windows.DependencyObject,System.Windows.DependencyPropertyChangedEventArgs)", Justification = "No text here")]
|
||||
[assembly: SuppressMessage("Globalization", "CA1309:Use ordinal string comparison", Justification = "The user's search term should be compared with culture based rules.", Scope = "type", Target = "~T:Microsoft.PowerToys.Run.Plugin.TimeDate.Components.SearchController")]
|
||||
|
||||
|
||||
@@ -515,8 +515,7 @@ namespace ManagedCommon
|
||||
return lightnessL.ToString(CultureInfo.InvariantCulture);
|
||||
case "Lc":
|
||||
var (lightnessC, _, _) = ConvertToCIELABColor(color);
|
||||
lightnessC = Math.Round(lightnessC, 2);
|
||||
return lightnessC.ToString(CultureInfo.InvariantCulture);
|
||||
return ColorPercentFormatted(lightnessC, paramFormat, 2);
|
||||
case "Lo":
|
||||
var (lightnessO, _, _) = ConvertToOklabColor(color);
|
||||
lightnessO = Math.Round(lightnessO, 2);
|
||||
@@ -531,12 +530,10 @@ namespace ManagedCommon
|
||||
return blackness.ToString(CultureInfo.InvariantCulture);
|
||||
case "Ca":
|
||||
var (_, chromaticityA, _) = ConvertToCIELABColor(color);
|
||||
chromaticityA = Math.Round(chromaticityA, 2);
|
||||
return chromaticityA.ToString(CultureInfo.InvariantCulture);
|
||||
return ColorPercentFormatted(chromaticityA, paramFormat, 2);
|
||||
case "Cb":
|
||||
var (_, _, chromaticityB) = ConvertToCIELABColor(color);
|
||||
chromaticityB = Math.Round(chromaticityB, 2);
|
||||
return chromaticityB.ToString(CultureInfo.InvariantCulture);
|
||||
return ColorPercentFormatted(chromaticityB, paramFormat, 2);
|
||||
case "Oa":
|
||||
var (_, chromaticityAOklab, _) = ConvertToOklabColor(color);
|
||||
chromaticityAOklab = Math.Round(chromaticityAOklab, 2);
|
||||
@@ -595,6 +592,24 @@ namespace ManagedCommon
|
||||
}
|
||||
}
|
||||
|
||||
private static string ColorPercentFormatted(double colorPercentValue, char paramFormat, int defaultDecimalDigits)
|
||||
{
|
||||
switch (paramFormat)
|
||||
{
|
||||
case 'i':
|
||||
double roundedColorPercentValue = Math.Round(colorPercentValue);
|
||||
if (roundedColorPercentValue == 0)
|
||||
{
|
||||
// convert -0 to 0
|
||||
roundedColorPercentValue = 0.0;
|
||||
}
|
||||
|
||||
return roundedColorPercentValue.ToString(CultureInfo.InvariantCulture);
|
||||
default:
|
||||
return Math.Round(colorPercentValue, defaultDecimalDigits).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetDefaultFormat(string formatName)
|
||||
{
|
||||
switch (formatName)
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<!-- TEMPORARILY_DISABLED: PowerDisplay
|
||||
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
|
||||
@@ -159,6 +160,7 @@
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
-->
|
||||
<policy name="ConfigureEnabledUtilityEnvironmentVariables" class="Both" displayName="$(string.ConfigureEnabledUtilityEnvironmentVariables)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityEnvironmentVariables">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />
|
||||
|
||||
@@ -248,7 +248,7 @@ If you don't configure this policy, the user will be able to control the setting
|
||||
<string id="ConfigureEnabledUtilityCmdPal">CmdPal: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityLightSwitch">Light Switch: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string>
|
||||
<!-- <string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string> --><!-- TEMPORARILY_DISABLED: PowerDisplay -->
|
||||
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>
|
||||
|
||||
@@ -57,6 +57,22 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration => _configuration;
|
||||
|
||||
public IReadOnlyList<AdvancedPastePythonScriptAction> PythonScriptActions => Array.Empty<AdvancedPastePythonScriptAction>();
|
||||
|
||||
public string PythonScriptsFolder => string.Empty;
|
||||
|
||||
public string PythonExecutablePath => string.Empty;
|
||||
|
||||
public bool PythonUseWsl => false;
|
||||
|
||||
public string PythonWslDistribution => string.Empty;
|
||||
|
||||
public int PythonScriptTimeoutSeconds => 30;
|
||||
|
||||
public bool IsPythonScriptsEnabled => true;
|
||||
|
||||
public IReadOnlyDictionary<string, string> TrustedScriptHashes => new Dictionary<string, string>();
|
||||
|
||||
public event EventHandler Changed;
|
||||
|
||||
public Task SetActiveAIProviderAsync(string providerId)
|
||||
@@ -65,4 +81,8 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void StoreTrustedScriptHash(string scriptPath, string hash)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Services.PythonScripts;
|
||||
using AdvancedPaste.UnitTests.Mocks;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace AdvancedPaste.UnitTests.ServicesTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class PythonScriptServiceTests
|
||||
{
|
||||
private PythonScriptService _service;
|
||||
|
||||
[TestInitialize]
|
||||
public void Setup()
|
||||
{
|
||||
_service = new PythonScriptService(new IntegrationTestUserSettings());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MergeWithAutoDetectedImports_DetectsSimpleImports()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"# @advancedpaste:name test",
|
||||
"import requests",
|
||||
"import numpy",
|
||||
"import os",
|
||||
"import sys",
|
||||
};
|
||||
|
||||
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
||||
|
||||
Assert.AreEqual(2, result.Count); // requests + numpy; os and sys are stdlib
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "requests" && r.PipPackage == "requests"));
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "numpy" && r.PipPackage == "numpy"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MergeWithAutoDetectedImports_DetectsFromImports()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"from PIL import Image",
|
||||
"from markitdown import MarkItDown",
|
||||
};
|
||||
|
||||
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
||||
|
||||
Assert.AreEqual(2, result.Count);
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "PIL" && r.PipPackage == "Pillow"));
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "markitdown" && r.PipPackage == "markitdown"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MergeWithAutoDetectedImports_WellKnownMappings()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"import cv2",
|
||||
"import win32clipboard",
|
||||
"import yaml",
|
||||
};
|
||||
|
||||
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
||||
|
||||
Assert.AreEqual(3, result.Count);
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "cv2" && r.PipPackage == "opencv-python"));
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "win32clipboard" && r.PipPackage == "pywin32"));
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "yaml" && r.PipPackage == "PyYAML"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MergeWithAutoDetectedImports_ExplicitRequirementsTakePrecedence()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"import cv2",
|
||||
"import requests",
|
||||
};
|
||||
|
||||
var explicitReqs = new List<PythonRequirement>
|
||||
{
|
||||
new("cv2", "opencv-python-headless"),
|
||||
};
|
||||
|
||||
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, explicitReqs);
|
||||
|
||||
Assert.AreEqual(2, result.Count);
|
||||
|
||||
// cv2 should use the explicit pip package name, not the well-known mapping
|
||||
var cv2Req = result.First(r => r.ImportName == "cv2");
|
||||
Assert.AreEqual("opencv-python-headless", cv2Req.PipPackage);
|
||||
|
||||
// requests should be auto-detected
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "requests"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MergeWithAutoDetectedImports_SkipsStdlib()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"import os",
|
||||
"import sys",
|
||||
"import json",
|
||||
"import io",
|
||||
"import pathlib",
|
||||
"import tempfile",
|
||||
"import subprocess",
|
||||
};
|
||||
|
||||
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
||||
|
||||
Assert.AreEqual(0, result.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MergeWithAutoDetectedImports_SkipsComments()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"# import requests",
|
||||
"# from PIL import Image",
|
||||
"import json",
|
||||
};
|
||||
|
||||
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
||||
|
||||
Assert.AreEqual(0, result.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MergeWithAutoDetectedImports_HandlesMultipleImportsOnOneLine()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"import requests, numpy, pandas",
|
||||
};
|
||||
|
||||
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
||||
|
||||
Assert.AreEqual(3, result.Count);
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "requests"));
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "numpy"));
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "pandas"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MergeWithAutoDetectedImports_HandlesSubmoduleImport()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"import win32com.client",
|
||||
"from llama_cpp import Llama",
|
||||
};
|
||||
|
||||
var result = PythonScriptService.MergeWithAutoDetectedImports(lines, []);
|
||||
|
||||
Assert.AreEqual(2, result.Count);
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "win32com" && r.PipPackage == "pywin32"));
|
||||
Assert.IsTrue(result.Any(r => r.ImportName == "llama_cpp" && r.PipPackage == "llama-cpp-python"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePythonError_ModuleNotFoundError()
|
||||
{
|
||||
var stderr = """
|
||||
Traceback (most recent call last):
|
||||
File "C:\scripts\reverse.py", line 4, in <module>
|
||||
import win32clipboard
|
||||
ModuleNotFoundError: No module named 'win32clipboard'
|
||||
""";
|
||||
|
||||
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
||||
|
||||
Assert.IsTrue(summary.Contains("reverse.py"), $"Summary should mention the script: {summary}");
|
||||
Assert.IsTrue(summary.Contains("line 4"), $"Summary should mention the line: {summary}");
|
||||
Assert.IsTrue(summary.Contains("win32clipboard"), $"Summary should mention the module: {summary}");
|
||||
Assert.IsTrue(summary.Contains("pywin32"), $"Summary should suggest pip package: {summary}");
|
||||
Assert.IsTrue(!string.IsNullOrEmpty(details));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePythonError_SyntaxError()
|
||||
{
|
||||
var stderr = """
|
||||
File "test.py", line 5
|
||||
def foo(
|
||||
^
|
||||
SyntaxError: unexpected EOF while parsing
|
||||
""";
|
||||
|
||||
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
||||
|
||||
Assert.IsTrue(summary.Contains("test.py"), $"Summary should mention the script: {summary}");
|
||||
Assert.IsTrue(summary.Contains("line 5"), $"Summary should mention the line: {summary}");
|
||||
Assert.IsTrue(summary.Contains("Python syntax error:"), $"Summary: {summary}");
|
||||
Assert.IsTrue(!string.IsNullOrEmpty(details));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePythonError_SyntaxErrorWithColumn()
|
||||
{
|
||||
var stderr = " File \"script.py\", line 3\n x = (1 +\n ^\nSyntaxError: '(' was never closed\n";
|
||||
|
||||
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
||||
|
||||
Assert.IsTrue(summary.Contains("script.py"), $"Summary should mention the script: {summary}");
|
||||
Assert.IsTrue(summary.Contains("line 3"), $"Summary should mention the line: {summary}");
|
||||
Assert.IsTrue(summary.Contains("col"), $"Summary should mention the column: {summary}");
|
||||
Assert.IsTrue(summary.Contains("Python syntax error:"), $"Summary: {summary}");
|
||||
Assert.IsTrue(!string.IsNullOrEmpty(details));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePythonError_GenericError()
|
||||
{
|
||||
var stderr = """
|
||||
Traceback (most recent call last):
|
||||
File "test.py", line 10, in <module>
|
||||
result = 1 / 0
|
||||
ZeroDivisionError: division by zero
|
||||
""";
|
||||
|
||||
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
||||
|
||||
Assert.IsTrue(summary.Contains("test.py"), $"Summary should mention the script: {summary}");
|
||||
Assert.IsTrue(summary.Contains("line 10"), $"Summary should mention the line: {summary}");
|
||||
Assert.IsTrue(summary.Contains("ZeroDivisionError"), $"Summary: {summary}");
|
||||
Assert.IsTrue(!string.IsNullOrEmpty(details));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePythonError_NestedTraceback_ShowsLastFrame()
|
||||
{
|
||||
var stderr = """
|
||||
Traceback (most recent call last):
|
||||
File "main.py", line 5, in <module>
|
||||
helper()
|
||||
File "helper.py", line 12, in helper
|
||||
do_work()
|
||||
File "worker.py", line 8, in do_work
|
||||
raise RuntimeError("bad state")
|
||||
RuntimeError: bad state
|
||||
""";
|
||||
|
||||
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
||||
|
||||
Assert.IsTrue(summary.Contains("worker.py"), $"Summary should mention the last script in the chain: {summary}");
|
||||
Assert.IsTrue(summary.Contains("line 8"), $"Summary should mention the line of the last frame: {summary}");
|
||||
Assert.IsTrue(summary.Contains("bad state"), $"Summary should contain the error message: {summary}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePythonError_EmptyStderr()
|
||||
{
|
||||
var (summary, details) = PythonScriptService.ParsePythonError(string.Empty);
|
||||
|
||||
Assert.IsTrue(!string.IsNullOrEmpty(summary));
|
||||
Assert.AreEqual(string.Empty, details);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePythonError_NoTraceback_PlainStderr()
|
||||
{
|
||||
var stderr = "Something went wrong in the script\n";
|
||||
|
||||
var (summary, details) = PythonScriptService.ParsePythonError(stderr);
|
||||
|
||||
// No File "..." reference, so no location — just the message
|
||||
Assert.IsTrue(summary.Contains("Something went wrong"), $"Summary: {summary}");
|
||||
Assert.IsFalse(summary.Contains("line"), $"Summary should not contain 'line' without a traceback: {summary}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExtractLastTracebackLocation_BasicTraceback()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"Traceback (most recent call last):",
|
||||
" File \"script.py\", line 10, in <module>",
|
||||
" result = 1 / 0",
|
||||
"ZeroDivisionError: division by zero",
|
||||
};
|
||||
|
||||
var location = PythonScriptService.ExtractLastTracebackLocation(lines);
|
||||
|
||||
Assert.IsNotNull(location);
|
||||
Assert.AreEqual("script.py", location.Value.FileName);
|
||||
Assert.AreEqual(10, location.Value.Line);
|
||||
Assert.IsNull(location.Value.Column);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExtractLastTracebackLocation_WithCaret()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
" File \"test.py\", line 5",
|
||||
" def foo(",
|
||||
" ^",
|
||||
"SyntaxError: unexpected EOF while parsing",
|
||||
};
|
||||
|
||||
var location = PythonScriptService.ExtractLastTracebackLocation(lines);
|
||||
|
||||
Assert.IsNotNull(location);
|
||||
Assert.AreEqual("test.py", location.Value.FileName);
|
||||
Assert.AreEqual(5, location.Value.Line);
|
||||
Assert.IsNotNull(location.Value.Column);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExtractLastTracebackLocation_FullPath_ReturnsBasename()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"Traceback (most recent call last):",
|
||||
" File \"C:\\Users\\user\\scripts\\my_script.py\", line 42, in <module>",
|
||||
" some_call()",
|
||||
"ValueError: invalid value",
|
||||
};
|
||||
|
||||
var location = PythonScriptService.ExtractLastTracebackLocation(lines);
|
||||
|
||||
Assert.IsNotNull(location);
|
||||
Assert.AreEqual("my_script.py", location.Value.FileName);
|
||||
Assert.AreEqual(42, location.Value.Line);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExtractLastTracebackLocation_NoFileLine_ReturnsNull()
|
||||
{
|
||||
var lines = new[]
|
||||
{
|
||||
"Some random error output",
|
||||
"No traceback here",
|
||||
};
|
||||
|
||||
var location = PythonScriptService.ExtractLastTracebackLocation(lines);
|
||||
|
||||
Assert.IsNull(location);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePipInstallError_ExtractsErrorLine()
|
||||
{
|
||||
var stderr = """
|
||||
Collecting some-package
|
||||
Downloading some-package-1.0.tar.gz (15 kB)
|
||||
ERROR: Could not find a version that satisfies the requirement some-package (from versions: none)
|
||||
ERROR: No matching distribution found for some-package
|
||||
""";
|
||||
|
||||
var (summary, fullStderr) = PythonScriptService.ParsePipInstallError(stderr);
|
||||
|
||||
Assert.IsTrue(summary.Contains("No matching distribution"), $"Summary should contain the last ERROR line: {summary}");
|
||||
Assert.IsTrue(!string.IsNullOrEmpty(fullStderr));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePipInstallError_NoErrorPrefix_UsesLastLine()
|
||||
{
|
||||
var stderr = "permission denied: /usr/lib/python3/dist-packages\n";
|
||||
|
||||
var (summary, fullStderr) = PythonScriptService.ParsePipInstallError(stderr);
|
||||
|
||||
Assert.IsTrue(summary.Contains("permission denied"), $"Summary: {summary}");
|
||||
Assert.IsTrue(!string.IsNullOrEmpty(fullStderr));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParsePipInstallError_EmptyStderr()
|
||||
{
|
||||
var (summary, fullStderr) = PythonScriptService.ParsePipInstallError(string.Empty);
|
||||
|
||||
Assert.AreEqual("unknown error", summary);
|
||||
Assert.AreEqual(string.Empty, fullStderr);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_FunctionNameDeterminesFormat_Text()
|
||||
{
|
||||
// The new interface uses function names like advanced_paste_from_text_to_text(...)
|
||||
// to determine supported formats, not parameter signatures.
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_text(text):\n return text.upper()\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual(Models.ClipboardFormat.Text, metadata.SupportedFormats);
|
||||
Assert.AreEqual("text", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_FunctionNameDeterminesFormat_Html()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_html_to_text(html: str) -> str:\n return html\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual(Models.ClipboardFormat.Html, metadata.SupportedFormats);
|
||||
Assert.AreEqual("text", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_FunctionNameDeterminesFormat_Image()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_image_to_text(image_path):\n return 'desc'\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual(Models.ClipboardFormat.Image, metadata.SupportedFormats);
|
||||
Assert.AreEqual("text", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_FunctionNameDeterminesFormat_Files()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_files_to_text(file_paths):\n return ''\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual(Models.ClipboardFormat.File, metadata.SupportedFormats);
|
||||
Assert.AreEqual("text", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_OutputTypeHint_Image()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_image(text):\n return '/path/img.png'\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual("image", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_OutputTypeHint_File()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_file(text):\n return '/path/out.txt'\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual("file", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_OutputTypeHint_Files()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_files_to_files(file_paths):\n return file_paths\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual("files", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_RejectsMultipleFunctions()
|
||||
{
|
||||
var scriptPath = CreateTempScript(
|
||||
"def advanced_paste_from_text_to_text(text):\n return text\n\n" +
|
||||
"def advanced_paste_from_html_to_text(html):\n return html\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNull(metadata);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_RejectsNoFunction()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def some_other_function(text):\n return text\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNull(metadata);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_RejectsOldFormatWithoutTo()
|
||||
{
|
||||
// Old format (advanced_paste_from_text without _to_) should be rejected.
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_text(text):\n return text\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNull(metadata);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_FunctionNameDeterminesFormat_Audio()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_audio_to_text(audio_path):\n return 'transcribed'\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual(Models.ClipboardFormat.Audio, metadata.SupportedFormats);
|
||||
Assert.AreEqual("text", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_FunctionNameDeterminesFormat_Video()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_video_to_text(video_path):\n return 'description'\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual(Models.ClipboardFormat.Video, metadata.SupportedFormats);
|
||||
Assert.AreEqual("text", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_OutputTypeHint_Audio()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_audio(text):\n return '/path/out.mp3'\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual(Models.ClipboardFormat.Text, metadata.SupportedFormats);
|
||||
Assert.AreEqual("audio", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadMetadata_OutputTypeHint_Video()
|
||||
{
|
||||
var scriptPath = CreateTempScript("def advanced_paste_from_text_to_video(text):\n return '/path/out.mp4'\n");
|
||||
var metadata = _service.ReadMetadata(scriptPath);
|
||||
|
||||
Assert.IsNotNull(metadata);
|
||||
Assert.AreEqual(Models.ClipboardFormat.Text, metadata.SupportedFormats);
|
||||
Assert.AreEqual("video", metadata.OutputTypeHint);
|
||||
File.Delete(scriptPath);
|
||||
}
|
||||
|
||||
private static string CreateTempScript(string content)
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"test_script_{Guid.NewGuid():N}.py");
|
||||
File.WriteAllText(path, content);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -155,5 +155,9 @@
|
||||
<Content Include="Assets\AdvancedPaste\SemanticKernel.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
<Content Include="Services\PythonScripts\_runner.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -14,6 +14,7 @@ using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Services.PythonScripts;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.ViewModels;
|
||||
using ManagedCommon;
|
||||
@@ -83,6 +84,8 @@ namespace AdvancedPaste
|
||||
services.AddSingleton<IPasteAIProviderFactory, PasteAIProviderFactory>();
|
||||
services.AddSingleton<ICustomActionTransformService, CustomActionTransformService>();
|
||||
services.AddSingleton<IKernelService, AdvancedAIKernelService>();
|
||||
services.AddSingleton<IPythonScriptService, PythonScriptService>();
|
||||
services.AddSingleton<IPythonScriptTrustService, PythonScriptTrustService>();
|
||||
services.AddSingleton<IPasteFormatExecutor, PasteFormatExecutor>();
|
||||
services.AddSingleton<OptionsViewModel>();
|
||||
}).Build();
|
||||
|
||||
@@ -755,63 +755,7 @@
|
||||
<animations:OpacityAnimation To="0.0" Duration="0:0:0.167" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</TextBlock>
|
||||
<Grid
|
||||
x:Name="ErrorMessageGrid"
|
||||
x:Uid="ErrorMessageGrid"
|
||||
Grid.Row="1"
|
||||
Margin="8,8,0,0"
|
||||
ColumnSpacing="8"
|
||||
Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<ToolTipService.ToolTip>
|
||||
<ToolTip VerticalOffset="-105" Visibility="{x:Bind ViewModel.PasteActionError.HasDetails, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel
|
||||
MinWidth="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<TextBox
|
||||
x:Name="AIErrorMessage"
|
||||
x:Uid="AIErrorMessage"
|
||||
FontSize="12"
|
||||
IsReadOnly="True"
|
||||
Text="{x:Bind ViewModel.PasteActionError.Details, Mode=OneWay}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ToolTip>
|
||||
</ToolTipService.ToolTip>
|
||||
<FontIcon
|
||||
Margin="0,3,3,0"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Glyph=""
|
||||
Visibility="{x:Bind ViewModel.PasteActionError.HasDetails, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<TextBlock
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.PasteActionError.Text, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsBtn"
|
||||
Grid.Column="1"
|
||||
Margin="0,-1,0,0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Command="{x:Bind ViewModel.OpenSettingsCommand}"
|
||||
FontSize="12" />
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation To="1.0" Duration="0:0:0.6" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation To="0.0" Duration="0:0:0.167" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</Grid>
|
||||
<!-- Error message grid moved to MainPage.xaml so it remains enabled when PromptBox is disabled -->
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="DefaultState" />
|
||||
@@ -832,7 +776,6 @@
|
||||
<VisualState.Setters>
|
||||
<Setter Target="InputTxtBox.BorderBrush" Value="{ThemeResource SystemFillColorCriticalBrush}" />
|
||||
<Setter Target="DisclaimerPresenter.Visibility" Value="Collapsed" />
|
||||
<Setter Target="ErrorMessageGrid.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
|
||||
@@ -43,7 +43,8 @@ namespace AdvancedPaste
|
||||
double GetHeight(int maxCustomActionCount) =>
|
||||
baseHeight +
|
||||
new PasteFormatsToHeightConverter().GetHeight(coreActionCount + _userSettings.AdditionalActions.Count) +
|
||||
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(_optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
|
||||
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(_optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0) +
|
||||
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(_optionsViewModel.PythonScriptPasteFormats.Count);
|
||||
|
||||
MinHeight = GetHeight(1);
|
||||
Height = GetHeight(5);
|
||||
@@ -59,6 +60,7 @@ namespace AdvancedPaste
|
||||
UpdateHeight();
|
||||
}
|
||||
};
|
||||
_optionsViewModel.PythonScriptPasteFormats.CollectionChanged += (_, _) => UpdateHeight();
|
||||
|
||||
AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico");
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
@@ -141,11 +143,7 @@ namespace AdvancedPaste
|
||||
internal void FinishLoading(bool success)
|
||||
{
|
||||
MainPage.CustomFormatTextBox.IsLoading(false);
|
||||
|
||||
if (success)
|
||||
{
|
||||
VisualStateManager.GoToState(MainPage.CustomFormatTextBox, "DefaultState", true);
|
||||
}
|
||||
VisualStateManager.GoToState(MainPage.CustomFormatTextBox, "DefaultState", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
</Page.KeyboardAccelerators>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -250,10 +251,12 @@
|
||||
Grid.Row="1"
|
||||
Margin="20,0,20,0"
|
||||
x:FieldModifier="public"
|
||||
IsEnabled="{x:Bind ViewModel.IsCustomAIServiceEnabled, Mode=OneWay}"
|
||||
IsEnabled="True"
|
||||
TabIndex="0">
|
||||
<controls:PromptBox.Footer>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<StackPanel
|
||||
Orientation="Horizontal"
|
||||
Visibility="{x:Bind ViewModel.IsCustomAIServiceEnabled, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<TextBlock
|
||||
x:Uid="AIMistakeNote"
|
||||
Margin="0,0,2,0"
|
||||
@@ -299,19 +302,70 @@
|
||||
</StackPanel>
|
||||
</controls:PromptBox.Footer>
|
||||
</controls:PromptBox>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<Grid
|
||||
x:Name="ErrorMessageGrid"
|
||||
Grid.Row="2"
|
||||
Margin="20,4,20,0"
|
||||
ColumnSpacing="8"
|
||||
Visibility="{x:Bind ViewModel.PasteActionError.HasText, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
Grid.Column="0"
|
||||
Margin="0,3,3,0"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Glyph=""
|
||||
Visibility="{x:Bind ViewModel.PasteActionError.HasDetails, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
MaxLines="2"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="Wrap"
|
||||
Text="{x:Bind ViewModel.PasteActionError.Text, Mode=OneWay}" />
|
||||
<HyperlinkButton
|
||||
x:Name="ShowErrorDetailsBtn"
|
||||
x:Uid="ShowErrorDetailsBtn"
|
||||
Grid.Column="2"
|
||||
Margin="4,-1,0,0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Click="ShowErrorDetailsBtn_Click"
|
||||
FontSize="12"
|
||||
Visibility="{x:Bind ViewModel.PasteActionError.HasDetails, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsBtn"
|
||||
Grid.Column="3"
|
||||
Margin="0,-1,0,0"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Command="{x:Bind ViewModel.OpenSettingsCommand}"
|
||||
FontSize="12" />
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="3">
|
||||
<Grid RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListView
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
@@ -341,6 +395,27 @@
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
|
||||
<Rectangle
|
||||
Grid.Row="3"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind ViewModel.PythonScriptPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
|
||||
<ListView
|
||||
x:Name="PythonScriptsListView"
|
||||
Grid.Row="4"
|
||||
VerticalAlignment="Top"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.PythonScriptPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None"
|
||||
TabIndex="3" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
@@ -208,5 +208,43 @@ namespace AdvancedPaste.Pages
|
||||
Clipboard.SetHistoryItemAsContent(item.Item);
|
||||
}
|
||||
}
|
||||
|
||||
private async void ShowErrorDetailsBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var details = ViewModel.PasteActionError?.Details;
|
||||
if (string.IsNullOrEmpty(details))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scrollViewer = new ScrollViewer
|
||||
{
|
||||
MaxHeight = 400,
|
||||
MinWidth = 400,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
};
|
||||
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = details,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Consolas"),
|
||||
FontSize = 12,
|
||||
IsTextSelectionEnabled = true,
|
||||
};
|
||||
|
||||
scrollViewer.Content = textBlock;
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = ResourceLoaderInstance.ResourceLoader.GetString("ErrorDetailsDialogTitle"),
|
||||
CloseButtonText = ResourceLoaderInstance.ResourceLoader.GetString("ErrorDetailsDialogClose"),
|
||||
Content = scrollViewer,
|
||||
XamlRoot = this.XamlRoot,
|
||||
};
|
||||
|
||||
await dialog.ShowAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,26 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; }
|
||||
|
||||
public IReadOnlyList<AdvancedPastePythonScriptAction> PythonScriptActions { get; }
|
||||
|
||||
public bool IsPythonScriptsEnabled { get; }
|
||||
|
||||
public string PythonScriptsFolder { get; }
|
||||
|
||||
public string PythonExecutablePath { get; }
|
||||
|
||||
public bool PythonUseWsl { get; }
|
||||
|
||||
public string PythonWslDistribution { get; }
|
||||
|
||||
public int PythonScriptTimeoutSeconds { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> TrustedScriptHashes { get; }
|
||||
|
||||
public event EventHandler Changed;
|
||||
|
||||
Task SetActiveAIProviderAsync(string providerId);
|
||||
|
||||
void StoreTrustedScriptHash(string scriptPath, string hash);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -25,6 +26,10 @@ namespace AdvancedPaste.Settings
|
||||
private readonly Lock _loadingSettingsLock = new();
|
||||
private readonly List<PasteFormats> _additionalActions;
|
||||
private readonly List<AdvancedPasteCustomAction> _customActions;
|
||||
private readonly List<AdvancedPastePythonScriptAction> _pythonScriptActions;
|
||||
private FileSystemWatcher _scriptFolderWatcher;
|
||||
private CancellationTokenSource _scriptFolderDebounce;
|
||||
private string _watchedScriptsFolder = string.Empty;
|
||||
|
||||
private const string AdvancedPasteModuleName = "AdvancedPaste";
|
||||
private const int MaxNumberOfRetry = 5;
|
||||
@@ -48,6 +53,22 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration { get; private set; }
|
||||
|
||||
public IReadOnlyList<AdvancedPastePythonScriptAction> PythonScriptActions => _pythonScriptActions;
|
||||
|
||||
public string PythonScriptsFolder { get; private set; }
|
||||
|
||||
public bool IsPythonScriptsEnabled { get; private set; }
|
||||
|
||||
public string PythonExecutablePath { get; private set; }
|
||||
|
||||
public bool PythonUseWsl { get; private set; }
|
||||
|
||||
public string PythonWslDistribution { get; private set; } = string.Empty;
|
||||
|
||||
public int PythonScriptTimeoutSeconds { get; private set; } = 30;
|
||||
|
||||
public IReadOnlyDictionary<string, string> TrustedScriptHashes { get; private set; } = new Dictionary<string, string>();
|
||||
|
||||
public UserSettings(IFileSystem fileSystem)
|
||||
{
|
||||
_settingsUtils = new SettingsUtils(fileSystem);
|
||||
@@ -57,8 +78,14 @@ namespace AdvancedPaste.Settings
|
||||
CloseAfterLosingFocus = false;
|
||||
EnableClipboardPreview = true;
|
||||
PasteAIConfiguration = new PasteAIConfiguration();
|
||||
PythonScriptsFolder = GetDefaultScriptsFolder();
|
||||
PythonExecutablePath = string.Empty;
|
||||
PythonUseWsl = false;
|
||||
PythonWslDistribution = string.Empty;
|
||||
PythonScriptTimeoutSeconds = 30;
|
||||
_additionalActions = [];
|
||||
_customActions = [];
|
||||
_pythonScriptActions = [];
|
||||
_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
LoadSettingsFromJson();
|
||||
@@ -66,6 +93,14 @@ namespace AdvancedPaste.Settings
|
||||
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", OnSettingsFileChanged, fileSystem);
|
||||
}
|
||||
|
||||
private static string GetDefaultScriptsFolder() =>
|
||||
System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Microsoft",
|
||||
"PowerToys",
|
||||
"AdvancedPaste",
|
||||
"Scripts");
|
||||
|
||||
private void OnSettingsFileChanged()
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
@@ -131,6 +166,42 @@ namespace AdvancedPaste.Settings
|
||||
_customActions.Clear();
|
||||
_customActions.AddRange(properties.CustomActions.Value.Where(customAction => customAction.IsShown && customAction.IsValid));
|
||||
|
||||
var pythonScripts = properties.PythonScripts ?? new AdvancedPastePythonScriptSettings();
|
||||
pythonScripts.MigrateLegacyIfNeeded();
|
||||
|
||||
var mode = pythonScripts.Mode ?? "disabled";
|
||||
IsPythonScriptsEnabled = !string.Equals(mode, "disabled", StringComparison.OrdinalIgnoreCase);
|
||||
PythonUseWsl = string.Equals(mode, "wsl", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (PythonUseWsl)
|
||||
{
|
||||
var wslSettings = pythonScripts.WslSettings ?? new PythonScriptWslSettings();
|
||||
PythonScriptsFolder = string.IsNullOrWhiteSpace(wslSettings.ScriptsFolder)
|
||||
? GetDefaultScriptsFolder()
|
||||
: wslSettings.ScriptsFolder;
|
||||
PythonExecutablePath = string.Empty;
|
||||
PythonWslDistribution = wslSettings.Distribution ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var winSettings = pythonScripts.WindowsSettings ?? new PythonScriptWindowsSettings();
|
||||
PythonScriptsFolder = string.IsNullOrWhiteSpace(winSettings.ScriptsFolder)
|
||||
? GetDefaultScriptsFolder()
|
||||
: winSettings.ScriptsFolder;
|
||||
PythonExecutablePath = winSettings.PythonExecutablePath ?? string.Empty;
|
||||
PythonWslDistribution = string.Empty;
|
||||
}
|
||||
|
||||
PythonScriptTimeoutSeconds = pythonScripts.TimeoutSeconds > 0 ? pythonScripts.TimeoutSeconds : 30;
|
||||
TrustedScriptHashes = new Dictionary<string, string>(
|
||||
pythonScripts.TrustedScriptHashes ?? new Dictionary<string, string>(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_pythonScriptActions.Clear();
|
||||
_pythonScriptActions.AddRange(pythonScripts.Value.Where(a => a.IsShown));
|
||||
|
||||
UpdateScriptFolderWatcher(PythonScriptsFolder);
|
||||
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
@@ -295,6 +366,102 @@ namespace AdvancedPaste.Settings
|
||||
return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private void UpdateScriptFolderWatcher(string folderPath)
|
||||
{
|
||||
if (string.Equals(_watchedScriptsFolder, folderPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scriptFolderWatcher?.Dispose();
|
||||
_scriptFolderWatcher = null;
|
||||
_watchedScriptsFolder = folderPath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(folderPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!System.IO.Directory.Exists(folderPath))
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(folderPath);
|
||||
}
|
||||
|
||||
_scriptFolderWatcher = new FileSystemWatcher(folderPath, "*.py")
|
||||
{
|
||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime,
|
||||
EnableRaisingEvents = true,
|
||||
IncludeSubdirectories = false,
|
||||
};
|
||||
|
||||
_scriptFolderWatcher.Changed += OnScriptFolderChanged;
|
||||
_scriptFolderWatcher.Created += OnScriptFolderChanged;
|
||||
_scriptFolderWatcher.Deleted += OnScriptFolderChanged;
|
||||
_scriptFolderWatcher.Renamed += OnScriptFolderChanged;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to set up script folder watcher for {folderPath}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScriptFolderChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
_scriptFolderDebounce?.Cancel();
|
||||
_scriptFolderDebounce = new CancellationTokenSource();
|
||||
|
||||
Task.Delay(TimeSpan.FromMilliseconds(500))
|
||||
.ContinueWith(
|
||||
_ =>
|
||||
{
|
||||
Task.Factory
|
||||
.StartNew(
|
||||
() => Changed?.Invoke(this, EventArgs.Empty),
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_taskScheduler)
|
||||
.Wait();
|
||||
},
|
||||
_scriptFolderDebounce.Token,
|
||||
TaskContinuationOptions.NotOnCanceled,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
}
|
||||
|
||||
public void StoreTrustedScriptHash(string scriptPath, string hash)
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteModuleName);
|
||||
if (settings?.Properties?.PythonScripts is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
settings.Properties.PythonScripts.TrustedScriptHashes ??= new Dictionary<string, string>();
|
||||
settings.Properties.PythonScripts.TrustedScriptHashes[scriptPath] = hash;
|
||||
settings.Save(_settingsUtils);
|
||||
|
||||
// Update in-memory cache.
|
||||
var updated = new Dictionary<string, string>(TrustedScriptHashes, StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[scriptPath] = hash,
|
||||
};
|
||||
TrustedScriptHashes = updated;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to store trusted script hash", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetActiveAIProviderAsync(string providerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
@@ -387,6 +554,8 @@ namespace AdvancedPaste.Settings
|
||||
if (disposing)
|
||||
{
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_scriptFolderDebounce?.Dispose();
|
||||
_scriptFolderWatcher?.Dispose();
|
||||
_watcher?.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,14 @@ public sealed class PasteFormat
|
||||
IsSavedQuery = isSavedQuery,
|
||||
};
|
||||
|
||||
public static PasteFormat CreatePythonScriptFormat(string name, string scriptPath, ClipboardFormat availableFormats) =>
|
||||
new(PasteFormats.PythonScript, availableFormats, isAIServiceEnabled: false)
|
||||
{
|
||||
Name = name,
|
||||
Prompt = scriptPath,
|
||||
IsSavedQuery = true,
|
||||
};
|
||||
|
||||
public PasteFormatMetadataAttribute Metadata => MetadataDict[Format];
|
||||
|
||||
public string IconGlyph => Metadata.IconGlyph;
|
||||
|
||||
@@ -122,4 +122,13 @@ public enum PasteFormats
|
||||
KernelFunctionDescription = "Takes user instructions and applies them to the current clipboard content (text or image). Use this function for image analysis, description, or transformation tasks beyond simple OCR.",
|
||||
RequiresPrompt = true)]
|
||||
CustomTextTransformation,
|
||||
|
||||
[PasteFormatMetadata(
|
||||
IsCoreAction = false,
|
||||
IconGlyph = "\uE943",
|
||||
RequiresAIService = false,
|
||||
CanPreview = true,
|
||||
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Image | ClipboardFormat.Audio | ClipboardFormat.Video | ClipboardFormat.File,
|
||||
KernelFunctionDescription = "Runs a user-provided Python script on clipboard content.")]
|
||||
PythonScript,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -9,15 +9,23 @@ using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services.CustomActions;
|
||||
using AdvancedPaste.Services.PythonScripts;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Services;
|
||||
|
||||
public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService) : IPasteFormatExecutor
|
||||
public sealed class PasteFormatExecutor(
|
||||
IKernelService kernelService,
|
||||
ICustomActionTransformService customActionTransformService,
|
||||
IPythonScriptService pythonScriptService,
|
||||
IPythonScriptTrustService pythonScriptTrustService) : IPasteFormatExecutor
|
||||
{
|
||||
private readonly IKernelService _kernelService = kernelService;
|
||||
private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
|
||||
private readonly IPythonScriptService _pythonScriptService = pythonScriptService;
|
||||
private readonly IPythonScriptTrustService _pythonScriptTrustService = pythonScriptTrustService;
|
||||
|
||||
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
{
|
||||
@@ -32,6 +40,15 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
|
||||
|
||||
var clipboardData = Clipboard.GetContent();
|
||||
|
||||
// PythonScript must NOT run inside Task.Run: the trust confirmation (ContentDialog)
|
||||
// requires the UI (XAML) thread and will throw if called from a thread-pool thread.
|
||||
// Python script execution is fully async (process.WaitForExitAsync), so it is safe
|
||||
// to await it directly without wrapping in Task.Run.
|
||||
if (format == PasteFormats.PythonScript)
|
||||
{
|
||||
return await ExecutePythonScriptAsync(pasteFormat.Prompt, clipboardData, cancellationToken, progress);
|
||||
}
|
||||
|
||||
// Run on thread-pool; although we use Async routines consistently, some actions still occasionally take a long time without yielding.
|
||||
return await Task.Run(async () =>
|
||||
pasteFormat.Format switch
|
||||
@@ -42,6 +59,92 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomAct
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<DataPackage> ExecutePythonScriptAsync(
|
||||
string scriptPath,
|
||||
DataPackageView clipboardData,
|
||||
CancellationToken cancellationToken,
|
||||
IProgress<double> progress)
|
||||
{
|
||||
// Security: ensure the script is trusted before executing.
|
||||
if (!_pythonScriptTrustService.IsTrusted(scriptPath))
|
||||
{
|
||||
var hash = _pythonScriptTrustService.ComputeHash(scriptPath);
|
||||
var approved = await _pythonScriptTrustService.RequestTrustAsync(scriptPath, hash);
|
||||
|
||||
if (!approved)
|
||||
{
|
||||
throw new OperationCanceledException("User declined to trust the Python script.");
|
||||
}
|
||||
|
||||
_pythonScriptTrustService.StoreTrust(scriptPath, hash);
|
||||
}
|
||||
|
||||
var metadata = _pythonScriptService.ReadMetadata(scriptPath);
|
||||
|
||||
// Pre-flight: check for missing packages and offer to install them.
|
||||
var missingPackages = await _pythonScriptService.GetMissingRequirementsAsync(metadata, cancellationToken);
|
||||
if (missingPackages.Count > 0)
|
||||
{
|
||||
var approved = await _pythonScriptTrustService.RequestInstallAsync(metadata.Name, missingPackages);
|
||||
if (!approved)
|
||||
{
|
||||
throw new OperationCanceledException("User declined to install missing Python packages.");
|
||||
}
|
||||
|
||||
await _pythonScriptService.InstallRequirementsAsync(missingPackages, metadata.Platform, cancellationToken);
|
||||
}
|
||||
|
||||
var detectedFormat = await clipboardData.GetAvailableFormatsAsync();
|
||||
|
||||
// V2 interface: script defines advanced_paste_from_*_to_*() — use unified runner.
|
||||
if (metadata.IsV2)
|
||||
{
|
||||
return await _pythonScriptService.ExecuteScriptAsync(scriptPath, metadata.Platform, clipboardData, detectedFormat, cancellationToken, progress);
|
||||
}
|
||||
|
||||
// Legacy paths for backward compatibility.
|
||||
if (string.Equals(metadata.Platform, "linux", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return await _pythonScriptService.ExecuteWslScriptAsync(scriptPath, clipboardData, detectedFormat, cancellationToken, progress);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Windows mode: script modifies the clipboard in-process; we return the updated clipboard.
|
||||
await _pythonScriptService.ExecuteWindowsScriptAsync(scriptPath, detectedFormat, cancellationToken, progress);
|
||||
|
||||
// Re-read clipboard after script has run.
|
||||
return Clipboard.GetContent() is { } updatedView
|
||||
? await DataPackageFromViewAsync(updatedView)
|
||||
: new DataPackage();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> DataPackageFromViewAsync(DataPackageView view)
|
||||
{
|
||||
var pkg = new DataPackage();
|
||||
|
||||
if (view.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
pkg.SetText(await view.GetTextAsync());
|
||||
}
|
||||
else if (view.Contains(StandardDataFormats.Html))
|
||||
{
|
||||
pkg.SetHtmlFormat(await view.GetHtmlFormatAsync());
|
||||
}
|
||||
else if (view.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
var items = await view.GetStorageItemsAsync();
|
||||
pkg.SetStorageItems(items);
|
||||
}
|
||||
else if (view.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
var bitmap = await view.GetBitmapAsync();
|
||||
pkg.SetBitmap(bitmap);
|
||||
}
|
||||
|
||||
return pkg;
|
||||
}
|
||||
|
||||
private static void WriteTelemetry(PasteFormats format, PasteActionSource source)
|
||||
{
|
||||
switch (source)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Services.PythonScripts;
|
||||
|
||||
public interface IPythonScriptService
|
||||
{
|
||||
/// <summary>
|
||||
/// V2 unified execution: C# reads the clipboard, pipes data as JSON to the runner,
|
||||
/// and receives a DataPackage from JSON stdout. Works on both Windows and WSL
|
||||
/// depending on the specified platform.
|
||||
/// </summary>
|
||||
Task<DataPackage> ExecuteScriptAsync(string scriptPath, string platform, DataPackageView clipboardData, ClipboardFormat detectedFormat, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
|
||||
/// <summary>
|
||||
/// Legacy Windows mode: the script directly manipulates the clipboard. C# waits for the process to exit.
|
||||
/// Kept for backward compatibility with scripts that use win32clipboard directly.
|
||||
/// </summary>
|
||||
Task ExecuteWindowsScriptAsync(string scriptPath, ClipboardFormat detectedFormat, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
|
||||
/// <summary>
|
||||
/// Legacy WSL mode: C# passes data via JSON stdin, receives a DataPackage from JSON stdout.
|
||||
/// Kept for backward compatibility with scripts that use json.load(sys.stdin) directly.
|
||||
/// </summary>
|
||||
Task<DataPackage> ExecuteWslScriptAsync(string scriptPath, DataPackageView clipboardData, ClipboardFormat detectedFormat, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
|
||||
/// <summary>
|
||||
/// Parses the @advancedpaste: header comments from a Python script file.
|
||||
/// </summary>
|
||||
PythonScriptMetadata ReadMetadata(string scriptPath);
|
||||
|
||||
/// <summary>
|
||||
/// Discovers all .py scripts in <paramref name="folderPath"/> and returns their metadata.
|
||||
/// </summary>
|
||||
IReadOnlyList<PythonScriptMetadata> DiscoverScripts(string folderPath);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the Python executable to use. Returns null if none is found.
|
||||
/// </summary>
|
||||
string TryFindPythonExecutable(string overridePath = null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if wsl.exe is available on this machine.
|
||||
/// </summary>
|
||||
bool IsWslAvailable();
|
||||
|
||||
/// <summary>
|
||||
/// Checks which of the declared requirements are not yet importable.
|
||||
/// Returns an empty list if all packages are installed.
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<PythonRequirement>> GetMissingRequirementsAsync(
|
||||
PythonScriptMetadata metadata,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Installs the given packages via pip / pip3.
|
||||
/// </summary>
|
||||
Task InstallRequirementsAsync(
|
||||
IReadOnlyList<PythonRequirement> requirements,
|
||||
string platform,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AdvancedPaste.Services.PythonScripts;
|
||||
|
||||
public interface IPythonScriptTrustService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the script at <paramref name="scriptPath"/> is currently trusted (hash matches stored value).
|
||||
/// </summary>
|
||||
bool IsTrusted(string scriptPath);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a UI confirmation dialog for the script. Returns true if the user approved execution.
|
||||
/// </summary>
|
||||
Task<bool> RequestTrustAsync(string scriptPath, string hash);
|
||||
|
||||
/// <summary>
|
||||
/// Persists the trust entry for <paramref name="scriptPath"/> with the given <paramref name="hash"/>.
|
||||
/// </summary>
|
||||
void StoreTrust(string scriptPath, string hash);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the SHA-256 hash of the script file and returns the hex string.
|
||||
/// </summary>
|
||||
string ComputeHash(string scriptPath);
|
||||
|
||||
/// <summary>
|
||||
/// Shows a confirmation dialog listing the missing packages and asking the user
|
||||
/// whether to install them. Returns true if the user approved installation.
|
||||
/// </summary>
|
||||
Task<bool> RequestInstallAsync(string scriptName, IReadOnlyList<PythonRequirement> missingPackages);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace AdvancedPaste.Services.PythonScripts;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single Python package requirement declared via
|
||||
/// <c># @advancedpaste:requires import_name=pip_package</c>.
|
||||
/// </summary>
|
||||
/// <param name="ImportName">The Python import name used in the script (e.g. "cv2").</param>
|
||||
/// <param name="PipPackage">The pip install name (e.g. "opencv-python-headless"). Equals <see cref="ImportName"/> when not explicitly specified.</param>
|
||||
public sealed record PythonRequirement(string ImportName, string PipPackage);
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
|
||||
namespace AdvancedPaste.Services.PythonScripts;
|
||||
|
||||
public sealed record PythonScriptMetadata(
|
||||
string ScriptPath,
|
||||
string Name,
|
||||
string Description,
|
||||
ClipboardFormat SupportedFormats,
|
||||
string Platform,
|
||||
string Version,
|
||||
bool IsEnabled,
|
||||
IReadOnlyList<PythonRequirement> Requirements,
|
||||
bool IsV2 = false,
|
||||
string OutputTypeHint = null);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Settings;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace AdvancedPaste.Services.PythonScripts;
|
||||
|
||||
public sealed class PythonScriptTrustService(IUserSettings userSettings) : IPythonScriptTrustService
|
||||
{
|
||||
private readonly IUserSettings _userSettings = userSettings;
|
||||
|
||||
public bool IsTrusted(string scriptPath)
|
||||
{
|
||||
var hashes = _userSettings.TrustedScriptHashes;
|
||||
if (hashes is null || !hashes.TryGetValue(scriptPath, out var storedHash))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var currentHash = ComputeHash(scriptPath);
|
||||
return string.Equals(currentHash, storedHash, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to compute hash for {scriptPath}", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RequestTrustAsync(string scriptPath, string hash)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = resourceLoader.GetString("PythonScriptTrustTitle"),
|
||||
Content = string.Format(
|
||||
System.Globalization.CultureInfo.CurrentCulture,
|
||||
resourceLoader.GetString("PythonScriptTrustContent"),
|
||||
scriptPath),
|
||||
PrimaryButtonText = resourceLoader.GetString("PythonScriptTrustConfirm"),
|
||||
CloseButtonText = resourceLoader.GetString("PythonScriptTrustCancel"),
|
||||
};
|
||||
|
||||
// XamlRoot must be set for ContentDialog to function.
|
||||
var mainWindow = (Microsoft.UI.Xaml.Application.Current as AdvancedPaste.App)?.GetMainWindow();
|
||||
if (mainWindow?.Content?.XamlRoot is { } xamlRoot)
|
||||
{
|
||||
dialog.XamlRoot = xamlRoot;
|
||||
}
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
return result == ContentDialogResult.Primary;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to show trust dialog", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void StoreTrust(string scriptPath, string hash)
|
||||
{
|
||||
_userSettings.StoreTrustedScriptHash(scriptPath, hash);
|
||||
}
|
||||
|
||||
public string ComputeHash(string scriptPath)
|
||||
{
|
||||
using var stream = File.OpenRead(scriptPath);
|
||||
var hashBytes = SHA256.HashData(stream);
|
||||
return Convert.ToHexStringLower(hashBytes);
|
||||
}
|
||||
|
||||
public async Task<bool> RequestInstallAsync(string scriptName, IReadOnlyList<PythonRequirement> missingPackages)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
|
||||
var packageList = string.Join("\n", missingPackages.Select(r =>
|
||||
string.Equals(r.ImportName, r.PipPackage, StringComparison.Ordinal)
|
||||
? $" • {r.PipPackage}"
|
||||
: $" • {r.PipPackage} (import: {r.ImportName})"));
|
||||
|
||||
var dialog = new ContentDialog
|
||||
{
|
||||
Title = resourceLoader.GetString("PythonPackageInstallTitle"),
|
||||
Content = string.Format(
|
||||
System.Globalization.CultureInfo.CurrentCulture,
|
||||
resourceLoader.GetString("PythonPackageInstallContent"),
|
||||
scriptName,
|
||||
packageList),
|
||||
PrimaryButtonText = resourceLoader.GetString("PythonPackageInstallConfirm"),
|
||||
CloseButtonText = resourceLoader.GetString("PythonPackageInstallCancel"),
|
||||
};
|
||||
|
||||
var mainWindow = (Microsoft.UI.Xaml.Application.Current as AdvancedPaste.App)?.GetMainWindow();
|
||||
if (mainWindow?.Content?.XamlRoot is { } xamlRoot)
|
||||
{
|
||||
dialog.XamlRoot = xamlRoot;
|
||||
}
|
||||
|
||||
var result = await dialog.ShowAsync();
|
||||
return result == ContentDialogResult.Primary;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to show package install dialog", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
# Copyright (c) Microsoft Corporation
|
||||
# The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
# See the LICENSE file in the project root for more information.
|
||||
|
||||
"""
|
||||
Advanced Paste – Script Runner (V3 Named Function Interface)
|
||||
|
||||
This runner is shipped with PowerToys and is NOT user-editable.
|
||||
It loads a user script, discovers the single advanced_paste_from_<input>_to_<output>
|
||||
function by name convention, calls it with the current clipboard data, and formats
|
||||
the return value into JSON on stdout.
|
||||
|
||||
Each script must define exactly one function matching the pattern:
|
||||
def advanced_paste_from_<input>_to_<output>(<param>)
|
||||
|
||||
Supported input types:
|
||||
- text, html, image, files
|
||||
|
||||
Required output types (declared via _to_ suffix):
|
||||
- text, html, image, file, files
|
||||
|
||||
Examples:
|
||||
- advanced_paste_from_text_to_text(text: str) → output is text
|
||||
- advanced_paste_from_text_to_image(text: str) → output is image
|
||||
- advanced_paste_from_image_to_text(image_path) → output is text
|
||||
- advanced_paste_from_files_to_text(file_paths) → output is text
|
||||
|
||||
Protocol:
|
||||
- Input: JSON on stdin (clipboard data from C#)
|
||||
- Output: JSON on stdout (result for C# to set on clipboard)
|
||||
- Errors: stderr (displayed to user on failure)
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _apply_output_hint(result, hint: str) -> dict:
|
||||
"""
|
||||
Force the output to the type specified by the _to_ suffix in the function name.
|
||||
Converts the return value to match the hinted type.
|
||||
"""
|
||||
if result is None:
|
||||
if hint == "text":
|
||||
return {"result_type": "text", "text": ""}
|
||||
elif hint == "html":
|
||||
return {"result_type": "html", "html": ""}
|
||||
elif hint == "image":
|
||||
return {"result_type": "image", "image_path": ""}
|
||||
elif hint == "audio":
|
||||
return {"result_type": "audio", "audio_path": ""}
|
||||
elif hint == "video":
|
||||
return {"result_type": "video", "video_path": ""}
|
||||
elif hint in ("file", "files"):
|
||||
return {"result_type": hint, "file_paths": []}
|
||||
|
||||
if hint == "text":
|
||||
return {"result_type": "text", "text": str(result) if not isinstance(result, str) else result}
|
||||
elif hint == "html":
|
||||
return {"result_type": "html", "html": str(result) if not isinstance(result, str) else result}
|
||||
elif hint == "image":
|
||||
path = str(result)
|
||||
return {"result_type": "image", "image_path": path}
|
||||
elif hint == "audio":
|
||||
path = str(result)
|
||||
return {"result_type": "audio", "audio_path": path}
|
||||
elif hint == "video":
|
||||
path = str(result)
|
||||
return {"result_type": "video", "video_path": path}
|
||||
elif hint == "file":
|
||||
if isinstance(result, (list, tuple)):
|
||||
paths = [str(p) for p in result]
|
||||
else:
|
||||
paths = [str(result)]
|
||||
return {"result_type": "file", "file_paths": paths}
|
||||
elif hint == "files":
|
||||
if isinstance(result, (list, tuple)):
|
||||
paths = [str(p) for p in result]
|
||||
else:
|
||||
paths = [str(result)]
|
||||
return {"result_type": "files", "file_paths": paths}
|
||||
|
||||
# Fallback (shouldn't happen with valid hints)
|
||||
return {"result_type": "text", "text": str(result)}
|
||||
|
||||
# Pattern matching advanced_paste_from_<input>_to_<output> function names.
|
||||
_AP_FUNCTION_PATTERN = re.compile(
|
||||
r"^advanced_paste_from_(text|html|image|audio|video|files)_to_(text|html|image|audio|video|file|files)$"
|
||||
)
|
||||
|
||||
|
||||
def _load_user_module(script_path: str):
|
||||
"""Dynamically load the user script as a Python module."""
|
||||
spec = importlib.util.spec_from_file_location("_user_script", script_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Cannot load script: {script_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
# Add the script's directory to sys.path so relative imports/helpers work.
|
||||
script_dir = os.path.dirname(os.path.abspath(script_path))
|
||||
if script_dir not in sys.path:
|
||||
sys.path.insert(0, script_dir)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _discover_ap_function(module) -> tuple:
|
||||
"""
|
||||
Discover the single advanced_paste_from_<input>_to_<output> function in the module.
|
||||
Returns a tuple (input_type, output_type, function) or None if not found.
|
||||
Exits with error if multiple functions are defined.
|
||||
"""
|
||||
matches = []
|
||||
for name in dir(module):
|
||||
match = _AP_FUNCTION_PATTERN.match(name)
|
||||
if match:
|
||||
fn = getattr(module, name)
|
||||
if callable(fn):
|
||||
input_type = match.group(1)
|
||||
output_type = match.group(2)
|
||||
matches.append((input_type, output_type, fn))
|
||||
|
||||
if len(matches) == 0:
|
||||
return None
|
||||
if len(matches) > 1:
|
||||
names = [f"advanced_paste_from_{m[0]}_to_{m[1]}" for m in matches]
|
||||
print(
|
||||
f"Error: script defines multiple advanced_paste_from_*_to_* functions "
|
||||
f"({', '.join(names)}). Only one is allowed per script.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _format_output(result, output_type: str) -> dict:
|
||||
"""
|
||||
Format the return value according to the declared output type from the function name.
|
||||
The output_type comes from the _to_ suffix and is always provided.
|
||||
"""
|
||||
if result is None:
|
||||
if output_type in ("file", "files"):
|
||||
return {"result_type": output_type, "file_paths": []}
|
||||
elif output_type == "image":
|
||||
return {"result_type": "image", "image_path": ""}
|
||||
elif output_type == "audio":
|
||||
return {"result_type": "audio", "audio_path": ""}
|
||||
elif output_type == "video":
|
||||
return {"result_type": "video", "video_path": ""}
|
||||
elif output_type == "html":
|
||||
return {"result_type": "html", "html": ""}
|
||||
return {"result_type": "text", "text": ""}
|
||||
|
||||
return _apply_output_hint(result, output_type)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: _runner.py <script_path>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
script_path = sys.argv[1]
|
||||
|
||||
if not os.path.isfile(script_path):
|
||||
print(f"Error: script not found: {script_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Read input payload from stdin.
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: invalid JSON input: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Load the user script.
|
||||
module = _load_user_module(script_path)
|
||||
|
||||
# Discover the single advanced_paste_from_* function.
|
||||
ap_result = _discover_ap_function(module)
|
||||
|
||||
if ap_result is None:
|
||||
print(
|
||||
f"Error: script '{os.path.basename(script_path)}' does not define an "
|
||||
f"advanced_paste_from_<input>_to_<output> function.\n"
|
||||
f"Example: def advanced_paste_from_text_to_text(text): return text.upper()",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
input_type, output_type, fn = ap_result
|
||||
|
||||
# Determine the input data key for this function's input type.
|
||||
input_map = {
|
||||
"text": "text",
|
||||
"html": "html",
|
||||
"image": "image_path",
|
||||
"audio": "audio_path",
|
||||
"video": "video_path",
|
||||
"files": "file_paths",
|
||||
}
|
||||
|
||||
key = input_map.get(input_type, input_type)
|
||||
input_value = data.get(key)
|
||||
|
||||
# Expose work_dir as environment variable so scripts can write output files
|
||||
# to a location accessible from both WSL and Windows (under /mnt/c/...).
|
||||
work_dir = data.get("work_dir", "")
|
||||
if work_dir:
|
||||
os.environ["ADVANCED_PASTE_WORK_DIR"] = work_dir
|
||||
|
||||
# Check if the clipboard has matching data for this script's input type.
|
||||
formats = data.get("format", ["text"])
|
||||
if isinstance(formats, str):
|
||||
formats = [formats]
|
||||
|
||||
if input_type not in formats:
|
||||
print(
|
||||
f"Error: script expects '{input_type}' input but clipboard has [{', '.join(formats)}].",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if not input_value:
|
||||
print(
|
||||
f"Error: no data available for format '{input_type}' "
|
||||
f"(expected '{key}' in input payload).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Call the function.
|
||||
result = fn(input_value)
|
||||
output = _format_output(result, output_type)
|
||||
|
||||
# Output JSON result.
|
||||
json.dump(output, sys.stdout, ensure_ascii=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -212,10 +212,10 @@
|
||||
<value>Delete</value>
|
||||
</data>
|
||||
<data name="CustomFormatTextBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Describe what format you want..</value>
|
||||
<value>Search or describe what format you want..</value>
|
||||
</data>
|
||||
<data name="InputTxtBoxTooltip.Text" xml:space="preserve">
|
||||
<value>Describe what format you want..</value>
|
||||
<value>Search or describe what format you want..</value>
|
||||
</data>
|
||||
<data name="LearnMoreLink.Text" xml:space="preserve">
|
||||
<value>Privacy</value>
|
||||
@@ -372,4 +372,73 @@
|
||||
<value>Unable to load Foundry Local model: {0}</value>
|
||||
<comment>{0} is the model identifier. Do not translate {0}.</comment>
|
||||
</data>
|
||||
<data name="PythonNotFound" xml:space="preserve">
|
||||
<value>Python was not found. Please install Python or configure the path in Settings.</value>
|
||||
</data>
|
||||
<data name="WslNotAvailable" xml:space="preserve">
|
||||
<value>WSL is not installed or not available. Cannot run Linux scripts.</value>
|
||||
</data>
|
||||
<data name="PythonScriptFailed" xml:space="preserve">
|
||||
<value>The Python script failed to execute.</value>
|
||||
</data>
|
||||
<data name="PythonScriptTimeout" xml:space="preserve">
|
||||
<value>Script execution timed out ({0} seconds).</value>
|
||||
<comment>{0} is the configured timeout in seconds. Do not translate {0}.</comment>
|
||||
</data>
|
||||
<data name="PythonScriptNotFound" xml:space="preserve">
|
||||
<value>Script file not found: {0}</value>
|
||||
<comment>{0} is the script file path. Do not translate {0}.</comment>
|
||||
</data>
|
||||
<data name="PythonScriptInvalidJson" xml:space="preserve">
|
||||
<value>The script output is not valid JSON.</value>
|
||||
</data>
|
||||
<data name="PythonScriptTrustTitle" xml:space="preserve">
|
||||
<value>Run Python Script?</value>
|
||||
</data>
|
||||
<data name="PythonScriptTrustContent" xml:space="preserve">
|
||||
<value>This script has not been verified. Running untrusted scripts can be a security risk. Do you want to run the following script?
|
||||
|
||||
{0}</value>
|
||||
<comment>{0} is the script file path. Do not translate {0}.</comment>
|
||||
</data>
|
||||
<data name="PythonScriptTrustConfirm" xml:space="preserve">
|
||||
<value>Run</value>
|
||||
</data>
|
||||
<data name="PythonScriptTrustCancel" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="PythonPackageInstallTitle" xml:space="preserve">
|
||||
<value>Install Missing Packages?</value>
|
||||
</data>
|
||||
<data name="PythonPackageInstallContent" xml:space="preserve">
|
||||
<value>The script "{0}" requires the following Python packages that are not installed:
|
||||
|
||||
{1}
|
||||
|
||||
Install them now?</value>
|
||||
<comment>{0} = script display name, {1} = bullet list of package names. Do not translate package names.</comment>
|
||||
</data>
|
||||
<data name="PythonPackageInstallConfirm" xml:space="preserve">
|
||||
<value>Install</value>
|
||||
</data>
|
||||
<data name="PythonPackageInstallCancel" xml:space="preserve">
|
||||
<value>Skip</value>
|
||||
</data>
|
||||
<data name="PythonPackageInstallFailed" xml:space="preserve">
|
||||
<value>Failed to install package(s) "{0}": {1}</value>
|
||||
<comment>{0} = pip package names, {1} = error detail. Do not translate package names.</comment>
|
||||
</data>
|
||||
<data name="PythonPackageInstallTimeout" xml:space="preserve">
|
||||
<value>Package installation for "{0}" timed out ({1} seconds).</value>
|
||||
<comment>{0} = pip package names, {1} = timeout in seconds. Do not translate {0} or {1}.</comment>
|
||||
</data>
|
||||
<data name="ShowErrorDetailsBtn.Content" xml:space="preserve">
|
||||
<value>Show details</value>
|
||||
</data>
|
||||
<data name="ErrorDetailsDialogTitle" xml:space="preserve">
|
||||
<value>Error Details</value>
|
||||
</data>
|
||||
<data name="ErrorDetailsDialogClose" xml:space="preserve">
|
||||
<value>Close</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -16,6 +16,7 @@ using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using AdvancedPaste.Services.PythonScripts;
|
||||
using AdvancedPaste.Settings;
|
||||
using Common.UI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
@@ -41,6 +42,7 @@ namespace AdvancedPaste.ViewModels
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
||||
private readonly IAICredentialsProvider _credentialsProvider;
|
||||
private readonly IPythonScriptService _pythonScriptService;
|
||||
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
@@ -100,6 +102,8 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public ObservableCollection<PasteFormat> CustomActionPasteFormats { get; } = [];
|
||||
|
||||
public ObservableCollection<PasteFormat> PythonScriptPasteFormats { get; } = [];
|
||||
|
||||
public bool IsCustomAIServiceEnabled
|
||||
{
|
||||
get
|
||||
@@ -258,11 +262,12 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public event EventHandler PreviewRequested;
|
||||
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor, IPythonScriptService pythonScriptService)
|
||||
{
|
||||
_credentialsProvider = credentialsProvider;
|
||||
_userSettings = userSettings;
|
||||
_pasteFormatExecutor = pasteFormatExecutor;
|
||||
_pythonScriptService = pythonScriptService;
|
||||
|
||||
GeneratedResponses = [];
|
||||
GeneratedResponses.CollectionChanged += (s, e) =>
|
||||
@@ -413,12 +418,51 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
|
||||
UpdateFormats(StandardPasteFormats, Enum.GetValues<PasteFormats>()
|
||||
.Where(format => PasteFormat.MetadataDict[format].IsCoreAction || _userSettings.AdditionalActions.Contains(format))
|
||||
.Where(format => format != PasteFormats.PythonScript &&
|
||||
(PasteFormat.MetadataDict[format].IsCoreAction || _userSettings.AdditionalActions.Contains(format)))
|
||||
.Select(CreateStandardPasteFormat));
|
||||
|
||||
UpdateFormats(
|
||||
CustomActionPasteFormats,
|
||||
IsCustomAIServiceEnabled ? _userSettings.CustomActions.Select(customAction => CreateCustomAIPasteFormat(customAction.Name, customAction.Prompt, isSavedQuery: true)) : []);
|
||||
|
||||
UpdateFormats(
|
||||
PythonScriptPasteFormats,
|
||||
BuildPythonScriptFormats());
|
||||
}
|
||||
|
||||
private IEnumerable<PasteFormat> BuildPythonScriptFormats()
|
||||
{
|
||||
if (!_userSettings.IsPythonScriptsEnabled)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var folder = _userSettings.PythonScriptsFolder;
|
||||
if (string.IsNullOrWhiteSpace(folder))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
var discoveredScripts = _pythonScriptService.DiscoverScripts(folder);
|
||||
var scriptActions = _userSettings.PythonScriptActions;
|
||||
|
||||
// Use metadata from discovered scripts, but apply IsShown from saved settings.
|
||||
var hiddenPaths = new System.Collections.Generic.HashSet<string>(
|
||||
scriptActions.Where(a => !a.IsShown).Select(a => a.ScriptPath),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var meta in discoveredScripts)
|
||||
{
|
||||
if (hiddenPaths.Contains(meta.ScriptPath) || !meta.IsEnabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter by intersection: only pass clipboard formats the script supports.
|
||||
var filteredFormats = AvailableClipboardFormats & meta.SupportedFormats;
|
||||
yield return PasteFormat.CreatePythonScriptFormat(meta.Name, meta.ScriptPath, filteredFormats);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -692,7 +736,10 @@ namespace AdvancedPaste.ViewModels
|
||||
_pasteActionCancellationTokenSource = new();
|
||||
TransformProgress = double.NaN;
|
||||
PasteActionError = PasteActionError.None;
|
||||
Query = pasteFormat.Query;
|
||||
|
||||
// For Python scripts the Prompt field holds the file path, not a user-visible query.
|
||||
// Setting Query to the path would show it in the AI prompt box, which is misleading.
|
||||
Query = pasteFormat.Format == PasteFormats.PythonScript ? string.Empty : pasteFormat.Query;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -732,7 +779,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
internal async Task ExecutePasteFormatAsync(VirtualKey key)
|
||||
{
|
||||
var pasteFormat = StandardPasteFormats.Concat(CustomActionPasteFormats)
|
||||
var pasteFormat = StandardPasteFormats.Concat(CustomActionPasteFormats).Concat(PythonScriptPasteFormats)
|
||||
.Where(pasteFormat => pasteFormat.IsEnabled)
|
||||
.ElementAtOrDefault(key - VirtualKey.Number1);
|
||||
|
||||
|
||||
@@ -21,13 +21,9 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar x:Name="titleBar" IsTabStop="False">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="/Assets/EnvironmentVariables/EnvironmentVariables.ico" />
|
||||
</TitleBar.LeftHeader>
|
||||
<TitleBar.IconSource>
|
||||
<ImageIconSource ImageSource="/Assets/EnvironmentVariables/EnvironmentVariables.ico" />
|
||||
</TitleBar.IconSource>
|
||||
</TitleBar>
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -27,8 +27,8 @@ namespace EnvironmentVariables
|
||||
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
SetTitleBar(titleBar);
|
||||
|
||||
AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico");
|
||||
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
var title = App.GetService<IElevationHelper>().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle");
|
||||
|
||||
|
||||
@@ -21,13 +21,9 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar x:Name="titleBar" IsTabStop="False">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="/Assets/FileLocksmith/Icon.ico" />
|
||||
</TitleBar.LeftHeader>
|
||||
<TitleBar.IconSource>
|
||||
<ImageIconSource ImageSource="/Assets/FileLocksmith/Icon.ico" />
|
||||
</TitleBar.IconSource>
|
||||
</TitleBar>
|
||||
<views:MainPage x:Name="mainPage" Grid.Row="1" />
|
||||
</Grid>
|
||||
|
||||
@@ -20,7 +20,6 @@ namespace FileLocksmithUI
|
||||
mainPage.ViewModel.IsElevated = isElevated;
|
||||
SetTitleBar(titleBar);
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Tall;
|
||||
AppWindow.SetIcon("Assets/FileLocksmith/Icon.ico");
|
||||
WindowHelpers.ForceTopBorder1PixelInsetOnWindows10(this.GetWindowHandle());
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "HostsEditor.FuzzTests.dll",
|
||||
"class": "HostsEditor.FuzzTests.FuzzTests",
|
||||
"class": "Hosts.FuzzTests.FuzzTests",
|
||||
"method": "FuzzValidIPv4",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.Hosts.dll"
|
||||
@@ -46,7 +46,7 @@
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "HostsEditor.FuzzTests.dll",
|
||||
"class": "HostsEditor.FuzzTests.FuzzTests",
|
||||
"class": "Hosts.FuzzTests.FuzzTests",
|
||||
"method": "FuzzValidIPv6",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.Hosts.dll"
|
||||
@@ -87,7 +87,7 @@
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "HostsEditor.FuzzTests.dll",
|
||||
"class": "HostsEditor.FuzzTests.FuzzTests",
|
||||
"class": "Hosts.FuzzTests.FuzzTests",
|
||||
"method": "FuzzValidHosts",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.Hosts.dll"
|
||||
@@ -128,7 +128,7 @@
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "HostsEditor.FuzzTests.dll",
|
||||
"class": "HostsEditor.FuzzTests.FuzzTests",
|
||||
"class": "Hosts.FuzzTests.FuzzTests",
|
||||
"method": "FuzzWriteAsync",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.Hosts.dll"
|
||||
|
||||
@@ -21,13 +21,9 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<TitleBar x:Name="titleBar" IsTabStop="False">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="/Assets/Hosts/Hosts.ico" />
|
||||
</TitleBar.LeftHeader>
|
||||
<TitleBar.IconSource>
|
||||
<ImageIconSource ImageSource="/Assets/Hosts/Hosts.ico" />
|
||||
</TitleBar.IconSource>
|
||||
</TitleBar>
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -940,10 +940,12 @@ VideoRecordingSession::VideoRecordingSession(
|
||||
video.PixelAspectRatio().Denominator(1);
|
||||
m_encodingProfile.Video(video);
|
||||
|
||||
// Always set up audio profile for loopback capture (stereo AAC)
|
||||
auto audio = m_encodingProfile.Audio();
|
||||
audio = winrt::AudioEncodingProperties::CreateAac(48000, 2, 192000);
|
||||
m_encodingProfile.Audio(audio);
|
||||
if (captureAudio || captureSystemAudio)
|
||||
{
|
||||
auto audio = m_encodingProfile.Audio();
|
||||
audio = winrt::AudioEncodingProperties::CreateAac(48000, 2, 192000);
|
||||
m_encodingProfile.Audio(audio);
|
||||
}
|
||||
|
||||
// Describe our input: uncompressed BGRA8 buffers
|
||||
auto properties = winrt::VideoEncodingProperties::CreateUncompressed(
|
||||
@@ -964,8 +966,14 @@ VideoRecordingSession::VideoRecordingSession(
|
||||
winrt::check_hresult(m_previewSwapChain->GetBuffer(0, winrt::guid_of<ID3D11Texture2D>(), backBuffer.put_void()));
|
||||
winrt::check_hresult(m_d3dDevice->CreateRenderTargetView(backBuffer.get(), nullptr, m_renderTargetView.put()));
|
||||
|
||||
// Always create audio generator for loopback capture; captureAudio controls microphone
|
||||
m_audioGenerator = std::make_unique<AudioSampleGenerator>(captureAudio, captureSystemAudio, micMonoMix);
|
||||
if (captureAudio || captureSystemAudio)
|
||||
{
|
||||
m_audioGenerator = std::make_unique<AudioSampleGenerator>(captureAudio, captureSystemAudio, micMonoMix);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_audioGenerator = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1207,14 +1215,8 @@ void VideoRecordingSession::OnMediaStreamSourceSampleRequested(
|
||||
{
|
||||
try
|
||||
{
|
||||
if (auto sample = m_audioGenerator->TryGetNextSample())
|
||||
{
|
||||
request.Sample(sample.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Sample(nullptr);
|
||||
}
|
||||
auto sample = m_audioGenerator ? m_audioGenerator->TryGetNextSample() : std::optional<winrt::MediaStreamSample>{};
|
||||
request.Sample(sample.has_value() ? sample.value() : nullptr);
|
||||
}
|
||||
catch (winrt::hresult_error const& error)
|
||||
{
|
||||
|
||||
@@ -5507,9 +5507,29 @@ auto GetUniqueRecordingFilename()
|
||||
return GetUniqueFilename(g_RecordingSaveLocation, defaultFile, FOLDERID_Videos);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
//
|
||||
// GetUniqueScreenshotFilename
|
||||
//
|
||||
// Gets a unique file name for screenshot saves, using the current date and
|
||||
// time as a suffix. This reduces the chance that the user could overwrite an
|
||||
// existing file if they are saving multiple captures in the same folder, and
|
||||
// also ensures that ordering is correct when sorted by name.
|
||||
//
|
||||
//----------------------------------------------------------------------------
|
||||
auto GetUniqueScreenshotFilename()
|
||||
{
|
||||
return GetUniqueFilename(g_ScreenshotSaveLocation, DEFAULT_SCREENSHOT_FILE, FOLDERID_Pictures);
|
||||
SYSTEMTIME lt;
|
||||
GetLocalTime(<);
|
||||
|
||||
// Format: "ZoomIt YYYY-MM-DD HHMMSS.png"
|
||||
wchar_t buffer[MAX_PATH];
|
||||
swprintf_s(buffer, L"%s %04d-%02d-%02d %02d%02d%02d.png",
|
||||
APPNAME,
|
||||
lt.wYear, lt.wMonth, lt.wDay,
|
||||
lt.wHour, lt.wMinute, lt.wSecond);
|
||||
|
||||
return std::wstring(buffer);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
@@ -21,6 +21,25 @@ namespace NonLocalizable
|
||||
{
|
||||
const static wchar_t* TOOL_WINDOW_CLASS_NAME = L"AlwaysOnTopWindow";
|
||||
const static wchar_t* WINDOW_IS_PINNED_PROP = L"AlwaysOnTop_Pinned";
|
||||
constexpr UINT SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND = 0xEFE0;
|
||||
constexpr DWORD SYSTEM_EVENT_MENU_POPUP_START = 0x0006;
|
||||
constexpr DWORD SYSTEM_EVENT_MENU_POPUP_END = 0x0007;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
void UnsubscribeEvents(std::vector<HWINEVENTHOOK>& hooks) noexcept
|
||||
{
|
||||
for (const auto hook : hooks)
|
||||
{
|
||||
if (hook)
|
||||
{
|
||||
UnhookWinEvent(hook);
|
||||
}
|
||||
}
|
||||
|
||||
hooks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool isExcluded(HWND window)
|
||||
@@ -32,7 +51,7 @@ bool isExcluded(HWND window)
|
||||
}
|
||||
|
||||
AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
|
||||
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps}),
|
||||
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps, SettingId::ShowInSystemMenu}),
|
||||
m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase)),
|
||||
m_useCentralizedLLKH(useLLKH),
|
||||
m_mainThreadId(mainThreadId),
|
||||
@@ -53,6 +72,11 @@ AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
|
||||
|
||||
SubscribeToEvents();
|
||||
StartTrackingTopmostWindows();
|
||||
|
||||
if (HWND foregroundWindow = GetForegroundWindow())
|
||||
{
|
||||
UpdateSystemMenuItem(foregroundWindow);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -144,6 +168,13 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SettingId::ShowInSystemMenu:
|
||||
{
|
||||
UpdateSystemMenuEventHooks(AlwaysOnTopSettings::settings().showInSystemMenu);
|
||||
m_lastSystemMenuWindow = nullptr;
|
||||
UpdateSystemMenuItem(GetForegroundWindow());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -225,6 +256,8 @@ void AlwaysOnTop::ProcessCommand(HWND window)
|
||||
{
|
||||
m_sound.Play(soundType);
|
||||
}
|
||||
|
||||
UpdateSystemMenuItem(window);
|
||||
}
|
||||
|
||||
void AlwaysOnTop::StartTrackingTopmostWindows()
|
||||
@@ -414,6 +447,86 @@ void AlwaysOnTop::SubscribeToEvents()
|
||||
Logger::error(L"Failed to set win event hook");
|
||||
}
|
||||
}
|
||||
|
||||
UpdateSystemMenuEventHooks(AlwaysOnTopSettings::settings().showInSystemMenu);
|
||||
}
|
||||
|
||||
void AlwaysOnTop::UpdateSystemMenuEventHooks(bool enable)
|
||||
{
|
||||
constexpr std::array<DWORD, 3> menu_events_to_subscribe = {
|
||||
NonLocalizable::SYSTEM_EVENT_MENU_POPUP_START,
|
||||
NonLocalizable::SYSTEM_EVENT_MENU_POPUP_END,
|
||||
EVENT_OBJECT_INVOKED,
|
||||
};
|
||||
|
||||
if (enable)
|
||||
{
|
||||
if (m_systemMenuWinEventHooks.size() == menu_events_to_subscribe.size())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Recover from any partial hook registration before re-registering.
|
||||
UnsubscribeEvents(m_systemMenuWinEventHooks);
|
||||
|
||||
for (const auto event : menu_events_to_subscribe)
|
||||
{
|
||||
auto hook = SetWinEventHook(event, event, nullptr, WinHookProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
|
||||
if (hook)
|
||||
{
|
||||
m_systemMenuWinEventHooks.emplace_back(hook);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Failed to set system menu win event hook");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UnsubscribeEvents(m_systemMenuWinEventHooks);
|
||||
}
|
||||
}
|
||||
|
||||
void AlwaysOnTop::UpdateSystemMenuItem(HWND window) const noexcept
|
||||
{
|
||||
if (!window || !IsWindow(window))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto systemMenu = GetSystemMenu(window, false);
|
||||
if (!systemMenu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AlwaysOnTopSettings::settings().showInSystemMenu)
|
||||
{
|
||||
if (GetMenuState(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, MF_BYCOMMAND) != static_cast<UINT>(-1))
|
||||
{
|
||||
RemoveMenu(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, MF_BYCOMMAND);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto text = GET_RESOURCE_STRING(IDS_SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP);
|
||||
MENUITEMINFOW menuItemInfo{};
|
||||
menuItemInfo.cbSize = sizeof(menuItemInfo);
|
||||
menuItemInfo.fMask = MIIM_ID | MIIM_STATE | MIIM_STRING;
|
||||
menuItemInfo.wID = NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND;
|
||||
menuItemInfo.fState = IsPinned(window) ? MFS_CHECKED : MFS_UNCHECKED;
|
||||
menuItemInfo.dwTypeData = text.data();
|
||||
|
||||
if (GetMenuState(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, MF_BYCOMMAND) == static_cast<UINT>(-1))
|
||||
{
|
||||
InsertMenuItemW(systemMenu, SC_CLOSE, FALSE, &menuItemInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
menuItemInfo.fMask = MIIM_STATE | MIIM_STRING;
|
||||
SetMenuItemInfoW(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, FALSE, &menuItemInfo);
|
||||
}
|
||||
}
|
||||
|
||||
void AlwaysOnTop::UnpinAll()
|
||||
@@ -434,6 +547,9 @@ void AlwaysOnTop::UnpinAll()
|
||||
|
||||
void AlwaysOnTop::CleanUp()
|
||||
{
|
||||
UnsubscribeEvents(m_systemMenuWinEventHooks);
|
||||
UnsubscribeEvents(m_staticWinEventHooks);
|
||||
|
||||
UnpinAll();
|
||||
if (m_window)
|
||||
{
|
||||
@@ -492,6 +608,79 @@ bool AlwaysOnTop::IsTracked(HWND window) const noexcept
|
||||
|
||||
void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
|
||||
{
|
||||
switch (data->event)
|
||||
{
|
||||
case NonLocalizable::SYSTEM_EVENT_MENU_POPUP_START:
|
||||
{
|
||||
if (data->idObject == OBJID_SYSMENU && data->hwnd)
|
||||
{
|
||||
m_lastSystemMenuWindow = AlwaysOnTopSettings::settings().showInSystemMenu ? data->hwnd : nullptr;
|
||||
UpdateSystemMenuItem(data->hwnd);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case NonLocalizable::SYSTEM_EVENT_MENU_POPUP_END:
|
||||
{
|
||||
if (data->idObject == OBJID_SYSMENU && data->hwnd == m_lastSystemMenuWindow)
|
||||
{
|
||||
m_lastSystemMenuWindow = nullptr;
|
||||
}
|
||||
}
|
||||
return;
|
||||
case EVENT_OBJECT_INVOKED:
|
||||
{
|
||||
if (!AlwaysOnTopSettings::settings().showInSystemMenu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (data->idChild != static_cast<LONG>(NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const bool isMenuRelatedObject = (data->idObject == OBJID_SYSMENU || data->idObject == OBJID_MENU || data->idObject == OBJID_CLIENT);
|
||||
if (!isMenuRelatedObject && (!m_lastSystemMenuWindow || !IsWindow(m_lastSystemMenuWindow)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto hasToggleMenuItem = [](HWND window) -> bool {
|
||||
if (!window || !IsWindow(window))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto systemMenu = GetSystemMenu(window, false);
|
||||
return systemMenu &&
|
||||
GetMenuState(systemMenu, NonLocalizable::SYSTEM_MENU_TOGGLE_ALWAYS_ON_TOP_COMMAND, MF_BYCOMMAND) != static_cast<UINT>(-1);
|
||||
};
|
||||
|
||||
HWND commandWindow = nullptr;
|
||||
const auto trySetCommandWindow = [&](HWND candidate) noexcept {
|
||||
if (!commandWindow && hasToggleMenuItem(candidate))
|
||||
{
|
||||
commandWindow = candidate;
|
||||
}
|
||||
};
|
||||
|
||||
if (m_lastSystemMenuWindow && IsWindow(m_lastSystemMenuWindow))
|
||||
{
|
||||
trySetCommandWindow(m_lastSystemMenuWindow);
|
||||
}
|
||||
trySetCommandWindow(data->hwnd);
|
||||
trySetCommandWindow(GetForegroundWindow());
|
||||
|
||||
if (commandWindow)
|
||||
{
|
||||
ProcessCommand(commandWindow);
|
||||
}
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!AlwaysOnTopSettings::settings().enableFrame || !data->hwnd)
|
||||
{
|
||||
return;
|
||||
@@ -566,6 +755,8 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
|
||||
break;
|
||||
case EVENT_SYSTEM_FOREGROUND:
|
||||
{
|
||||
UpdateSystemMenuItem(data->hwnd);
|
||||
|
||||
if (!is_process_elevated() && IsProcessOfWindowElevated(data->hwnd))
|
||||
{
|
||||
m_notificationUtil->WarnIfElevationIsRequired(GET_RESOURCE_STRING(IDS_ALWAYSONTOP),
|
||||
@@ -776,4 +967,4 @@ void AlwaysOnTop::RestoreWindowAlpha(HWND window)
|
||||
SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ private:
|
||||
|
||||
static inline AlwaysOnTop* s_instance = nullptr;
|
||||
std::vector<HWINEVENTHOOK> m_staticWinEventHooks{};
|
||||
std::vector<HWINEVENTHOOK> m_systemMenuWinEventHooks{};
|
||||
Sound m_sound;
|
||||
VirtualDesktopUtils m_virtualDesktopUtils;
|
||||
|
||||
@@ -69,15 +70,18 @@ private:
|
||||
std::thread m_thread;
|
||||
const bool m_useCentralizedLLKH;
|
||||
bool m_running = true;
|
||||
HWND m_lastSystemMenuWindow{ nullptr };
|
||||
std::unique_ptr<notifications::NotificationUtil> m_notificationUtil;
|
||||
|
||||
LRESULT WndProc(HWND, UINT, WPARAM, LPARAM) noexcept;
|
||||
void HandleWinHookEvent(WinHookEvent* data) noexcept;
|
||||
void UpdateSystemMenuItem(HWND window) const noexcept;
|
||||
|
||||
bool InitMainWindow();
|
||||
void RegisterHotkey() const;
|
||||
void RegisterLLKH();
|
||||
void SubscribeToEvents();
|
||||
void UpdateSystemMenuEventHooks(bool enable);
|
||||
|
||||
void ProcessCommand(HWND window);
|
||||
void StartTrackingTopmostWindows();
|
||||
|
||||
@@ -131,4 +131,7 @@
|
||||
<data name="System_Foreground_Elevated_Dialog_Dont_Show_Again" xml:space="preserve">
|
||||
<value>Don't show again</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="System_Menu_Toggle_Always_On_Top" xml:space="preserve">
|
||||
<value>Always on top</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace NonLocalizable
|
||||
|
||||
const static wchar_t* HotkeyID = L"hotkey";
|
||||
const static wchar_t* SoundEnabledID = L"sound-enabled";
|
||||
const static wchar_t* ShowInSystemMenuID = L"show-in-system-menu";
|
||||
const static wchar_t* FrameEnabledID = L"frame-enabled";
|
||||
const static wchar_t* FrameThicknessID = L"frame-thickness";
|
||||
const static wchar_t* FrameColorID = L"frame-color";
|
||||
@@ -115,6 +116,16 @@ void AlwaysOnTopSettings::LoadSettings()
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::ShowInSystemMenuID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
if (m_settings.showInSystemMenu != val)
|
||||
{
|
||||
m_settings.showInSystemMenu = val;
|
||||
NotifyObservers(SettingId::ShowInSystemMenu);
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto jsonVal = values.get_int_value(NonLocalizable::FrameThicknessID))
|
||||
{
|
||||
auto val = *jsonVal;
|
||||
|
||||
@@ -18,6 +18,7 @@ struct Settings
|
||||
static constexpr int minTransparencyPercentage = 20; // minimum transparency (can't go below 20%)
|
||||
static constexpr int maxTransparencyPercentage = 100; // maximum (fully opaque)
|
||||
static constexpr int transparencyStep = 10; // step size for +/- adjustment
|
||||
bool showInSystemMenu = false;
|
||||
bool enableFrame = true;
|
||||
bool enableSound = true;
|
||||
bool roundCornersEnabled = true;
|
||||
@@ -56,4 +57,4 @@ private:
|
||||
std::unordered_set<SettingsObserver*> m_observers;
|
||||
|
||||
void NotifyObservers(SettingId id) const;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ enum class SettingId
|
||||
{
|
||||
Hotkey = 0,
|
||||
SoundEnabled,
|
||||
ShowInSystemMenu,
|
||||
FrameEnabled,
|
||||
FrameThickness,
|
||||
FrameColor,
|
||||
@@ -12,4 +13,4 @@ enum class SettingId
|
||||
ExcludeApps,
|
||||
FrameAccentColor,
|
||||
RoundCornersEnabled
|
||||
};
|
||||
};
|
||||
|
||||
@@ -264,3 +264,15 @@ dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
[*.{cs,vb}]
|
||||
# CS8305: Type is for evaluation purposes only and is subject to change or removal in future updates.
|
||||
dotnet_diagnostic.CS8305.severity = suggestion
|
||||
|
||||
##################################################
|
||||
# Solutions and projects
|
||||
##################################################
|
||||
|
||||
[*.{*proj,props,target}]
|
||||
tab_width = 2
|
||||
indent_size = 2
|
||||
end_of_line = crlf
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
|
||||
"name": "Update template project",
|
||||
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory."
|
||||
},
|
||||
{
|
||||
"input": " .\\extensionsdk\\nuget\\BuildSDKHelper.ps1 -VersionOfSDK 0.0.1",
|
||||
"name": "Build SDK",
|
||||
"description": "Builds the SDK nuget package with the specified version."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
"src\\common\\version\\version.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
|
||||
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
|
||||
"src\\modules\\cmdpal\\Core\\Microsoft.CmdPal.Core.Common\\Microsoft.CmdPal.Core.Common.csproj",
|
||||
"src\\modules\\cmdpal\\Core\\Microsoft.CmdPal.Core.ViewModels\\Microsoft.CmdPal.Core.ViewModels.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI.ViewModels\\Microsoft.CmdPal.UI.ViewModels.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI\\Microsoft.CmdPal.UI.csproj",
|
||||
"src\\modules\\cmdpal\\Microsoft.Terminal.UI\\Microsoft.Terminal.UI.vcxproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Core.Common.UnitTests\\Microsoft.CmdPal.Core.Common.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Common.UnitTests\\Microsoft.CmdPal.Common.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Apps.UnitTests\\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj",
|
||||
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Calc.UnitTests\\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj",
|
||||
@@ -37,6 +36,7 @@
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Calc\\Microsoft.CmdPal.Ext.Calc.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.ClipboardHistory\\Microsoft.CmdPal.Ext.ClipboardHistory.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Indexer\\Microsoft.CmdPal.Ext.Indexer.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.PerformanceMonitor\\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Registry\\Microsoft.CmdPal.Ext.Registry.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.RemoteDesktop\\Microsoft.CmdPal.Ext.RemoteDesktop.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.Shell\\Microsoft.CmdPal.Ext.Shell.csproj",
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\CoreCommonProps.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Core.Common</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
||||
|
||||
internal sealed partial class PiiRuleProvider : ISanitizationRuleProvider
|
||||
{
|
||||
public IEnumerable<SanitizationRule> GetRules()
|
||||
{
|
||||
yield return new(EmailRx(), "[EMAIL_REDACTED]", "Email addresses");
|
||||
yield return new(SsnRx(), "[SSN_REDACTED]", "Social Security Numbers");
|
||||
yield return new(CreditCardRx(), "[CARD_REDACTED]", "Credit card numbers");
|
||||
|
||||
// phone number regex is the most generic, so it goes last
|
||||
// we can't make this too generic; otherwise we over-redact error codes, dates, etc.
|
||||
yield return new(PhoneRx(), "[PHONE_REDACTED]", "Phone numbers");
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\b[a-zA-Z0-9]([a-zA-Z0-9._%-]*[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}\b",
|
||||
SanitizerDefaults.DefaultOptions, SanitizerDefaults.DefaultMatchTimeoutMs)]
|
||||
private static partial Regex EmailRx();
|
||||
|
||||
[GeneratedRegex("""
|
||||
(?xi)
|
||||
# ---------- boundaries ----------
|
||||
(?<!\w) # not after a letter/digit/underscore
|
||||
(?<![A-Za-z0-9]-) # avoid starting inside hyphenated tokens (GUID middles, etc.)
|
||||
|
||||
# ---------- global do-not-match guards ----------
|
||||
(?! # ISO date (yyyy-mm-dd / yyyy.mm.dd / yyyy/mm/dd)
|
||||
(?:19|20)\d{2}[-./](?:0[1-9]|1[0-2])[-./](?:0[1-9]|[12]\d|3[01])\b
|
||||
)
|
||||
(?! # EU date (dd-mm-yyyy / dd.mm.yyyy / dd/mm/yyyy)
|
||||
(?:0[1-9]|[12]\d|3[01])[-./](?:0[1-9]|1[0-2])[-./](?:19|20)\d{2}\b
|
||||
)
|
||||
(?! # ISO datetime like 2025-08-24T14:32[:ss][Z|±hh:mm]
|
||||
(?:19|20)\d{2}-\d{2}-\d{2}[T\s]\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?\b
|
||||
)
|
||||
(?!\b(?:\d{1,3}\.){3}\d{1,3}(?::\d{1,5})?\b) # IPv4 with optional :port
|
||||
(?!\b[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\b) # GUID, lowercase
|
||||
(?!\b[0-9A-F]{8}-(?:[0-9A-F]{4}-){3}[0-9A-F]{12}\b) # GUID, uppercase
|
||||
(?!\bv?\d+(?:\.\d+){2,}\b) # semantic/file versions like 1.2.3 or 10.0.22631.3448
|
||||
(?!\b(?:[0-9A-F]{2}[:-]){5}[0-9A-F]{2}\b) # MAC address
|
||||
|
||||
# ---------- digit budget ----------
|
||||
(?=(?:\D*\d){7,15}) # 7–15 digits in total
|
||||
|
||||
# ---------- number body ----------
|
||||
(?:
|
||||
# A with explicit country code, allow compact digits (E.164-ish) or grouped
|
||||
(?:\+|00)[1-9]\d{0,2}
|
||||
(?:
|
||||
[\p{Zs}.\-\/]*\d{6,14}
|
||||
|
|
||||
[\p{Zs}.\-\/]* (?:\(\d{1,4}\)|\d{1,4})
|
||||
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
|
||||
)
|
||||
|
|
||||
# B no country code => require separators between blocks (avoid plain big ints)
|
||||
(?:\(\d{1,4}\)|\d{1,4})
|
||||
(?:[\p{Zs}.\-\/]+(?:\(\d{2,4}\)|\d{2,4})){1,6}
|
||||
)
|
||||
|
||||
# ---------- optional extension ----------
|
||||
(?:[\p{Zs}.\-,:;]* (?:ext\.?|x) [\p{Zs}]* (?<ext>\d{1,6}))?
|
||||
|
||||
(?!-\w) # don't end just before '-letter'/'-digit'
|
||||
""",
|
||||
SanitizerDefaults.DefaultOptions | RegexOptions.IgnorePatternWhitespace, SanitizerDefaults.DefaultMatchTimeoutMs)]
|
||||
private static partial Regex PhoneRx();
|
||||
|
||||
[GeneratedRegex(@"\b\d{3}-\d{2}-\d{4}\b",
|
||||
SanitizerDefaults.DefaultOptions, SanitizerDefaults.DefaultMatchTimeoutMs)]
|
||||
private static partial Regex SsnRx();
|
||||
|
||||
[GeneratedRegex(@"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b",
|
||||
SanitizerDefaults.DefaultOptions, SanitizerDefaults.DefaultMatchTimeoutMs)]
|
||||
private static partial Regex CreditCardRx();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record HideDetailsMessage()
|
||||
{
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record LaunchUriMessage(Uri Uri)
|
||||
{
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record ShowDetailsMessage(DetailsViewModel Details)
|
||||
{
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\CoreCommonProps.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"allowMarshaling": false
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
GetPhysicallyInstalledSystemMemory
|
||||
GlobalMemoryStatusEx
|
||||
GetSystemInfo
|
||||
CoCreateInstance
|
||||
SetForegroundWindow
|
||||
IsIconic
|
||||
RegisterHotKey
|
||||
SetWindowLongPtr
|
||||
CallWindowProc
|
||||
ShowWindow
|
||||
SetForegroundWindow
|
||||
SetFocus
|
||||
SetActiveWindow
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
SHCreateStreamOnFileEx
|
||||
CoAllowSetForegroundWindow
|
||||
SHCreateStreamOnFileEx
|
||||
SHLoadIndirectString
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
|
||||
: PageViewModel(null, scheduler, extensionHost);
|
||||
@@ -1,72 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.ViewModels.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show details.
|
||||
/// </summary>
|
||||
public static string ShowDetailsCommand {
|
||||
get {
|
||||
return ResourceManager.GetString("ShowDetailsCommand", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ShowDetailsCommand" xml:space="preserve">
|
||||
<value>Show details</value>
|
||||
<comment>Name for the command that shows details of an item</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -10,7 +10,7 @@
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4188" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools.MSIX" Version="1.7.20250829.1" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260209005" />
|
||||
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
namespace Microsoft.CmdPal.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the packaging flavor of the application.
|
||||
@@ -2,9 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
namespace Microsoft.CmdPal.Common;
|
||||
|
||||
public static class CoreLogger
|
||||
{
|
||||
@@ -15,6 +13,8 @@ public static class CoreLogger
|
||||
|
||||
private static ILogger? _logger;
|
||||
|
||||
public static ILogger? Instance => _logger;
|
||||
|
||||
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);
|
||||
@@ -2,10 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides utility methods for building diagnostic and error messages.
|
||||
@@ -2,12 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
namespace Microsoft.CmdPal.Common;
|
||||
|
||||
public partial class ExtensionHostInstance
|
||||
{
|
||||
@@ -2,9 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an item that can provide precomputed fuzzy matching targets for its title and subtitle.
|
||||
@@ -2,9 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe boolean implementation using atomic operations
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.CmdPal.Core.Common.Text;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static partial class InternalListHelpers
|
||||
{
|
||||
@@ -2,12 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static partial class NativeEventWaiter
|
||||
{
|
||||
@@ -6,7 +6,7 @@ using System.Runtime.CompilerServices;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Storage.FileSystem;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static class PathHelper
|
||||
{
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public partial class PinnedDockItem : WrappedDockItem
|
||||
{
|
||||
public override string Title => $"{base.Title} ({Properties.Resources.PinnedItemSuffix})";
|
||||
|
||||
public PinnedDockItem(ICommand command)
|
||||
: base(command, command.Name)
|
||||
{
|
||||
}
|
||||
|
||||
public PinnedDockItem(IListItem item, string id)
|
||||
: base([item], id, item.Title)
|
||||
{
|
||||
Icon = item.Icon;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// An async gate that ensures only one operation runs at a time.
|
||||
@@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// An async gate that ensures only one value computation runs at a time.
|
||||
@@ -5,7 +5,7 @@
|
||||
using System.Diagnostics;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for retrieving application version information safely.
|
||||
@@ -4,10 +4,9 @@
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Helpers;
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known key chords used in the Command Palette and extensions.
|
||||
@@ -1,7 +1,9 @@
|
||||
<Project>
|
||||
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Common</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- For MVVM Toolkit Partial Properties/AOT support -->
|
||||
<LangVersion>preview</LangVersion>
|
||||
@@ -23,6 +25,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -32,20 +39,24 @@
|
||||
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(MSBuildThisFileDirectory)\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
|
||||
<_Parameter1>$(AssemblyName).UnitTests</_Parameter1>
|
||||
<_Parameter1>$(AssemblyName).UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
namespace Microsoft.CmdPal.Common.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.Common.Properties.Resources", typeof(Resources).Assembly);
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Common.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
@@ -72,5 +72,14 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
|
||||
return ResourceManager.GetString("ErrorReport_Global_Preamble", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pinned.
|
||||
/// </summary>
|
||||
internal static string PinnedItemSuffix {
|
||||
get {
|
||||
return ResourceManager.GetString("PinnedItemSuffix", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -117,6 +117,10 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
<data name="ErrorReport_Global_Preamble" xml:space="preserve">
|
||||
<value>This is an error report generated by Windows Command Palette.
|
||||
If you are seeing this, it means something went a little sideways in the app.
|
||||
@@ -124,4 +128,4 @@ You can help us fix it by filing a report at https://aka.ms/powerToysReportBug.
|
||||
|
||||
(While you’re at it, give the details below a quick skim — just to make sure there’s nothing personal you’d prefer not to share. It’s rare, but sometimes little surprises sneak in.)</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
@@ -5,11 +5,11 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of IApplicationInfoService providing application-wide information.
|
||||
@@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to application-wide information such as version, packaging flavor, and directory paths.
|
||||
@@ -2,11 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
public interface IExtensionService
|
||||
{
|
||||
@@ -2,13 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
public interface IExtensionWrapper
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services;
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
public interface IRunHistoryService
|
||||
{
|
||||
@@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Reports;
|
||||
namespace Microsoft.CmdPal.Common.Services.Reports;
|
||||
|
||||
public sealed class ErrorReportBuilder : IErrorReportBuilder
|
||||
{
|
||||
@@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Reports;
|
||||
namespace Microsoft.CmdPal.Common.Services.Reports;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a contract for creating human-readable error reports from exceptions,
|
||||
@@ -2,7 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
|
||||
namespace Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that sanitizes text by applying a set of configurable, regex-based rules.
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
|
||||
namespace Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
|
||||
public readonly record struct SanitizationRule
|
||||
{
|
||||
@@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
||||
namespace Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
|
||||
internal sealed partial class ConnectionStringRuleProvider : ISanitizationRuleProvider
|
||||
{
|
||||
@@ -3,9 +3,9 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
||||
namespace Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
|
||||
internal sealed class EnvironmentPropertiesRuleProvider : ISanitizationRuleProvider
|
||||
{
|
||||
@@ -2,9 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
||||
namespace Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
|
||||
/// <summary>
|
||||
/// Specific sanitizer used for error report content. Builds on top of the generic TextSanitizer.
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
using System.Collections.Frozen;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CmdPal.Core.Common.Services.Sanitizer.Abstraction;
|
||||
using Microsoft.CmdPal.Common.Services.Sanitizer.Abstraction;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common.Services.Sanitizer;
|
||||
namespace Microsoft.CmdPal.Common.Services.Sanitizer;
|
||||
|
||||
internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
|
||||
{
|
||||
@@ -25,6 +25,7 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
|
||||
"env",
|
||||
"environment",
|
||||
"manifest",
|
||||
"log",
|
||||
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IEnumerable<SanitizationRule> GetRules()
|
||||
@@ -61,6 +62,11 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
|
||||
return full;
|
||||
}
|
||||
|
||||
if (IsVersionSegment(file))
|
||||
{
|
||||
return full;
|
||||
}
|
||||
|
||||
string stem, ext;
|
||||
if (dot > 0 && dot < file.Length - 1)
|
||||
{
|
||||
@@ -106,4 +112,30 @@ internal sealed class FilenameMaskRuleProvider : ISanitizationRuleProvider
|
||||
var maskedCount = Math.Max(1, stem.Length - keep);
|
||||
return stem[..keep] + new string('*', maskedCount);
|
||||
}
|
||||
|
||||
private static bool IsVersionSegment(string file)
|
||||
{
|
||||
var dotIndex = file.IndexOf('.');
|
||||
if (dotIndex <= 0 || dotIndex == file.Length - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasDot = false;
|
||||
foreach (var ch in file)
|
||||
{
|
||||
if (ch == '.')
|
||||
{
|
||||
hasDot = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!char.IsDigit(ch))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return hasDot;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user