Compare commits

...

88 Commits

Author SHA1 Message Date
Christian Gaarden Gaardmark
bc56443443 New++ updated attribution (#49047)
## Summary of the Pull Request
* Updated New+ attribution text and link after conversation with Niels
* Text="Based on Christian Gaardmark's New++ from the Productivity Plus
Pack"
*
Link="https://www.onegreatworld.com/products/productivity-plus-pack/?ref=settings_pt"

## PR Checklist
- [ ] Closes: #xxx
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [n/a] **Tests:** Added/updated and all pass
- [n/a] **Localization:** All end-user-facing strings can be localized
- [n/a] **Dev docs:** Added/updated
- [n/a] **New binaries:** Added on the required places
- [n/a] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [n/a] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [n/a] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [n/a] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [n/a] **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
* Text="Based on Christian Gaardmark's New++ from the Productivity Plus
Pack"
*
Link="https://www.onegreatworld.com/products/productivity-plus-pack/?ref=settings_pt"

## Validation Steps Performed
1. Manually confirmed visual layout
2. Manually confirmed link

<img width="470" height="65" alt="image"
src="https://github.com/user-attachments/assets/56d6e454-89cf-4a26-b570-b162df51f4a9"
/>

cc:
@niels9001
2026-07-01 14:18:05 +02:00
Clint Rutkas
3298625b67 Cancel stale Quick Accent toolbar render timer (#48944)
## Summary

Quick Accent's ShowToolbar queues a delayed render of the accent menu
through Task.Delay(...).ContinueWith(...). The continuation only checked
_visible before rendering, so a timer started by an earlier key press
could still fire for a **newer** summon — or one that had already been
hidden — popping the accent menu earlier than the configured delay
intended.

This was surfaced while reviewing the press-and-hold work in #48937, but
it is a pre-existing race independent of that feature, so it ships on
its own.

## Fix

- Tag each `ShowToolbar` summon with an incrementing `_showGeneration`
id and capture it in the local closure.
- The delayed continuation now renders only when it is still the most
recent summon (`generation == _showGeneration`) **and** `_visible`.
- Bump the generation when the toolbar hides, so any in-flight timer
queued before the hide is cancelled.

Everything runs on the UI thread (the dispatcher marshals
`ShowToolbar`/hide and the continuation uses
`FromCurrentSynchronizationContext`), so the counter needs no locking.

## Validation

- Built `PowerAccent.UI` (C++ hook + Core) Debug|x64 — 0 warnings, 0
errors.
- Manual: rapid repeated taps of an accent-capable letter no longer
flash the menu early from a leftover timer; normal hold-to-open and
navigation/commit are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 15:36:50 +08:00
Clint Rutkas
ae9f241ef1 [FancyZones] Harden three shutdown races in WorkArea / ZonesOverlay / OnThreadExecutor (#48473)
## Summary

Four small shutdown-/teardown-race fixes in FancyZones that I spotted
while reading through the work-area and overlay teardown sequence for an
unrelated review. Each one is independently safe in the happy path, but
in combination they can crash the FancyZones host process during display
changes, monitor configuration changes, a settings toggle mid-drag, or
normal exit.

## Issues fixed

### 1. `~ZonesOverlay` joins a non-joinable thread when the constructor
early-returns
`ZonesOverlay::ZonesOverlay` can return early in two places — if
`GetClientRect` fails or if `CreateHwndRenderTarget` returns a failure
HRESULT (both reachable in the wild during a display-driver TDR or when
a monitor is disconnected mid-init). When that happens, `m_renderThread`
is never started and stays default-constructed. The destructor
unconditionally calls `m_renderThread.join()`, which on a non-joinable
thread is undefined behavior (MSVC throws `std::system_error`); thrown
from an implicit-noexcept destructor it calls `std::terminate()`.

Fix: guard the wake-up-and-join sequence with `if
(m_renderThread.joinable())`.

### 2. `~WorkArea` returns the HWND to the window pool before the
renderer is torn down
`WorkArea`'s explicit destructor body calls
`windowPool.FreeZonesOverlayWindow(m_window)` first, and only afterwards
does implicit member destruction run `~ZonesOverlay` (which joins the
render thread). Between those two steps the HWND is back in the pool and
immediately eligible for reuse by the next `NewZonesOverlayWindow` call,
while the still-alive render thread is using `m_renderTarget` to draw
into it. If the pool hands the same HWND to a freshly-built
`ZonesOverlay`, two render targets target the same window concurrently.

Fix: reset `m_zonesOverlay` (which joins the render thread) before
returning the window to the pool.

### 3. `~OnThreadExecutor` writes `_shutdown_request` outside the mutex
The destructor mutates the shutdown flag without holding `_task_mutex`,
then calls `_task_cv.notify_one()`. The worker checks the same flag
inside `_task_cv.wait(lock, predicate)`. The atomic does make the value
visible eventually, but if the notify lands in the narrow window where
the worker has just evaluated the predicate as false and is about to
atomically release the lock and sleep, the wakeup can be missed and
`_worker_thread.join()` hangs.

Fix: take `_task_mutex` around the `_shutdown_request = true` write so
it pairs correctly with the `cv.wait`.

### 4. `WindowMouseSnap` keeps a dangling `WorkArea*` across
`WorkAreaConfiguration::Clear()`
`FancyZones::UpdateWorkAreas()` rebuilds `m_workAreaConfiguration`
whenever monitor state changes mid-session, and the
`SpanZonesAcrossMonitors` settings toggle hits the same `Clear()`. If
the user is mid-drag at the moment one of these runs, the
`WindowMouseSnap` instance owned by `FancyZones` is still holding both a
`const` reference to the map being cleared (`m_activeWorkAreas`) and a
raw `WorkArea*` into one of the entries that's about to be destroyed
(`m_currentWorkArea`). The next `WM_MOUSEMOVE` -> `MoveSizeUpdate()`
then dereferences a freed pointer. `WindowMouseSnap`'s destructor only
resets window transparency, so relying on it doesn't help; the snapper
has to be torn down explicitly.

Fix: call `FancyZones::MoveSizeEnd()` (which already tears down the
snapper cleanly and is a no-op when the snapper is null) before each
`m_workAreaConfiguration.Clear()` call on these paths.

## Risk

Low. All four changes are localized to teardown / reconfiguration paths
and only tighten existing destruction sequences — the steady-state
behavior of `ZonesOverlay::Render`/`Show`/`Hide`, the work-area public
API, `OnThreadExecutor::submit`/`cancel`, and `WindowMouseSnap` drag
handling is unchanged. The `WorkArea` reordering is the most behavioral
change; it now guarantees the render thread has stopped using the HWND
before the pool can recycle it, which is what the existing
implicit-member-destruction order already implied but couldn't enforce
given the explicit destructor body.

## Validation

Spot-built locally; this repo's `dotnet restore` runtime-pack issue
(unrelated to this PR — same NU1102 pattern that's affecting other open
PRs) prevents a full `Build.cmd` here, but the C++ FancyZones modules
involved are unchanged in their public surface and are exercised by
existing unit tests in `FancyZonesTests` for the WorkArea code paths.

---

ADO: https://microsoft.visualstudio.com/OS/_workitems/edit/54653316/

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-01 15:35:03 +08:00
Jiří Polášek
67a9fa2d13 CmdPal: Ensure that directory paths passed from Bookmarks is quoted (#48955)
This PR ensures that directory paths passed from Bookmarks through
`CommandLauncher` to Windows Explorer are properly quoted. In current
implementation the directory path that should be opened through Windows
Explorer is not quoted and if it contains a space then Explorer will
treat it as multiple arguments instead.

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

- [x] Closes: #48672 
<!-- - [ ] 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-06-30 23:24:01 -05:00
Clint Rutkas
1cfc923bdb Fix mismatched WebView2 versions, upgrade WebView2 (#49051) 2026-06-30 18:18:06 -05:00
Clint Rutkas
2dd802f367 Fixing Windows.ImplementationLibrary mismatch between proj and package.config (#49050)
In the vcxproj files, it lists it correctly for
Microsoft.Windows.ImplementationLibrary.1.0.260126.7 but the package
files are incorrect
2026-06-30 18:17:27 -05:00
Clint Rutkas
a0d17406ba Updating MessagePack (#49029)
Version bump on Message Pack
2026-06-30 14:26:32 +02:00
Copilot
4a27c5d5f9 New+: Fix French translation guidance (Nouveau+ not Nouveauté+) (#47225)
## Summary of the Pull Request

French translation of "New+" was rendered as "Nouveauté+" ("Novelty+")
instead of "Nouveau+" ("New+"), inconsistent with how Windows itself
translates the "New" context menu item in French. This updates
translator guidance comments in the English resource files to explicitly
call out the correct French form.

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

Translator-facing `<comment>` fields updated across three resource files
to explicitly state the correct French translation and flag the wrong
one:

- **`NewShellExtensionContextMenu/resources.resx`** and
**`NewShellExtensionContextMenu.win10/resources.resx`** —
`context_menu_item_new`:
> _"…e.g. Danish it would become Ny+, **French it would become Nouveau+
(not Nouveauté+)**"_

- **`Settings.UI/Strings/en-us/Resources.resw`** — five `NewPlus.*` /
`Oobe_NewPlus.*` strings:
> _"…Localize product name in accordance with Windows New. **e.g. French
would be Nouveau+ (not Nouveauté+)**"_

Actual `.lcl` translation files are managed by the CDPX localization
pipeline; these comment updates feed directly into the guidance the
localization team sees when updating those files.

## Validation Steps Performed

Comment-only changes to XML resource files; no runtime behavior
affected. Verified all targeted entries were updated and no existing
checked-in `Nouveauté` strings remain in the 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-06-30 17:06:25 +08:00
Dave Rayment
8bd5c1be6f [Quick Accent] Additions and reorg for IPA set. Additions to Special set (#49030)
<!-- 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

- [x] Closes: #48840
- [x] Closes: #32437
- [x] **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

The following additions and changes were made in response to the
feedback given in #48840 and also following an audit of the IPA set.

#### IPA

| Character | Action
|--|--|
| `ɱ` | Added to **M** |
| `ʍ` | Added to **W** | 
| `ɚ` | Added to **E** |
| ` ͡ ` and `  ͜  ` | Added to **PERIOD**
| `ɡ` | Added to **G**
| `ɫ` | Added to **I**
| `ʱ` | Added to **H**
| `◌̝`, `◌̥`, `◌̚`, `ˈ` and `ˌ` | added to **PERIOD**
| `ʎ` | Added to **Y**
| `ʔ` | Added to **COMMA** and **SLASH**
| `æ` | Added to **A** and **E**
| `œ` | Added to **O** and **E**
| `ʘ` | Added to **B** and **O**
| `β`, `ɓ` | Added to **B**
| `χ` | Added to **C** and **X**
| `ç`, `ǂ` | Added to **C**
| `ð`, `ɗ`, `ɖ`, `ǀ` | Added to **D**
| `ɠ`, `ʛ` | Added to **G**
| `ħ`, `ɥ`, `ɧ` | Added to **H**
| `ʄ` | Added to **J**
| `ɫ`, `ǁ` | Added to **L**
| `ø` | Added to **O**

I removed the caron vowel characters `ǎ`, `ǒ` and `ǔ`, as they should
not have been in the IPA set. These characters are available in the
Pinyin set.

A small number of keys had entries reordered where common mappings would
have been towards the end.

This IPA update includes:

- Click Consonants (`ʘ`, `ǀ`, `ǃ`, `ǂ`, `ǁ`), which are used in the
phonetic transcription of Southern and Eastern African languages, most
notably the Khoisan language groups and several Bantu languages (like
Zulu and Xhosa).
- Implosives and Ejectives (`ɓ`, `ɗ`, `ʼ`, etc.), which are essential
for transcribing languages across the globe, including Indigenous
languages of the Americas (e.g., Navajo and Mayan), the Caucasus (e.g.,
Georgian), Southeast Asia (e.g., Vietnamese), and widely across the
African continent.

#### SPECIAL set

| Character | Action | Notes
|--|--|--|
| `‽` and `⸘` | Added to **SLASH** |
| `⟨`, `⟩`, `⟪` and `⟫` | Replaced full-width CJK brackets with the
Western versions.
| `‰` and `‱` | Added to **P**

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

Manual testing, with specific tests to confirm that the new combining
marks display OK in the UI.
2026-06-30 10:06:02 +08:00
Clint Rutkas
7b19b4c219 Add build-time guard for Windows long path support (#49028)
PowerToys has deeply nested source paths that exceed the legacy
260-character MAX_PATH limit. Contributors who haven't enabled Windows
long path support hit cryptic 'path too long' / 'could not find file'
errors during their first build.

Add an EnsureLongPathsEnabled MSBuild target in Directory.Build.targets
that reads
HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled and
fails fast with an actionable error (PTLONGPATH) pointing at
tools\build\setup-dev-environment.ps1. Covers both Visual Studio and the
command-line build scripts, skips design-time builds, and can be
bypassed with /p:SkipLongPathsCheck=true.

**What happens if Long file path isn't enabled.**
<img width="824" height="916" alt="image"
src="https://github.com/user-attachments/assets/30731f65-4011-48c0-94b9-e521b4c7d266"
/>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-29 21:29:14 +00:00
Jessica Dene Earley-Cha
b73fd670be [CmdPal] Fix excessive Narrator announcements on More button open (#48928)
<!-- 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

Opening the More button caused Narrator to read a long cascade of
overlapping announcements — popup window, filter TextBox placeholder,
ListView item name, position ("1 of 4"), keyboard shortcut text — all
from a single keypress with no further input.

This PR replaces that cascade with a single, clean announcement:
**"Menu, {0} commands. {1}, {2} of {0}"**
(num of commands, first item in list, num of order in list, num of
commands)
**"Menu, 3 commands. Calculator, 1 of 3."**


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

- [x] Closes: #48899
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **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


| File | Change |
|------|--------|
| `ContextMenu.xaml` | Set `AccessibilityView="Raw"` on ListView and
TextBox; added `NarratorAnnouncer` TextBlock for raising UIA
notifications |
| `ContextMenu.xaml.cs` | Added `AnnounceOpened()`,
`AnnounceSelectedItem()`, and `_isOpening` guard to prevent programmatic
selection changes from triggering UIA events during the flyout
transition |
| `CommandBar.xaml.cs` | Call `AnnounceOpened()` on flyout open |
| `DockControl.xaml.cs` | Same for dock context menu |
| `Resources.resw` | Added `ScreenReader_Announcement_ContextMenuOpened`
localized format string |

## How it works

- ListView and TextBox are permanently `AccessibilityView="Raw"` —
invisible
  to Narrator, preventing all system-driven UIA announcements
- A zero-size `NarratorAnnouncer` TextBlock
(`AccessibilityView="Content"`,
  `LiveSetting="Assertive"`) serves as the sole notification source
- On open: a deferred `RaiseNotificationEvent` fires one consolidated
announcement
- On arrow navigation: `AnnounceSelectedItem()` fires item name and
position


<!-- 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/4660741f-6b91-4a32-9a56-83c64343b67a



- [x] Build succeeds (0 errors)
- [x] 124 ViewModel unit tests pass
- [x] Manual Narrator testing: open, arrow navigate, close, reopen
- [x] Keyboard filtering (type to filter) still works
- [x] Enter to invoke selected command still works
- [x] Escape to close still works
2026-06-29 21:39:12 +02:00
Niels Laute
a46a4437e5 Fix unreadable What's New titles/links when OS and PowerToys themes differ (#48910)
## Summary of the Pull Request

When the OS theme differs from the PowerToys theme (e.g. OS in Light,
PowerToys set to Dark), the **What's New / release-notes (Scoobe)** page
renders heading titles and hyperlinks with brushes pinned to the wrong
theme, making them unreadable. The system caption buttons
(min/max/close) are also not tinted to match the window theme.

The release-notes page uses the CommunityToolkit `MarkdownTextBlock`,
which captures its heading and link brushes from
`Application.Current.Resources` when its theme config is created. Those
resolve against the **OS (application) theme** rather than the window's
selected element theme, so headings/links break whenever the two themes
differ.

## PR Checklist

- [x] Closes: #43970
- [x] Closes: #48832
- [x] **Communication:** Local workaround for a known upstream control
bug
- [ ] **Tests:** Manually validated (UI/theming change)
- [x] **Localization:** No new end-user-facing strings
- [ ] **Dev docs:** N/A

## Detailed Description of the Pull Request / Additional comments

These are deliberately **local workarounds** until the upstream control
resolves brushes against the element theme via
[CommunityToolkit/Labs-Windows#785](https://github.com/CommunityToolkit/Labs-Windows/pull/785).
Comments + `TODO`s in the code point at that PR so the workaround can be
removed once it ships.

**`ScoobeReleaseNotesPage`**
- Pin the `MarkdownTextBlock.RequestedTheme` to the selected app theme
and reassign the `H1`–`H6` heading brushes and the link brush (resolved
for that theme) before the markdown is rendered. The themed brushes are
read from the control's own `Foreground` (`TextFillColorPrimaryBrush`)
and a hidden `LinkBrushProvider` carrier element
(`AccentTextFillColorPrimaryBrush`).
- Re-run the workaround and force a re-render on runtime theme changes
(subscribe to the page's `ActualThemeChanged`), so titles/links stay
readable when the user switches Light/Dark while the window is open.

**`TitleBarHelper` (new) + `ScoobeWindow`**
- Add a small shared
`TitleBarHelper.ApplySystemThemeToCaptionButtons(window, theme)` (port
of the WinUI Gallery helper + PowerToys conventions) and drive the
Scoobe window's caption-button colors from the content's actual theme,
updating on `ActualThemeChanged`. `ScoobeWindow` uses the built-in WinUI
`TitleBar`, which — unlike the custom PowerToys `TitleBar` control used
by the other Settings windows — does not tint the system caption buttons
to the app theme.

## Validation Steps Performed

- OS Light + PowerToys Dark: opened What's New → headings, hyperlinks,
body text and caption buttons all render readable/dark-themed
(previously black/unreadable titles). Confirmed working.
- Switched OS Light → Dark while the Scoobe window was open → markdown
content and caption buttons update live.
- OS Dark: content pane, tables and titles render with the dark theme
(addresses the "light pane in a dark window" report).

<img width="1673" height="929" alt="Screenshot 2026-06-26 130638"
src="https://github.com/user-attachments/assets/1db7fb37-f5ee-485b-863e-fc1ba0d13f6f"
/>

_OS is in Light Mode, while the app settings are set to Dark mode_

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-29 20:33:40 +02:00
Clint Rutkas
3bf682048e Grab and Move: square overlay corners in remote sessions (#48999) 2026-06-29 07:05:19 -07:00
moooyo
28a9bbe8f0 [PowerDisplay] Add configurable mouse wheel increment for slider controls (#49002)
## Summary of the Pull Request

Adds a Settings option that controls how much the PowerDisplay flyout
sliders (brightness, contrast, volume) change per mouse-wheel notch. The
value is chosen from a **preset dropdown** (`1, 2, 5, 10, 15, 20, 25`)
and defaults to **5**, preserving today's behavior. Previously the
per-notch step was hardcoded as
`helpers:SliderExtensions.MouseWheelChange="5"` in four places in the
flyout.

<img width="1282" height="620" alt="image"
src="https://github.com/user-attachments/assets/3b299a47-eb7b-4b53-b3dc-0540fbb25bfc"
/>


## PR Checklist

- [x] Closes: #48805
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass — _added
`MouseWheelIncrementSettingsTests` to the existing
`PowerDisplay.Lib.UnitTests`; passing via `vstest.console.exe`._
- [x] **Localization:** All end-user-facing strings can be localized —
_new `PowerDisplay_MouseWheelIncrement.Header`/`.Description` in
`en-us/Resources.resw`, surfaced via `x:Uid`._
- [ ] **Dev docs:** Added/updated — _N/A: small settings addition, no
behavioral/architecture docs affected._
- [ ] **New binaries:** Added on the required places — _N/A: no new
binaries or projects (the unit test was added to an existing test
project)._
- [ ] [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:** _N/A._

## Detailed Description of the Pull Request / Additional comments

A single `int MouseWheelIncrement` is added to `PowerDisplayProperties`
and persisted to `settings.json` under `mouse_wheel_increment`; it is
edited in the Settings UI and read by the flyout app. The value applies
uniformly to all four flyout sliders.

**Data model** —
`src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs`
- `MouseWheelIncrement` (`int`,
`[JsonPropertyName("mouse_wheel_increment")]`, default `5` set in the
constructor, mirroring `MonitorRefreshDelay`). Old `settings.json`
without the key deserializes to `5` (no migration). The whole type is
already registered with the source-generated JSON contexts, so the new
property serializes on both the Settings and flyout sides with no
context change.

**Settings UI (write side)** — `src/settings-ui/Settings.UI/...`
- `PowerDisplayViewModel`: `MouseWheelIncrement` (get/set via
`SetSettingsProperty`, calls `SignalSettingsUpdated()` so an open flyout
updates live) and `MouseWheelIncrementOptions` = `{ 1, 2, 5, 10, 15, 20,
25 }`.
- `PowerDisplayPage.xaml`: a `ComboBox` `SettingsCard` in the
flyout-settings expander, immediately after "Monitor refresh delay",
matching that card's markup.
- `Strings/en-us/Resources.resw`: `Mouse wheel increment` + description.

**Flyout (read side)** — `src/modules/powerdisplay/PowerDisplay/...`
- `MainViewModel`: `[ObservableProperty] int MouseWheelIncrement`
(default 5), loaded from settings in `LoadUIDisplaySettings()` (runs at
startup and on the settings-updated IPC event, so the all-displays
slider updates live).
- `MonitorViewModel`: a read-only proxy `MouseWheelIncrement =>
_mainViewModel?.MouseWheelIncrement ?? 5` plus
`RefreshMouseWheelIncrement()`, called from `ApplySettingsFromUI`'s
per-monitor loop so the per-monitor sliders update live.
- `MainWindow.xaml`: the four sliders'
`SliderExtensions.MouseWheelChange` now bind to the setting — the
all-displays slider to `ViewModel.MouseWheelIncrement`, the three
per-monitor sliders (brightness/contrast/volume, inside the
`MonitorViewModel` `DataTemplate`) to `MouseWheelIncrement`.

## Validation Steps Performed

**Automated**
- Unit tests
(`PowerDisplay.Lib.UnitTests/MouseWheelIncrementSettingsTests.cs`), run
via `vstest.console.exe` — passing:
  - default value is `5`;
- legacy `settings.json` missing the key deserializes to `5` (no
migration);
  - round-trip preserves a non-default value;
  - serialization emits the `mouse_wheel_increment` snake_case key.
- `PowerToys.Settings` and the PowerDisplay flyout app both compile
clean (the XAML compiler validates the new `x:Bind` bindings).
- Confirmed the property flows through the source-generated JSON
contexts (whole-type `[JsonSerializable(typeof(PowerDisplaySettings))]`)
on both the Settings and flyout sides.

**Pending (manual, on-device — reason this PR is a draft)**
- [ ] Settings: the dropdown shows `5` on a fresh profile; changing it
writes the new `mouse_wheel_increment` value to `settings.json`.
- [ ] Flyout: scrolling each slider (all-displays brightness,
per-monitor brightness/contrast/volume) steps by the selected value,
including live update while the flyout is open.

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 07:56:44 +00:00
Niels Laute
536e768cac Extend MSIX context-menu icon sizing to the standard logo set (#48925)
## Summary

Addresses #48924. Several MSIX context-menu items did not ship the full
standard logo set (e.g. File Locksmith was missing the 44x44 logo), so
Windows fell back to a default icon in some surfaces.

This updates the MSIX assets for **File Locksmith**, **Image Resizer**
and **PowerRename** to provide the full logo set: 44x44 logo,
small/large/150x150 tiles, and store logo.

## PR Checklist
- [x] Closes #48924
- [ ] Tested manually

## Validation
Icon-asset only change; no code modified.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-28 09:33:50 +02:00
Copilot
70ff4013b9 Add Shortcut Guide V2 manifest spec link to copilot-instructions.md (#48967)
Adds a reference to the WinGet Manifest Keyboard Shortcuts schema spec
in `.github/copilot-instructions.md` so AI agents know where to find the
correct field definitions, file naming conventions, and the `+` prefix
rule when creating or editing Shortcut Guide V2 manifest files.

## Summary of the Pull Request

Adds a new `## Shortcut Guide V2 Manifests` section to
`.github/copilot-instructions.md` linking to [`doc/specs/WinGet Manifest
Keyboard Shortcuts
schema.md`](../doc/specs/WinGet%20Manifest%20Keyboard%20Shortcuts%20schema.md).
This ensures agents authoring new manifest files follow the correct
schema and naming scheme (e.g., `<PackageId>.<locale>.yml`, `+` prefix
for apps without a WinGet package).

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

`.github/copilot-instructions.md` gains a dedicated section:

```markdown
## Shortcut Guide V2 Manifests

When creating or editing Shortcut Guide keyboard shortcut manifest files, follow the schema and naming conventions in the spec:

- [WinGet Manifest Keyboard Shortcuts schema](<../doc/specs/WinGet Manifest Keyboard Shortcuts schema.md>) – manifest file format, field definitions, file naming, and the `+` prefix convention for apps without a WinGet package
```

No production code changes.

## Validation Steps Performed

- Verified the relative link resolves to the correct spec file in the
repository.
- Confirmed the section is correctly placed before "Detailed
Documentation".

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-27 21:07:48 +00:00
ABHIJEET KALE
7a04d4c270 [ShortcutGuide] Add DaVinci Resolve keyboard shortcut manifest (#48651) (#48652)
Adds a shortcut manifest for DaVinci Resolve (professional video editing
and color grading application by Blackmagic Design) so it appears in the
Shortcut Guide overlay when the app is focused.

The manifest contains 88 of the most commonly used DaVinci Resolve
keyboard shortcuts organized into 8 categories:

| Section | Shortcuts | Highlights |
|---------|-----------|------------|
| Popular shortcuts | 25 | Page navigation (F5-F8), Playback (JKL,
Space), Edit basics (Cut, Blade, Ripple Delete) |
| Timeline navigation | 13 | Frame/clip/track navigation, zoom, edit
point jumping |
| Edit | 14 | Cut/Copy/Paste/Duplicate, Render in Place, Compound Clip |
| Color | 17 | Node management (Alt+S/P/L), viewer modes (1-5),
Grade/Keyframe |
| Fairlight | 8 | Mute/Solo/Record/Automation, Bounce Mix |
| Fusion | 8 | View switching (1-4), Merge, Keyframe |
| Media | 8 | Import, Smart Bin, Reveal in Explorer, Rename |
| Deliver | 5 | Render Queue, Start Render, Output settings |

The manifest follows the same YAML schema as existing manifests. No code
changes needed -- manifests are auto-discovered at startup.
2026-06-27 19:48:39 +00:00
Michael Jolley
8c434cd6f4 CmdPal: Fix LayoutCycleException in gallery screenshot strip (#48090)
## Summary

Extension Gallery detail pages crash with `LayoutCycleException` /
`AG_E_LAYOUT_CYCLE` when scrolling the screenshot strip horizontally on
extensions that have many screenshots.

## Root Cause

The screenshot `ItemsView` uses a horizontal `StackLayout` and is placed
in a Grid row with `Height="Auto"`, inside a vertical `ScrollViewer`.
The `ItemsView` itself contains an internal `ScrollView` for horizontal
scrolling.

When screenshots load asynchronously (`BitmapImage` decode completes),
this triggers a layout feedback loop:

1. Image decodes → `ItemContainer` re-measures
2. Auto-height Grid row re-measures, offering `ItemsView` new available
height
3. `ItemsView`'s internal `ScrollView` recalculates extents
4. Parent Grid row invalidates → outer `ScrollViewer` re-layouts →
re-measures children → back to step 1

## Fix

Two XAML-only changes to `ExtensionGalleryItemPage.xaml`:

1. **Fixed Grid row height**: Changed the screenshot strip row from
`Height="Auto"` to `Height="232"` (200px image + 16px top/bottom
padding). The parent no longer queries `ItemsView` for desired height,
breaking the cycle.

2. **Fixed item width**: Added `Width="356"` to the screenshot `Border`
template (16:9 ratio: 200 × 16/9 ≈ 356). Provides a stable size before
async image decode, preventing re-measure triggers.

Fixes #47901

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-27 12:32:13 +02:00
ScymicX
d983dbc285 Fix VS Code workspace UNC paths (#48922)
## Summary of the Pull Request

Fixes #47719

VS Code stores recently opened workspaces as URIs. A workspace on a
Windows network share can be stored as
`file://server/share/workspace.code-workspace`.

The VS Code Workspaces plugin interpreted the server part of that URI as
a VS Code remote authority. Because it was not a recognized remote
authority, valid UNC workspaces were discarded.

This change recognizes file URIs with a server authority as local UNC
paths and converts them to the Windows UNC format: `\\server\share\...`.

## PR Checklist

* [x] Closes #47719
* [x] **Communication:** This implementation follows the suggested
direction in the issue discussion.
* [ ] **Tests:** No automated regression test added.
* [x] **Localization:** No user-facing strings were changed.
* [x] **New binaries:** No binaries were added.
* [x] **Documentation updated:** No documentation changes are required.

## Validation Steps Performed

* Built the full PowerToys solution locally in `Release | x64` with 0
errors.
* Started the locally built PowerToys instance.
* Verified that the VS Code Workspaces plugin finds local workspaces.
* Verified that UNC workspaces using both a hostname and an IP address
appear in PowerToys Run.
* Opened a UNC workspace successfully from PowerToys Run.
2026-06-26 22:00:25 +00:00
Niels Laute
fb6843b0f1 Refactor transparent overlay into TransparentWindow + TransientSurface (#48915)
## Summary

Refactors the reusable transparent-overlay infrastructure in
`src/common/Common.UI.Controls/` into a clean separation between a pure
host window and a self-animating acrylic surface.

### What changed

- **`TransparentWindow`** is now animation-agnostic. It raises `Showing`
/ `Hiding` events; `Hiding` exposes a deferral so the HWND stays visible
until the surface's out-animation finishes.
- **`TransientSurface`** (renamed from `TransparentCard`) is a
self-animating "pseudo-window" content control. It owns all chrome —
`ThemeShadow`, always-active desktop acrylic, 1px border, rounded
corners — and its own show/hide slide animations.
- `SlideFrom` (`None`/`Left`/`Top`/`Right`/`Bottom`) selects the slide
edge. `None` is the default and plays **no animation at all** (instant
show/hide).
- `AcrylicKind` (new) is exposed and bound to the backdrop via
`TemplateBinding`, defaulting to **thin acrylic**. Consumers can
override to `Default`/`Base`.
- **`AlwaysActiveDesktopAcrylicBackdrop`** gains a matching `Kind`
dependency property.
- **CmdPal `ToastWindow`** is migrated to the new pattern as the proving
consumer (`Surface.SubscribeTo(this)`).

### Coordination model

A module declares a `<TransientSurface>` as the window's content and
calls `SubscribeTo(window)` once. The window raises `Showing`/`Hiding`;
the surface animates itself in/out and uses the `Hiding` deferral to
keep the window alive until the out-animation completes.

## Testing

- `Common.UI.Controls` builds clean (x64 Debug, exit 0).
- `Microsoft.CmdPal.UI` builds clean (x64 Debug, exit 0).
- ToastWindow keeps its slide-up animation (`SlideFrom="Bottom"`).


https://github.com/user-attachments/assets/a06b0f1a-740a-4fcd-bba8-6f7a64ed261b

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-26 21:25:17 +00:00
Clint Rutkas
6dd1ce5dd1 Dev/crutkas/ripple v2.1 + spelling allow-list follow-up (#48232)
## Summary of the Pull Request

Adds a follow-up metadata fix to the existing Mouse Highlighter ripple
v2.1 work by allowing the term `xhair` in repo spell-check
configuration.

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

- Added `xhair` to `.github/actions/spell-check/expect.txt`.
- This addresses spelling feedback on MouseHighlighter ripple/crosshair
code without changing runtime behavior.
- No functional changes to Mouse Highlighter logic were made in this
follow-up commit.

## Validation Steps Performed

- Verified the only content change is the new `xhair` entry in
spell-check expected words.
- Ran secret scanning on changed file
(`.github/actions/spell-check/expect.txt`) with no findings.
- Ran parallel validation:
  - Code Review: no issues.
  - CodeQL: skipped as trivial metadata-only change.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-26 21:35:27 +02:00
Michael Jolley
9ea30ec523 CmdPal: Fix fallback results showing when disabled in Command Palette (#48777)
Fallback results were showing in Command Palette search results even
when the user had disabled them in settings.

When a fallback command is disabled (e.g., VS Code for Command Palette
with `IsEnabled = false`), it is excluded from
`configuredGlobalFallbackIds` (which only contains enabled + global
fallbacks). However, during search filtering, all fallbacks NOT in
`configuredGlobalFallbackIds` were unconditionally added to
`commonFallbacks`, which gets scored and displayed in results. This
means disabled fallbacks still appeared.

To fix, I added an `IsEnabled` check when building the `commonFallbacks`
list in `MainListPage.cs`. Disabled fallback commands are now properly
excluded from search results.

Fixes #48504

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-26 12:40:53 -05:00
Niels Laute
c777fcc1e4 Fix CmdPal gallery crash when extension has no homepage (#48869)
## Summary

Selecting an extension in the CmdPal Extension Gallery crashed the app
when that extension had **no `homepage`** defined.

## Root cause

In `ExtensionGalleryItemPage.xaml`, the "View repository"
`HyperlinkButton` bound its `NavigateUri` (a `System.Uri`) directly to
the raw string property `ViewModel.Homepage`:

```xml
NavigateUri="{x:Bind ViewModel.Homepage, Mode=OneWay}"
```

`x:Bind` evaluates **all** bindings on an element regardless of
`Visibility`, and to assign a `string` to a `Uri` target it generates a
`new Uri(value)` conversion. When `homepage` is undefined, `Homepage` is
`null`, so the binding executes `new Uri(null)` →
`ArgumentNullException` → the page crashes on load. The
`Visibility="{... HasHomepage}"` collapse did not help because the
`NavigateUri` binding still runs.

Every other `NavigateUri` x:Bind in the codebase (`Link`, `LinkUri`) is
already bound to a `Uri?`, so the homepage hyperlink was the lone
offender.

## Fix

- **`ExtensionGalleryItemViewModel.cs`** — Added a validated `Uri?
HomepageUri` property backed by the existing `_homepageHttpUri` (already
`null` for missing or non-web homepages).
- **`ExtensionGalleryItemPage.xaml`** — Bound `NavigateUri` to
`ViewModel.HomepageUri` instead of the raw string. `null` is valid for
`NavigateUri`, so no conversion occurs. The tooltip still shows the
`Homepage` string.
- **Tests** — Added coverage asserting `HomepageUri` is set for a web
homepage and `null` when missing.

## Verification

- `Microsoft.CmdPal.UI.ViewModels`, `Microsoft.CmdPal.UI` (XAML
compile), and the unit test project all built cleanly (x64/Release, exit
code 0).
- All 24 `ExtensionGalleryItemViewModelTests` pass, including the two
new cases.
- Manually verified in VS that opening an extension without a homepage
no longer crashes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>



https://github.com/user-attachments/assets/b268bafb-6bee-4862-9fbf-7a0e06675e36

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 17:14:37 -05:00
Michael Jolley
28e078897a [CmdPal] Fix memory leak in PerformanceWidgetsPage network band items (#48880)
## Summary

Fixes a memory leak in the Performance Monitor dock extension where
`GetItems()` created **new** `ListItem` instances for `_networkUpItem`
and `_networkDownItem` on every call.

## Problem

When the dock subscribes to `ItemsChanged` and calls `GetItems()` to
refresh, the band page path allocates 2 new `ListItem` objects each time
— the old ones are replaced in the fields but never collected (they
remain referenced by the `DockItemViewModel` wrappers until the next
refresh cycle). Under normal operation this leaks ~2 objects/second
indefinitely.

## Fix

Move `_networkUpItem`/`_networkDownItem` creation into the constructor
(matching the pattern used by CPU, Memory, GPU, and Battery items).
`GetItems()` now returns stable references. The `Updated` event handler
already updates their `.Title` properties, which propagates to the UI
via `PropChanged` → `CommandItemViewModel.Model_PropChanged`.

## Validation

- Build succeeds (`Microsoft.CmdPal.Ext.PerformanceMonitor.csproj`)
- Network up/down band items still receive title updates via the
existing `Updated` handler
- No `RaiseItemsChanged()` needed — `ListItem.Title` setter fires
`PropChanged`, which `DockItemViewModel` already observes

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 21:06:13 +00:00
Michael Jolley
64f1243bdf Skip auto-labeling PRs that already have labels (#48877)
## Summary

The auto-labeler workflow now skips pull requests that already have
labels applied before running the AI classification. This avoids
overwriting or duplicating labels that were manually set by contributors
or maintainers.

## Changes

- Added a check in `labelIssue()` that returns early for PRs with
existing labels, logging which labels are already present.
- Issues continue to be labeled regardless (only PRs get the skip
logic).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 19:22:58 +00:00
Mario Hewardt
e1074bc835 ZoomIt - Update notices (#48843)
<!-- 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
Update notices for ZoomIt dependency

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

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **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-06-25 17:31:51 +02:00
Michael Jolley
2390aacbfc CmdPal: Prevent same-page settings navigation (#48703)
Fixes #48698 by preventing the Command Palette settings frame from
navigating to the same page again, which avoids adding a self-navigation
entry to the back stack.

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-25 14:54:39 +02:00
Clint Rutkas
a864d421fc [Keyboard Manager] Fix stuck modifiers and dropped key-to-text remaps (#48571)
## Summary
Fixes stuck modifier keys and silently-dropped remaps on Keyboard
Manager's **single key → text** path, and adds unit coverage (including
a mockable injection-failure seam).

## What this changes
1. **Insert a dummy key event before releasing held modifiers.**
Releasing a lone Win or Alt key-up otherwise triggers the Start Menu /
menu bar. The dummy key absorbs it so the release is inert. The dummy +
releases are only injected when a modifier is actually held.
2. **Accept `WM_SYSKEYDOWN` as well as `WM_KEYDOWN`.** While Alt is held
the system delivers `WM_SYSKEYDOWN`, so the previous `WM_KEYDOWN`-only
guard silently dropped the remap whenever Alt was down.
3. **Route `Helpers::SendTextInput` through `InputInterface`** instead
of calling Win32 `SendInput` directly. Besides making the path mockable,
this stops the existing unit tests from injecting real keystrokes into
the OS during a test run. Text is still flushed per character to
preserve the existing batching workaround.
4. **Never re-press released modifiers.** Once a modifier key-up is
injected, `GetAsyncKeyState` reports it as up, so re-pressing risks
leaving it stuck if the user let go during injection. Leaving it
released is always safe.

## Testing
- New `MockedInput` failure seam (`SetSendVirtualInputShouldFail`).
- `RemappedKey_ShouldPassOriginalKeyThrough_WhenInjectionFails` —
verifies the original key is passed through when injection fails (the
core stuck-key behavior, previously untestable because the mock always
succeeded).
-
`HandleSingleKeyToTextRemapEvent_ShouldFireAndReleaseAlt_WhenAltIsHeld`
— covers fix #2 by asserting the remap still fires (and releases the
held Alt) when the key arrives as `WM_SYSKEYDOWN`.
- Full Keyboard Manager engine suite: **98/98 passing**, Release x64,
against current `main`.

This is one of a small set of related "stuck key" hardening fixes; each
stands alone.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 20:10:35 -07:00
Mike Griese
3331bdf02a CmdPal: add support for compact mode (#48801)
This is a bear of a PR. Watch out.
This PR adds support for compact mode to the command palette. In compact
mode, the results of the command palette window are collapsed by
default. And the only thing that is visible is the search bar. When the
user types, the window is expanded only just enough to show the
available results[^1]



https://github.com/user-attachments/assets/fd11bbc9-1173-426f-8f44-b513baf2ac5f



This is made possible by a fairly annoyingly substantial refactoring to
how our windowing is done. Animating the size or bounds of an HWND on
Windows is not fun. With pretty much any XAML application, you're going
to get at least one frame of blackness when you resize your window. The
trick then is to create an experience where it looks like your
application is resizing, but the HWND never actually resizes.

So the main bulk of this PR is actually just refactoring our window
handling. Our `MainWindow` class now becomes a dummy holder of _some
content_, and most of the main content is moved into the
`CmdPalMainControl` class. `MainWindow`'s job then is to handle a
transparent window that hosts some XAML content inside of it, and
pretend that content is the real bounds of the window. We need to fake
our NCHITTEST results, so that the edges of the XAML content act like
they're the edge of the actual HWND. We need to hide our actual window
frame and shadow, but then also re-create them in XAML around our
content.
Previously we've done work like this using a single full screen
transparent window with XAML content inside of it. However that has the
downside of not allowing the XAML content to be movable across different
monitors. By faking out the NCHITTEST results, we allow users to resize
and move the window using the normal user32 move size loop.
Our HWND is also cropped (with SetWindowRgn to the bounds of the shadow
around our XAML content. We need to include the shadow in the hit
testable region, because if we don't, then the shadow will be visibly
cropped on the edges.

In compact mode, instead of centering our window in the middle of the
monitor, the user can set a relative height where the search box opens
on that monitor. This defaults to about 60% up from the bottom of the
monitor, so that there's room for the results to expand downwards and
feel centered within the screen. This position is a setting, so users
can customize it to whatever they like.

I've also added a developer only debug build only internal setting,
which allows you to see the actual frame of our HWND. This makes it
easier to visually debug where the bounds of the window are and
understand a little bit more about the layout of our application. This
setting and functionality is disabled in release builds.

<img width="1334" height="1164" alt="cmdpal-compact-diagram"
src="https://github.com/user-attachments/assets/cb1c273d-37cc-4cb7-8680-e1878aa20c9c"
/>

Closes #38423


[^1]: with some caveats: pages with details expand fully always.
2026-06-24 22:35:37 +00:00
Michael Jolley
9ee0c7259b CmdPal: Dock Auto-hide (#48565)
This pull request introduces a new "Auto-hide" feature for the dock,
allowing users to collapse the dock until they hover over its screen
edge. The changes include updates to the settings model, UI,
localization resources, and automated tests to support and verify this
new functionality.

**Show me:**


https://github.com/user-attachments/assets/689625e8-9050-4a54-9c4b-9e303a3da63a

**Conflicted?**

"What if I have Taskbar and Dock on the same side and both with
auto-hide turned on?"

<img width="1437" height="264" alt="Screenshot 2026-06-14 144814"
src="https://github.com/user-attachments/assets/bd037a11-0653-4b9a-bd21-625aca03b901"
/>


Closes #46239

---------

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 16:39:58 -05:00
Boliang Zhang
7e877558b9 Add powertoys-module-verification agent skill (#48717)
## Summary of the Pull Request

Adds a new **GitHub Copilot Agent Skill**,
`powertoys-module-verification`, under `.github/skills/`. It packages
the workflow, drive techniques, helper scripts, and per-module reference
data an AI agent uses to verify a single PowerToys module's
release-checklist items end-to-end (each checkbox → a structured PASS /
FAIL / BLOCKED verdict with evidence).

This is **docs/automation tooling only** — no product code, binaries, or
end-user strings are touched.

## PR Checklist

- [ ] Closes: #xxx
- [x] **Communication:** Internal tooling for release sign-off; happy to
adjust scope/placement based on review.
- [x] **Tests:** N/A — documentation/skill bundle only (no product code
paths). The 12 bundled `.ps1` helpers are agent utilities, not part of
the product build/test.
- [x] **Localization:** N/A — no end-user-facing strings.
- [x] **New binaries:** N/A — no binaries added.
- [x] **Dev docs:** This PR *is* developer-facing documentation/tooling.

## Detailed Description of the Pull Request / Additional comments

Layout follows the repo's Agent Skill guidelines
(`.github/instructions/agent-skills.instructions.md`) and matches the
existing skills under `.github/skills/`:

```
.github/skills/powertoys-module-verification/
├── SKILL.md            # single entry doc (name/description/license frontmatter)
├── LICENSE.txt         # Apache 2.0 (matches existing skills)
├── scripts/            # 12 PowerShell helper utilities used by the agent
└── references/
    ├── winapp-ui-testing.md         # UIA-mechanics prerequisite doc
    ├── pre-flight.md / reporting-format.md / environment-setup.md / explorer-context-menu-flow.md
    ├── modules/                     # per-module verification profiles
    └── release-checklist/           # per-module checklists + index
```

Notes for reviewers:
- **`references/winapp-ui-testing.md`** is adapted from the
`winui-ui-testing` skill in
[microsoft/win-dev-skills](https://github.com/microsoft/win-dev-skills)
(MIT, © Microsoft Corporation and Contributors), with PowerToys-specific
edits. Provenance is recorded in the file header. Its skill frontmatter
was intentionally stripped so it is treated as a reference doc, not a
separately-discovered skill.
- **Checklist scope:** only modules already verified end-to-end (with a
sign-off report) are included for now — Environment Variables, File
Locksmith, Image Resizer, New+, Peek, PowerRename. Remaining modules'
checklists will be added as each is verified.
- No existing files are modified; this is purely additive under
`.github/skills/`.

## Validation Steps Performed

- Validated the bundle against the Agent Skill checklist in
`.github/instructions/agent-skills.instructions.md`: valid `name` (≤64
chars) + `description` + `license` frontmatter; `SKILL.md` body under
the 500-line guidance; single `SKILL.md`; `scripts/` + `references/`
resource buckets; all resource references use relative paths.
- Verified internal cross-references resolve after the migration (no
stale `helpers/`, `Winapp-SKILL.md`, or absolute-path tokens;
`src/modules/...` source citations left intact).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 13:45:26 -07:00
Mike Griese
386a16ff94 Revert "[CmdPal][Dock] Fix performance meter showing '???' after restart" (#48835)
Reverts microsoft/PowerToys#48682

I'm quite sure that OP did not build or test these changes, and they
should not have merged.
2026-06-24 14:21:10 -05:00
Michael Jolley
ada75d040a CmdPal: Hide uninstall option for system apps (#48804)
Command Palette currently shows the "Uninstall" context menu option for
system apps like Windows Settings, Registry Editor, and Task Manager.
Users can accidentally trigger uninstall on packages that should not be
removable. This PR hides the uninstall option for system apps.

It's probably not a perfect solution, but seemed to catch all I tried to
test.

Fixes #46826

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 13:52:12 -05:00
Michael Jolley
086bd06eaf CmdPal: Accurate GPU usage in Dock (#48710)
Closes #48677.

## Root cause
`GPUStats.GetData()` summed the cooked `Utilization Percentage` value of
every `engtype=3D` GPU-Engine performance-counter instance for a given
physical adapter. Each instance is a `(process, engine)` pair. Summing
across processes for a single engine is correct (gives total engine
utilization), but summing across **multiple 3D engines** on the same
adapter compounds them and produces values >100% (the 104%/118%
screenshots in the issue). The displayed value `sum / 100` therefore
exceeded 1.0, and the chart fed the raw inflated `sum` directly.

## Fix
Mirroring the bounded-counter approach used for CPU in #46381:

1. Bucket cooked values by `(physId, engineId)` rather than just
`physId`.
2. Reduce per-adapter to the **maximum** engine utilization (what Task
Manager surfaces as the overall GPU number).
3. Clamp the per-adapter value to `[0, 100]` before storing it as a 0–1
fraction in `Data.Usage` and pushing to the chart.
4. Defensive checks: skip `NaN` / `Infinity` / negative cooked values,
and skip instances whose engine id can't be parsed.

Co-authored-by: root <root@io.bbq>
2026-06-24 11:32:52 -05:00
Michael Jolley
205ae601ce CmdPal: Fix bookmark dock bands disappearing on restart (#48092)
## Summary

Bookmarks pinned to the CmdPal dock disappear after a restart.

## Root Cause

`BookmarksCommandProvider` did not override `GetCommandItem(string id)`.
When `InitializeCommands` in `CommandProviderWrapper` processes pinned
dock band settings, the fallback path calls
`provider.GetCommandItem(commandId)`, which hit the base-class no-op
returning `null`. Any bookmark band that `GetDockBands()` failed to
return (due to a race, transient file error, or exception) was
irreversibly lost from the dock.

## Fix

Added `GetCommandItem` override to `BookmarksCommandProvider`. Given a
`"Bookmarks.Launch.{guid}"` ID, it:

1. Parses the GUID suffix
2. Looks up the `BookmarkData` in `_bookmarksManager` (under lock)
3. Constructs a `BookmarkListItem` (with `asBand: true`) and wraps it in
a `WrappedDockItem`

Uses `BookmarkTitle` (maps to `_bookmark.Name`, set synchronously in the
constructor) rather than `Title` (set asynchronously after
classification completes), matching the pattern used by
`AllAppsCommandProvider`, `SystemCommandExtensionProvider`, and others.

Fixes #47576

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 11:32:08 -05:00
Michael Jolley
80f2b9b07d CmdPal: Fix 1px gap between dock window and screen edge (#48091) 2026-06-24 08:41:37 -05:00
Copilot
5f1b496bf2 Shortcut Guide: Rename Win+Q to "Open Click to Do" for Copilot+ PCs (#48439)
Win+Q and Win+S were both labeled "Open search" in Shortcut Guide. On
Copilot+ PCs, Win+Q launches Click to Do — not Search — so the duplicate
label was misleading.

## Changes

- `+WindowsNT.Shell.en-US.yml`: Renamed Win+Q entry from `Open search` →
`Open Click to Do` with description `On Copilot+ PCs`; Win+S retains
`Open search`

## PR Checklist

- [ ] Closes: #xxx
- [ ] **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

The Windows key shortcuts manifest had two entries with identical names
(`Open search`) mapped to Win+Q and Win+S respectively. On Copilot+ PCs,
Win+Q opens Click to Do, not Search. The Win+Q entry is corrected to
`Open Click to Do` with a `On Copilot+ PCs` description to scope its
applicability.

## Validation Steps Performed

- Visually reviewed the YAML diff to confirm only the Win+Q entry name
and description changed; Win+S entry is unchanged.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-24 10:44:07 +00:00
Ingo Kodba
a2218b3c3b [KeyboardManager] Fix sticky Ctrl caused by stale AltGr flag (#46672)
## Summary

Fixes #46693

- The `static bool isAltRightKeyInvoked` flag in
`HandleShortcutRemapEvent` is set when AltGr (RAlt+LCtrl) is pressed,
but only reset inside Case 1's else branch — which requires a shortcut
to be actively invoked (`isShortcutInvoked == true`)
- If the user presses and releases AltGr when **no shortcut is active**,
the flag stays `true` permanently across all future calls
- Once stale, the flag blocks modifier key restoration and state resets
at 13 locations throughout the function (lines 646, 670, 791, 800, 866,
880, 889, 907, 927, 978, 996), and causes LCtrl key-up events to `break`
out of the handler loop (line 620-622) instead of being properly
processed
- This makes LCtrl permanently stuck for any shortcut remap that uses
LCtrl as a modifier

## Fix

Reset `isAltRightKeyInvoked` when `VK_RMENU` (Right Alt) is released,
regardless of whether a shortcut is currently invoked. This is added as
an `else if` right after the existing flag-set condition.

## Test plan

- [ ] Configure a shortcut remap using LCtrl (e.g. LCtrl+Y → Backspace)
- [ ] Press and release AltGr without triggering any shortcut
- [ ] Use the LCtrl shortcut — verify Ctrl does not become stuck
- [ ] Verify AltGr-based shortcuts still work correctly
- [ ] Verify normal AltGr character input (e.g. AltGr+Q for @ on German
layout) is unaffected

---------

Co-authored-by: fluffyspace <fluffyspace@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-24 10:42:54 +00:00
Clint Rutkas
47335149f3 [Settings] Extract HandleNavigationFailure and test all null permutations (#48410)
## Summary

Follow-up to #48409. That PR rewrote
`ShellViewModel.Frame_NavigationFailed` to set `e.Handled = true` and
log instead of re-throwing, but the unit tests it added only covered the
`GetPageDisplayName` formatting helper. The actual contract that matters
- "this handler must never throw, regardless of which fields on
`NavigationFailedEventArgs` happen to be null" - was not directly
testable because `NavigationFailedEventArgs` is a sealed WinRT type that
cannot be constructed from MSTest.

## Change

Tiny refactor: split the failure-handling logic out of
`Frame_NavigationFailed` into a pure static
`HandleNavigationFailure(Type sourcePageType, Exception exception)`. The
WinUI-shaped handler now just sets `Handled = true` and delegates.

This makes the "must not throw" invariant testable in isolation - no
WinUI Frame, no Microsoft.UI.Xaml.Navigation types needed.

## Tests

Added four new cases under `ShellViewModelTests`, exercising all four
`(SourcePageType, Exception)` null permutations:

| `sourcePageType` | `exception` |
| - | - |
| null | null |
| typeof(...) | null |
| null | new Exception(...) |
| typeof(...) | new Exception(...) |

Each test simply calls the helper and relies on MSTest's default "if it
throws, the test fails" behavior. Any future change that re-introduces
an unguarded dereference of `e.SourcePageType` or `e.Exception` will
turn the corresponding test red.

## Validation

All six tests pass:

```
Passed GetPageDisplayName_ReturnsFullName_ForKnownType
Passed GetPageDisplayName_ReturnsPlaceholder_ForNullType
Passed HandleNavigationFailure_DoesNotThrow_ForNullInputs
Passed HandleNavigationFailure_DoesNotThrow_ForNullException
Passed HandleNavigationFailure_DoesNotThrow_ForNullPageType
Passed HandleNavigationFailure_DoesNotThrow_ForBothInputsPresent
Total tests: 6, Passed: 6
```

`PowerToys.Settings.csproj` and `Settings.UI.UnitTests.csproj` both
build clean (Release|x64).

## Note

This PR is stacked on top of #48409 (same branch lineage). If #48409 is
merged first this PR will rebase cleanly to a small diff on
`ShellViewModel.cs` + the additional test cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-24 10:02:08 +02:00
dependabot[bot]
5c63486dcb build(deps): bump actions/checkout from 6 to 7 (#48743)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to
7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> made
their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@​yaananth</code></a>
made their first contribution in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
<h2>v6.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID
is set by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2355">actions/checkout#2355</a></li>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6.0.1...v6.0.2">https://github.com/actions/checkout/compare/v6.0.1...v6.0.2</a></p>
<h2>v6.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update all references from v5 and v4 to v6 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2314">actions/checkout#2314</a></li>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
<li>Clarify v6 README by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2328">actions/checkout#2328</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v6...v6.0.1">https://github.com/actions/checkout/compare/v6...v6.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run
by <a href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the
minor-actions-dependencies group across 1 directory by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@​actions/core</code> and
<code>@​actions/tool-cache</code> and Remove uuid by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a
href="https://github.com/aiqiaoy"><code>@​aiqiaoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a
href="https://github.com/yaananth"><code>@​yaananth</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements
by <a href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a
href="https://github.com/ericsciple"><code>@​ericsciple</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9c091bb21b"><code>9c091bb</code></a>
update error wording (<a
href="https://redirect.github.com/actions/checkout/issues/2467">#2467</a>)</li>
<li><a
href="1044a6dea9"><code>1044a6d</code></a>
getting ready for checkout v7 release (<a
href="https://redirect.github.com/actions/checkout/issues/2464">#2464</a>)</li>
<li><a
href="f0282184c7"><code>f028218</code></a>
Bump the minor-npm-dependencies group across 1 directory with 3 updates
(<a
href="https://redirect.github.com/actions/checkout/issues/2462">#2462</a>)</li>
<li><a
href="d914b262ff"><code>d914b26</code></a>
upgrade module to esm and update dependencies (<a
href="https://redirect.github.com/actions/checkout/issues/2463">#2463</a>)</li>
<li><a
href="537c7ef99c"><code>537c7ef</code></a>
Bump <code>@​actions/core</code> and <code>@​actions/tool-cache</code>
and Remove uuid (<a
href="https://redirect.github.com/actions/checkout/issues/2459">#2459</a>)</li>
<li><a
href="130a169078"><code>130a169</code></a>
Bump js-yaml from 4.1.0 to 4.2.0 (<a
href="https://redirect.github.com/actions/checkout/issues/2461">#2461</a>)</li>
<li><a
href="7d09575332"><code>7d09575</code></a>
Bump flatted from 3.3.1 to 3.4.2 (<a
href="https://redirect.github.com/actions/checkout/issues/2460">#2460</a>)</li>
<li><a
href="0f9f3aa320"><code>0f9f3aa</code></a>
Bump actions/publish-immutable-action (<a
href="https://redirect.github.com/actions/checkout/issues/2458">#2458</a>)</li>
<li><a
href="f9e715a95f"><code>f9e715a</code></a>
block checking out fork pr for pull_request_target and workflow_run (<a
href="https://redirect.github.com/actions/checkout/issues/2454">#2454</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v6...v7">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-24 09:51:55 +02:00
Gordon Lam
bf6dbd8865 feat(Advanced Paste): Add setting to show/hide AI paste section (#45242)
## Summary of the Pull Request

Adds a new "Show AI paste section" setting to Advanced Paste that allows
users to hide the AI paste input box from the Advanced Paste window.
This addresses user requests for a cleaner UI when the AI paste feature
is not needed or desired.

## PR Checklist

- [x] Closes: #32967
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **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

This PR implements a new boolean setting `ShowAIPaste` that controls the
visibility of the AI paste input box (PromptBox) in the Advanced Paste
window.

### Changes Made

**Settings UI Library:**
- `src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs` -
Added `ShowAIPaste` property with JSON serialization

**Settings UI:**
- `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs` -
Added view model property with change notification
-
`src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml`
- Added checkbox setting in the UI Behavior section
- `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw` - Added
localized strings for header and description

**Advanced Paste Module:**
- `src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs` -
Added interface property
- `src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs` -
Implemented setting loading from properties
-
`src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs`
- Added `ShowAIPasteSection` property combining user setting with GPO
policy
-
`src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml`
- Bound PromptBox visibility to new property

**Tests:**
-
`src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs`
- Added mock property
-
`src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json`
- Updated test settings file

### Behavior
- Default value is `true` (AI paste section visible) to maintain
backward compatibility
- The setting respects GPO policy - if AI is disabled by GPO, the
section will be hidden regardless of user preference
- Setting is persisted in the Advanced Paste settings JSON file

## Validation Steps Performed

- [x] Verified setting appears in Settings UI under Advanced Paste →
UI Behavior
- [x] Verified toggling the setting hides/shows the AI paste input box
in the Advanced Paste window
- [x] Verified setting persists after closing and reopening Settings
- [x] Verified default value is `true` (visible) for new installations
- [x] Verified existing unit tests pass with updated mock

Fixes #32967

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-06-24 15:43:23 +08:00
Deryl Fabiensyah
3c942ae356 Quick Accent: Add ¡ and ¿ to ! and ? quick accent menu (#20618) (#48599)
<!-- 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 addresses issue #20618 by adding the inverted exclamation mark
(**¡**) to the **!** key (`VK_1`) and the inverted question mark (**¿**)
to the **?** key (`VK_SLASH_`) for both Spanish (`SP`) and Catalan
(`CA`) character mappings in Quick Accent.

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

- [x] Closes: #20618
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **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
- [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

Adds the inverted punctuation marks `¡` (inverted exclamation mark) and
`¿` (inverted question mark) to the character mappings of Spanish (`SP`)
and Catalan (`CA`) languages in `CharacterMappings.cs`.
- `LetterKey.VK_1` (`!`) now maps to `¡`
- `LetterKey.VK_SLASH_` (`?`) now maps to `¿`

This enables quick accent popups when pressing these respective keys,
aligning with the expected behavior for these languages.

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

1. Verified character mapping changes build successfully.
2. Verified that the PowerToys CI pipeline checks completed
successfully.
2026-06-24 12:07:10 +08:00
Mario Hewardt
be4c3a1afa ZoomIt: add WebP/JPG screenshot encoder (#48818)
<!-- 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 webp and jpg support for screenshots.

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

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **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-06-23 15:48:34 -07:00
Clint Rutkas
968a7ac4b6 [Peek] Stop fail-fast in AppWindow.Closing path; reset cached preview-handler factories on release (#48564)
## Summary

Harden Peek's `AppWindow.Closing` path so a stale cached preview-handler
factory can't fail-fast the Peek process. Also clean up the matching
path in RegistryPreview.

## Background

Spotted while reading through Peek's `MainWindow` teardown sequence and
the `ShellPreviewHandlerPreviewer` cache for an unrelated review of how
Peek manages out-of-process preview-handler lifetimes.

The Peek `MainWindow` subscribes to `AppWindow.Closing`. The handler
doesn't actually close the window — it sets `args.Cancel = true` and
calls `Uninitialize()`, which in turn calls
`ShellPreviewHandlerPreviewer.ReleaseHandlerFactories()`.

`ReleaseHandlerFactories()` looked like this:

```csharp
public static void ReleaseHandlerFactories()
{
    foreach (var factory in HandlerFactories.Values)
    {
        try { Marshal.FinalReleaseComObject(factory); } catch { }
    }
}
```

Two problems:

1. The static `HandlerFactories` dictionary is never cleared. After
`FinalReleaseComObject`, the entries still point at separated RCWs. A
subsequent activation that races with this cleanup (or a second close in
the same process) can pick up the dead RCW from the cache.
2. The cached factory had `LockServer(true)` called on it when it was
first cached, but the matching `LockServer(false)` was never paired.

Any managed exception that escapes a WinRT event callback is projected
back to CFlat as a failed HRESULT and the CsWinRT dispatcher fail-fasts
the process. So a single `InvalidComObjectException` (HRESULT
0x80131527) thrown out of `Uninitialize()` is enough to terminate Peek.

## Changes

* **`ShellPreviewHandlerPreviewer.ReleaseHandlerFactories`** — snapshot
then clear the dictionary up front so that a subsequent call (or a
concurrent `LoadPreviewAsync`) can't pick up a stale RCW. Call
`LockServer(false)` before `FinalReleaseComObject` to mirror the
cache-time `LockServer(true)`. Both COM calls remain individually
wrapped because the RCW may already be unreachable during process
teardown.
* **`Peek.UI/MainWindow.xaml.cs` — `AppWindow_Closing`** — wrap the body
in try/catch + `Logger.LogError`. Any future exception in
`Uninitialize()` (or its callees) will now log instead of fail-fasting
the process.
* **`RegistryPreview/MainWindow.Events.cs` — `AppWindow_Closing`** —
same defensive try/catch, plus null-guard `jsonWindowPlacement` before
`SetNamedValue`. The placement dictionary can legitimately be null on
first run or after a corrupt placement file; previously that would NRE →
fail-fast.

## Risk

Low. The `ReleaseHandlerFactories` change matches the documented
`LockServer`/`FinalReleaseComObject` pairing and only widens the
lifetime window of the cache by `Clear()`-ing earlier; nothing in Peek
calls this method outside of teardown. The two try/catch wrappers
strictly add defense — the success path is unchanged.

## Validation

Spot-built locally; this repo's `dotnet restore` runtime-pack issue
(unrelated to this PR — same NU1102 pattern that's affecting other open
PRs) prevents a full `Build.cmd` here. The C++ side of Peek is
untouched.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---

ADO:
https://microsoft.visualstudio.com/DefaultCollection/OS/_workitems/edit/58765809/

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Boliang Zhang <122517415+LegendaryBlair@users.noreply.github.com>
2026-06-23 21:32:02 +08:00
Clint Rutkas
dd26d86580 [FancyZones] Fix stuck drag state and swallowed keys when a window is destroyed mid-drag (#48569)
## Summary
Fixes a class of "stuck drag" bugs in FancyZones where closing or
destroying a window **while it is being dragged** leaves FancyZones in a
half-dragging state — zone overlays stay on screen and subsequent
keystrokes (notably number keys) are swallowed or misrouted.

## What this changes
- **Subscribe to and dispatch `EVENT_OBJECT_DESTROY`.** `FancyZonesApp`
never subscribed to the destroy event, and the consumer's
`WM_PRIV_WINDOWDESTROYED` branch could therefore never fire. The event
is now registered and routed through `HandleWinHookEvent`.
- **Abort the drag (without snapping) when the dragged window is
destroyed.** On `WM_PRIV_WINDOWDESTROYED`, if the destroyed HWND is the
one being dragged, call the new `WindowMouseSnap::Abort()` (tears down
overlays/highlights/transparency) instead of `MoveSizeEnd()`, which
would try to snap the now-dead HWND and corrupt zone state. Dragging
state is then disabled.
- **Always clear dragging state in `MoveSizeEnd()`**, even when the
snapper was already null, so the state can't get stranded.
- **Require Win+Ctrl+Alt to switch layouts while dragging.** Previously
any digit switched layouts while `dragging` was true; if drag state was
stuck this "stole" number keys from the focused app. This is the
root-cause fix for the number-key-stealing symptom.
- **Only swallow the bare Shift key during a drag**, not `Shift+<other>`
combos, so real keystrokes are no longer eaten by an in-progress drag.

## Testing
- Builds Release x64 (FancyZones) clean against current `main`.
- Manually verified drag → close window mid-drag no longer leaves
overlays up or steals number keys. (FancyZones has no unit-test harness
for this path.)

This is one of a small set of related "stuck key / stuck state"
hardening fixes; each stands alone.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Muyuan Li <116717757+MuyuanMS@users.noreply.github.com>
2026-06-23 15:09:26 +02:00
Clint Rutkas
4771f15b6c [Runner] Harden centralized keyboard hook against stuck/ghost keys (#48570)
## Summary
Hardens the runner's centralized keyboard hook against stuck and "ghost"
key activations — cases where a hotkey action fires after the key was
already released, or a pending timer fires after the hook was torn down.

## What this changes
- **`vkCodePressed` is now `std::atomic<DWORD>`.** It is read/written
from the low-level hook callback and from the timer/teardown paths; the
plain `DWORD` was a data race.
- **Revalidate the key is still physically held before firing a held-key
timer.** The pressed-key timer callback now checks
`GetAsyncKeyState(virtualKey) & 0x8000` before invoking the action,
preventing ghost activations after the user has let go.
- **Tag the injected dummy `0xFF` key-up** with
`CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG` so the hook does not
reprocess its own synthetic event.
- **`Stop()` kills all pending pressed-key timers and resets
`vkCodePressed` before unhooking**, so a timer can't fire a callback
into a half-removed hook.

## Testing
- Builds Release x64 (runner / `PowerToys.exe`) clean against current
`main`.

This is one of a small set of related "stuck key" hardening fixes; each
stands alone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 13:54:19 +02:00
Copilot
ed93fb585a Fix ZoomIt module description to eliminate Chinese translation repetition bug (#47370)
The ZoomIt module description contained `"snip screenshots"` — both
words map to the same Chinese term (截图), causing the translation system
to emit the garbled `"截图截图截图"` in the Chinese UI.

## Changes

- **`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`**:
Changed `"to snip screenshots to the clipboard or to a file"` → `"to
copy screenshots to the clipboard or to a file"` in both
`ZoomIt.ModuleDescription` and `Oobe_ZoomIt.Description`
- `copy` has an unambiguous, distinct Chinese translation (`复制`) with no
overlap with `截图`
- Consistent with the existing `ZoomIt_SnipGroup.Description` which
already uses `"Copy a selected area of the screen to the clipboard or to
a file"`

## PR Checklist

- [ ] Closes: #xxx
- [ ] **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
- [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

## Detailed Description of the Pull Request / Additional comments

Before (zh-CN rendered):
> 还可以使用 Zoomlt 将屏幕**截图截图截图**剪贴板或文件中

After (expected zh-CN):
> 还可以使用 ZoomIt 将截图**复制到**剪贴板或文件中

Root cause: `snip` (Windows snipping action) and `screenshots` are both
translated to `截图` by the localization pipeline, producing triple
repetition. Replacing `snip` with `copy` — already used in the adjacent
Snip group description — resolves the collision without changing the
functional meaning.

## Validation Steps Performed

- Verified both string keys (`ZoomIt.ModuleDescription`,
`Oobe_ZoomIt.Description`) updated in `en-us/Resources.resw`
- Confirmed no other occurrences of `"snip screenshots"` remain in the
file

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: MuyuanMS <116717757+MuyuanMS@users.noreply.github.com>
2026-06-23 10:00:27 +00:00
Niels Laute
c3bec3935a Docs cleanup (#48813)
## Summary

Removes outdated/unused documentation and asset files:

- **`doc/unofficialInstallMethods.md`** — removed.
- **`doc/planning/`** — removed entire folder (`awake.md`,
`FancyZonesBacklog.md`, `PowerToysBacklog.md`,
`ShortcutGuideBacklog.md`).
- **`doc/images/icons/Video Conference Mute.png`** — removed (unused
module icon, not referenced anywhere in the repo).

Verified there are no remaining references to any of the removed files
across tracked sources (checked literal and `%20`-encoded paths).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 09:19:27 +00:00
Gordon Lam
334d1c8054 Validate cached installer filename from UpdateState.json in the updater (#48741)
## Summary of the Pull Request

`PowerToys.Update` reads `downloadedInstallerFilename` from the
persisted `UpdateState.json` and combines it with the `Updates` folder
to locate the installer to run. If that cached state is stale,
corrupted, or otherwise unexpected, the stored value could contain path
separators or an absolute path, which would make the updater look for
the installer outside the `Updates` folder.

This PR validates that the cached value is a plain filename and that the
resolved path stays inside the `Updates` folder before using it;
otherwise the update is treated as unavailable. The normal update flow
(a bare asset filename produced by the download step) is unaffected.

## PR Checklist

- [ ] **Communication:** I've discussed this with core contributors
already.
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** No end-user-facing strings added
- [x] **Dev docs:** N/A
- [x] **New binaries:** None

## Detailed Description of the Pull Request / Additional comments

- The filename check is factored into an inline helper
`updating::IsSafeDownloadedInstallerFilename` in
`src/common/updating/updateLifecycle.h` (next to the other inline update
helpers) so it can be unit-tested without a project reference.
- `ObtainInstaller` in `src/Update/PowerToys.Update.cpp` now calls that
helper and additionally confirms, via `weakly_canonical`, that the
resolved installer path's parent is the `Updates` directory.
- No behavior change for normal installer filenames, so the regular
update flow does not regress.

## Validation Steps Performed

- Added `IsSafeDownloadedInstallerFilenameTests` to `Updating.UnitTests`
covering normal filenames, empty values, parent-directory components,
nested path components, and absolute/drive-relative/UNC paths.
- Built `Updating.UnitTests` (Debug x64): 0 warnings, 0 errors.
- Ran the full `Updating.UnitTests` suite: 36/36 passed (30 existing + 6
new).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 15:01:25 +08:00
Niels Laute
5c16c97db5 [Settings] Fix FancyZones page scroll bounce at Excluded apps (#47937)
## Summary

Fixes #25353 — the FancyZones Settings page bounces / jumps when the
user scrolls down to the bottom (the **Excluded apps** expander).
Several closed dupes report the same behavior: #31500, #34259, #30164,
#45173, #32099, #32361.

## Root cause

The Excluded-apps `TextBox` (lines 283-305 of `FancyZonesPage.xaml`)
forced its own inner `ScrollViewer` to be permanently enabled with a
visible scrollbar, nested inside the page-level `ScrollViewer` provided
by `SettingsPageControl`:

```xml
ScrollViewer.IsVerticalRailEnabled="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Enabled"
```

The expander is `IsExpanded="True"` and sits at the very bottom of the
page, so when you wheel-scroll down the page the pointer ends up over
the TextBox. The TextBox''s inner scroller intercepts the wheel events;
when it has nothing left to scroll the event bubbles back to the outer
page scroller, which tries to over-scroll past content end. The result
is the bounce/jump visible in the videos on the issue.

The unbounded `MinHeight="160"` (with no `MaxHeight`) made it worse,
because the box can grow as users type, triggering layout re-measures
during scrolling.

## Fix

Minimal change to the same TextBox:

- Drop `ScrollViewer.IsVerticalRailEnabled` and
`ScrollViewer.VerticalScrollMode` — `AcceptsReturn="True"` already wires
up the inner scroller correctly.
- Change `ScrollViewer.VerticalScrollBarVisibility` from `Visible` to
`Auto`, so the inner scroller only competes with the outer one when
there is actual overflow to scroll.
- Add `MaxHeight="320"` so the box cannot keep growing as the user
types.

## Validation

- Builds clean (`tools\build\build.cmd` from
`src\settings-ui\Settings.UI`, arm64 Debug, exit 0, 0 errors).
- The same nested-scroller pattern exists on 9 other Settings pages
(AlwaysOnTop, GrabAndMove, MouseUtils, MouseWithoutBorders, PowerAccent,
PowerLauncher, ShortcutGuide). Keeping this PR atomic to the page called
out in #25353; happy to follow up with a sibling PR for those if
reviewers want them in the same pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 14:35:13 +08:00
Boliang Zhang
a255ece641 [File Locksmith] Fix crash (0xc000027b) when a listed process's image file no longer exists (#48719)
## Summary of the Pull Request

`PidToIconConverter.Convert` called `Icon.ExtractAssociatedIcon(path)`
without a guard. When a running process's image path is non-empty but
the file no longer exists on disk, the call throws
`FileNotFoundException`. The converter runs per-row while the process
`ListView` virtualizes, so the exception reached
`App_UnhandledException` (which only logs and never sets `e.Handled =
true`) and WinUI 3 fast-failed the whole app (`0xc000027b`).

This is routinely triggered by self-updating software that deletes its
old versioned directory while the old process keeps running — e.g.
Windows Defender (`SenseAPZ`, `MsMpEng`, `MpDefenderCoreService` under
versioned `...\Platform\<ver>\` / `...\DataCollection\<ver>\` paths).
Right-clicking a drive root enumerates every process and reliably
includes such a stale-path process, so the crash is easy to hit in
normal use.

The fix wraps the icon extraction in a try/catch and falls through to
the existing placeholder `BitmapImage`, logging a warning — mirroring
the exception handling already present in `MainViewModel.WatchProcess`.

## PR Checklist

- [x] Closes: #48693
- [ ] **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 — no existing test harness
covers this WinUI converter path; validated manually (steps below)
- [x] **Localization:** All end-user-facing strings can be localized —
n/a; the only new string is a developer log warning, not end-user UI
- [x] **Dev docs:** Added/updated — n/a
- [x] **New binaries:** Added on the required places — n/a; no new
binaries

## Detailed Description of the Pull Request / Additional comments


`src/modules/FileLocksmith/FileLocksmithUI/Converters/PidToIconConverter.cs`:

```csharp
if (!string.IsNullOrEmpty(y))
{
    try
    {
        icon = Icon.ExtractAssociatedIcon(y);
    }
    catch (Exception ex)
    {
        // The process image path can be non-empty but no longer exist on disk
        // (e.g. self-updating software that deletes its old versioned directory while
        // the old process is still running). ExtractAssociatedIcon then throws and,
        // because this converter runs per-row during ListView virtualization, the
        // exception would otherwise reach App_UnhandledException and fast-fail the app.
        // Fall through to the placeholder icon instead of crashing.
        Logger.LogWarning($"Couldn't extract the icon for '{y}'. {ex}");
    }
}
```

(+ `using ManagedCommon;` — already a project reference; `Logger` is
used identically in `App.xaml.cs`.)

Scope note: this keeps to the targeted converter guard. I deliberately
did **not** also blanket-set `e.Handled = true` in
`App_UnhandledException`, since that would mask unrelated genuine
crashes; the converter guard fully addresses this crash.

## Validation Steps Performed

1. Reproduced on the installed 0.100 build: right-click `C:\` → **Unlock
with File Locksmith** → scroll the list → crash (`0xc000027b`, faulting
module `Microsoft.UI.Xaml.dll`; the FileLocksmith log shows an unhandled
`FileNotFoundException` for `...\SenseAPZ.exe` in
`MainPage...ProcessBindings` / `PidToIconConverter`). Reproduced twice.
2. Built `FileLocksmithUI` (x64/Release) with the fix — 0 warnings / 0
errors.
3. Ran the produced exe on `C:\` and scrolled ~40× over the same list
(which includes the stale `SenseAPZ` row):
   - No crash; the process stayed alive and responsive.
- The log shows the new warning firing for the exact `...\SenseAPZ.exe`
path, confirming the catch branch executed on the previously-fatal row.
   - 0 unhandled exceptions in the session.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 14:33:06 +08:00
Clint Rutkas
106c970c8c Fix full rebuild: upgrade CalculatorEngineCommon to C++20 coroutines (#48790)
Remove _SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS and upgrade
LanguageStandard from stdcpp17 to stdcpp20 so that <coroutine> is used
instead of the removed <experimental/coroutine> header on VS 2026.

<img width="946" height="348" alt="image"
src="https://github.com/user-attachments/assets/53392fd8-1a81-4852-9913-d84575f2f3e1"
/>

was part of https://github.com/microsoft/PowerToys/pull/48102 prior

<!-- 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: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 00:16:59 +01:00
Mike Griese
ff8c1dbf85 Dock: Add a teachingtip to docks while pinning (#48726)
When you're pinning something to the dock with the dialog, I have _no_
idea what display is 1,2,3,4. That's meaningless to me.

This attempts to resolve that by adding a teachingtip to each dock while
pinning. When we open that pin to dock dialog, we'll display the
teachingtips, and dismiss them when the dialog is closed.

That way folks have at least some indication of where each display is
when pinning.
2026-06-22 16:54:55 -05:00
Michael Jolley
cd7465e22a CmdPal: Refactor TopLevelCommandManager to use IExtensionService implementations (#48417)
## Summary

The `TopLevelCommandManager` handles **alot.** And by "alot" I mean too
much. In preparation for future types of extensions, this PR attempts to
pull out the methods for loading/managing extensions to individual
`IExtensionService`s.

These `ExtensionService`s will be injected the `TopLevelCommandManager`
via dependency injection. It will iterate through them asking them to
load their extensions. During that process, they will surface
`ICommandProvider`s back to the `TopLevelCommandManager` for display in
Command Palette.

Currently, there are two types of `IExtensionService`s:
`BuiltInExtensionService` and `WinRTExtensionService`.

- The `BuiltInExtensionService` receives all `ICommandProvider`s
registered with DI.
- The `WinRTExtensionService` manages out-of-process WinRT AppExtension
providers.

## Additional Changes

- **Circular DI dependency resolved**: `BuiltInsCommandProvider` no
longer depends on `IRootPageService`.
Previously, it required the `IRootPageService` solely for the dock
command to open Command Palette (using the RootPage as its command.)
Now, it uses a new `GoHomeDockCommand` which returns
`CommandResult.GoHome()` with the existing `onBeforeShowConfirmation`
callback to show the palette at the dock button position.
  
 Also, fixes #48643

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 16:51:12 -05:00
Niels Laute
502dc40aa4 [Shortcut Guide] Prevent overlay crash on section navigation (#48448) (#48481)
## Summary of the Pull Request

Shortcut Guide overlay crashes and closes when navigating between
sidebar sections (e.g. clicking PowerToys, then clicking the Windows
icon to come back). Repro and crash logs in #48448. Crash logs in #48441
show the same propagation path plus a follow-up access violation in
coreclr, indicating exceptions that escape local catches.

This PR fixes the immediate navigation race and adds broader crash
hardening so future exceptions on the UI/background threads are logged
instead of tearing down the overlay.

### Navigation-race fix (commit 1)

Root cause: `WindowSelector_SelectionChanged` calls
`App.TaskBarWindow.Activate()` and then `SetWindowPosition()`
synchronously. `Activate()` runs a reentrant `Window_Activated` →
`BringToFront` → `TaskbarWindow.Activated` chain that can leave
`App.TaskBarWindow.AppWindow` momentarily null, so `SetWindowPosition`
throws `NullReferenceException`.

Because the initial `SelectedItem = MenuItems[0]` is set from
`SetNavItems`, the NRE bubbles up into `InitializeNavItemsAsync`'s catch
block — which sets `_closeType = "InitializationFailed"` and closes the
window. That matches both user-visible symptoms: the overlay "flashes
and disappears" (#48441) on open and "closes when clicking the Windows
icon" (#48448).

Edits in
`src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuideXAML/MainWindow.xaml.cs`:

- **`SetWindowPosition`**: null-guard `App.TaskBarWindow?.AppWindow` and
skip the taskbar-overlap height adjustment when it is not currently
observable. Wrap the body in `try`/`catch` with `Logger.LogError` so any
future positioning hiccup keeps the previous layout instead of taking
down the overlay.
- **`WindowSelector_SelectionChanged`**: null-guard
`App.TaskBarWindow?.Hide()` / `Activate()` and wrap the body in
`try`/`catch`. This breaks the propagation path that lets a navigation
exception reach `InitializeNavItemsAsync`'s "fatal init failure" branch.

### Crash hardening (commit 2)

Additional defensive changes to cover other unguarded paths surfaced
while reviewing #48441:

- **`App.xaml.cs`**: register `App.UnhandledException`,
`AppDomain.CurrentDomain.UnhandledException`, and
`TaskScheduler.UnobservedTaskException` so a stray exception (e.g. an IO
failure during a fire-and-forget UI handler, or a background `Task`
fault) gets logged instead of tearing the process down with an access
violation in coreclr. Mirrors what Peek, AdvancedPaste, and CmdPal
already do.
- **`App.OnLaunched`**: wrap the launch sequence in `try`/`catch` and
exit cleanly with an error log on failure (`LoadData` / `MainWindow` /
`TaskbarWindow` ctors and `Activate` are all reachable failure points).
- **`App.LoadData`**: broaden the `Pinned.json` deserialize catch to
also handle `IOException` / `UnauthorizedAccessException`, and guard the
round-trip `SaveSettings` call as best-effort with a warning log.
- **`PinnedShortcutsHelper.Save`**: catch `IOException` /
`UnauthorizedAccessException` / `JsonException` and log; `Pin` / `Unpin`
runs from a synchronous UI handler so an unguarded `File.WriteAllText`
would tear down the overlay on any disk hiccup.
- **`TaskbarWindow.UpdateTasklistButtons`**: move the `AppWindow.Move`
calls inside the existing `try` block, null-guard
`App.MainWindow?.AppWindow` up front, and wrap the whole body so the
method (which runs from the ctor and from `Activated`) cannot tear the
overlay down when `MainWindow` is in a transient state.

## PR Checklist

- [x] Closes: #48448
- [x] Likely also fixes: #48441
- [x] **Communication:** Defensive fix to known crash paths; no API
change.
- [ ] **Tests:** No automated tests added — the failures are reentrancy
/ timing races that are hard to deterministically trigger in CI.
Validated with a synthetic repro (see below).
- [x] **Localization:** N/A — only logger strings.
- [x] **Dev docs:** N/A
- [x] **New binaries:** N/A

## Detailed Description of the Pull Request / Additional comments

The fix is intentionally defensive (rather than restructuring the
reentrant activation flow) because the legacy taskbar UIA enumeration
(`TasklistPositions.GetButtons`) on Windows 10 is what most reliably
widens the race window and is out of scope to redesign for a hotfix.

## Validation Steps Performed

- Build: `tools\build\build.cmd` from
`src\modules\ShortcutGuide\ShortcutGuide.Ui` — exit 0, errors log empty
for both commits.
- Synthetic repro: temporarily injected `throw new
NullReferenceException()` at the top of `SetWindowPosition` to exercise
the exact propagation path the reporter's log shows (`SetWindowPosition`
→ `SelectionChanged` → `set_SelectedItem` →
`InitializeNavItemsAsync.catch` → `Close("InitializationFailed")`).
- Before fix: overlay flashes and closes, log shows `Failed to
initialize navigation items.` with the NRE.
- After fix: overlay stays open, page navigates, log shows `Failed to
set Shortcut Guide window position; keeping previous layout.` from the
new `catch`.
- Did not reproduce the live race on a Win11 dev box; the reporter's
repro is Windows 10 19045 / Microsoft Store install where the legacy
taskbar UIA timing makes the reentrant chain more likely to expose the
null.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 11:00:25 +02:00
Gordon Lam
b5bd626d70 Scope autorun scheduled task permissions to its owner (#48739)
## Summary

The per-user **"Autorun for &lt;user&gt;"** logon scheduled task
(created in `src/runner/auto_start_helper.cpp`) was registered with a
security descriptor that granted **full access to Everyone**
(`D:(A;;FA;;;WD)`). This PR scopes the task's permissions to only the
accounts that actually need them.

## Changes

- `src/runner/auto_start_helper.cpp`
- Added a small `get_auto_start_task_sddl()` helper that builds the
task's security descriptor granting full control to **Local System**,
the **Administrators** group, and **the user the task belongs to** (SID
read from the current process token).
- Use it when calling `RegisterTaskDefinition` instead of the previous
Everyone descriptor.
  - Added `#include <sddl.h>` for `ConvertSidToStringSidW`.

## Behavior / compatibility

- No change to the run-at-startup or run-elevated flows. The decision
logic in `general_settings.cpp` is untouched — only the descriptor
string passed to `RegisterTaskDefinition` changed.
- The task owner can still fully manage (enable / disable / delete) its
own task in both elevated and non-elevated runner modes, because a
user's token SID is identical across elevation.
- The Task Scheduler service (SYSTEM) retains full control, so the task
still runs at logon as before.

## Validation

- Built `src/runner/runner.vcxproj` (x64 / Debug): **0 errors, 0
warnings**.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 16:59:06 +08:00
Gordon Lam
ee7d4a1938 [EnvironmentVariables] Apply profile variables through dedicated user-scope helpers (#48740)
## Summary of the Pull Request

Profiles in the Environment Variables utility override **User**
variables only, as described in
[`doc/devdocs/modules/environmentvariables.md`](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/modules/environmentvariables.md).
This change makes the implementation apply that documented behavior
consistently: every registry write performed while applying, unapplying,
or editing a profile now goes through dedicated user-scope helpers
instead of the generic `ParentType`-based helpers. It also removes some
dead code along the way.

## PR Checklist

- [ ] Closes: N/A (code consistency / cleanup)
- [x] **Tests:** No automated test project exists for this module;
validated via build + manual steps (see below)
- [x] **Localization:** No end-user-facing strings changed
- [x] **Dev docs:** Existing docs already describe the user-scoped
profile behavior; no change needed
- [x] **New binaries:** None

## Detailed Description of the Pull Request / Additional comments

- Added `SetProfileVariableWithoutNotify` /
`UnsetProfileVariableWithoutNotify` to `EnvironmentVariablesHelper`.
They centralize writing profile-driven variables to the current-user
environment, reusing the existing
`SetEnvironmentVariableFromRegistryWithoutNotify` primitive.
- Updated `ProfileVariablesSet.Apply` / `UnapplyVariable` and the
profile branch of `Variable.Update` to use these helpers.
- Removed an unused local (`var applyToSystem =
variable.ApplyToSystem;`) in `ProfileVariablesSet.Apply`.
- Removed the now-unused `SetVariableWithoutNotify` /
`UnsetVariableWithoutNotify` helpers. The notify variants (`SetVariable`
/ `UnsetVariable`) are unchanged and still back the default User/System
set editing.
- Net change: 3 files, +18 / −33.

## Validation Steps Performed

- Built `EnvironmentVariablesUILib` (Debug, x64): **succeeded, 0
warnings / 0 errors**.
- Manual: created, enabled, disabled, and edited profiles; confirmed
profile variables apply, back up the overridden values, and restore them
on disable exactly as before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 16:57:20 +08:00
Gordon Lam
a9e1f0f20e [MouseWithoutBorders] Use per-connection random salt and IV for encrypted streams (#48742)
## Summary

Mouse Without Borders encrypts the socket streams it uses to talk
between machines. Previously the PBKDF2 salt and the AES-CBC IV were
both derived from a single fixed constant, so every connection reused
identical key-derivation material.

This change generates a fresh random salt and IV for each connection and
exchanges them in-band as a small cleartext header ahead of the cipher
text. The AES key is now derived from the per-connection salt.

## Changes

- `Encryption.cs`: generate a random per-connection salt + IV,
write/read them as a cleartext header in
`GetEncryptedStream`/`GetDecryptedStream`, and derive the key from that
salt.
- Removed the now-unused fixed-IV helper (`GenLegalIV`), the `InitialIV`
constant, and the derived-key cache (a per-connection salt makes caching
pointless).
- The header exchange reuses the same tolerant socket-close handling as
the existing random first-block handshake, so the exception behavior
seen by callers (`SocketStuff`, `Clipboard`) is unchanged. Call sites
are untouched.

## Compatibility

This changes the on-the-wire format, so all paired machines must run
this version. Mouse Without Borders already requires the same version on
every machine (it surfaces *"make sure you run the same version in all
machines"* on a handshake mismatch). Existing keys and settings are
preserved - no re-pairing is needed once every machine is updated.

## Validation

- Built `MouseWithoutBorders` App and UnitTests (Release | x64) - clean
(exit 0).
- Added `EncryptionTests` covering encrypt -> decrypt round-tripping and
per-connection uniqueness (salt/IV/header differ on every connection).
All 3 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 16:37:32 +08:00
Gordon Lam
1ce0509fc7 Fix GITHUB_TOKEN leak in github-artifacts MCP helper (#48782)
## Summary

Hardens the local `github-artifacts` MCP helper
(`tools/mcp/github-artifacts/server.js`) so the `GITHUB_TOKEN` bearer
token is only ever sent to allowlisted GitHub API hosts. Previously the
same token was attached when downloading image and ZIP URLs extracted
from issue/PR Markdown, which could forward the token to any host
referenced in untrusted issue content.

This is repository MCP dev-tooling, not the shipped PowerToys runtime.

## What changed

- **New `auth.js`** centralizes token handling:
  - `AUTH_ALLOWED_HOSTS = { api.github.com }`
- `shouldSendGitHubToken(url)` — true only for HTTPS + allowlisted host
- `headersForUrl(url, token, extra)` — attaches `Authorization` only
when the host is allowlisted
- **`server.js`** routes all three request sites (`fetchJson`,
`downloadBytes`, `downloadZipBytes`) through `headersForUrl`. Image/ZIP
URLs from issue content are now fetched **without** `Authorization`.
Removed the ZIP dual-attempt auth retry that previously sent the token
to untrusted hosts.
- **New `test-token-allowlist.js`** regression test (wired into `npm
test` / `npm run test:unit`).

## Verification

- Unit test: 13/13 pass — token attached for `api.github.com`, withheld
from arbitrary hosts, loopback, look-alike hostnames
(`api.github.com.attacker.com`), non-HTTPS, and
`*.githubusercontent.com`.
- Cross-origin redirects: confirmed Node's `fetch` (undici) strips
`Authorization` when a redirect leaves the origin.
- Functionality preserved (real public issues, real token): issue #25595
images (5/5 downloaded) and issue #39476 `PowerToysReport_*.zip`
(downloaded + extracted) both succeed without the token — GitHub serves
this content via public/signed URLs.

## Note

For private-repo attachments that would require an `Authorization`
header on a non-API host, the token is intentionally not sent (the
recommended tradeoff). PowerToys is public, so the helper's normal use
is unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 16:37:03 +08:00
Jiří Polášek
b848c70071 Solution: Restore Microsoft.Plugin.Folder.UnitTests as a project in the solution (#46897)
## Summary of the Pull Request

This PR adds `Microsoft.Plugin.Folder.UnitTests` to SLNX as a project
instead of a plain file.

<!-- 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-06-22 13:52:32 +08:00
Clint Rutkas
9f7a726a7c Remove unused UPDATE_STAGE2 constants from UpdateUtils.h (#46974)
Remove `UPDATE_STAGE2_RESTART_PT` and `UPDATE_STAGE2_DONT_START_PT`
constants from `UpdateUtils.h`. Never referenced anywhere in the
codebase.

Fixes #46968. Found during multi-agent code review of #46889.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-22 13:49:13 +08:00
moooyo
32ad98a0dd [PowerDisplay] Detect built-in panel when driven by the discrete GPU (#48637)
## Summary of the Pull Request

On dual-GPU laptops, Power Display stopped detecting the built-in panel
(and adjusting its brightness) when the **discrete GPU** drives the
display — it showed "can't detect the display". This fixes that by
classifying displays by **capability** (does WMI brightness work on it?)
instead of by the nominal `OutputTechnology` value, which the discrete
GPU misreports for the internal panel.

## PR Checklist

- [x] Closes: #48587
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass
- [x] **Localization:** All end-user-facing strings can be localized (no
new user-facing strings added)
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places

## Detailed Description of the Pull Request / Additional comments

### Root cause

On a hybrid / MUX laptop, when the **discrete GPU** drives the built-in
eDP panel, `QueryDisplayConfig` reports the panel's
`DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY` as `DISPLAYPORT_EXTERNAL` (`10`)
instead of the `INTERNAL` flag (`0x80000000`) it reports under the
integrated GPU. It is the *same physical panel* (same EDID) — only the
reported connector type changes with the active GPU.

PR #47740 introduced a strict classifier: `OutputTechnology` →
internal/external, then **internal → WMI-only, external → DDC/CI-only,
with no fallback**. So under the discrete GPU the built-in panel was
classified *external* and sent to DDC/CI only — but a laptop eDP panel
does not speak DDC/CI, so it was dropped and Power Display reported it
couldn't detect any monitor. (`WmiMonitorBrightness` still exposes that
panel regardless of which GPU drives it, so the panel was actually
controllable — it just never got routed to WMI.)

### Fix: classify by capability, not by nominal output technology

- **`MonitorManager`** now runs **WMI discovery first** over the full
`QueryDisplayConfig` inventory. Every display `WmiMonitorBrightness`
exposes is treated as internal (WMI-controlled); whatever WMI does
**not** claim is routed to DDC/CI. The `OutputTechnology`-based
classifier is gone.
- **`WmiController`** matches the system-wide `WmiMonitorBrightness`
results against the full inventory by `Monitor.Id`. The persisted
`Monitor.Id` is still taken from the matched `DevicePath`
(byte-identical to the DDC route and to prior releases), so saved
brightness/per-monitor settings survive upgrades.
- New **`MonitorIdentity.FromInstanceName`** reduces a WMI
`InstanceName` to the same canonical `Monitor.Id` as `FromDevicePath`;
the separate `PnpHardwareKey` helper is removed.
- **Deleted** `DisplayClassifier` and `MonitorDisplayInfo.IsInternal`
(net ~150 fewer lines).

### Accepted trade-off

A monitor that exposes **both** `WmiMonitorBrightness` **and** DDC/CI is
now controlled via WMI only and won't get DDC-only features (contrast /
volume / input source / color temperature / power). This is uncommon
(typical laptop panels are WMI-only; typical external monitors are
DDC-only) and is a deliberate decision: it removes the entire class of
`OutputTechnology` misclassification bugs while keeping the performance
win of not DDC-probing internal panels.

## Validation Steps Performed

- Built the Power Display app (`PowerDisplay.csproj`) and
`PowerDisplay.Lib.UnitTests` (x64 / Debug) with MSBuild — both succeed,
including after merging latest `main` (Windows App SDK 2.2.0).
- Ran the unit test suite: **128/128 pass**, including new
`FromInstanceName` tests — the `FromInstanceName == FromDevicePath`
equivalence invariant and a concrete #48587 regression case (the BOE
panel reported as `OutputTechnology=10`).
- Traced the fix against the reporter's diagnostic logs: the panel that
previously went `OutputTechnology=10 → External → DDC → dropped` is now
claimed by WMI and controllable.
- Reviewed the diff for regressions (Monitor.Id persistence, monitor
blacklist, mirror mode, dual-internal-panel devices, external-only
desktops).

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:37:32 +08:00
Dave Rayment
2ffc248792 [Build] Increase sleep time in InteropTests to attempt to reduce CI failures (#48156)
<!-- 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
Increase sleep duration while waiting for IPC setup in the interop unit
test.

A number of PRs have had failures recently even though they do not touch
interop, likely due to this sleep being too short. An existing comment
points to the addition of the delay being because of this, and
resource-constrained build agents/environments could need a longer
duration.

<!-- 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
See comments on  #47211 and #48106.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
N/A
2026-06-22 12:55:26 +08:00
Bryce Cindrich
cabb71108a fix(shortcut-guide): use <N> token for literal digit keys in manifests (#48757)
## Summary of the Pull Request

Converts the literal-digit shortcut keys in the bundled Shortcut Guide
manifests to the `<N>` special-key convention, so they render as the
correct number.

Per the manifest spec, a bare number in `Keys:` is a virtual-key code. A
literal digit key authored as a bare number is therefore misread (VK `9`
is Tab, VK `1` is the left mouse button, VK `0` is undefined) and
renders incorrectly. The fix authors these as the `<N>` token (for
example `"<9>"`), which `KeyVisual` strips to display the digit.

This is a data-only change: **91 literal-digit keys across 14
manifests** become `<N>` tokens. No code or doc changes; the renderer
and converter already support `<N>`, and the convention is documented in
the spec.

Follow-up to #48461, which introduced and documented the `<N>`
convention (per @noraa-junker's request for a separate PR to fix the
remaining manifests). Together with #48461 this resolves the rendering
reported in #48460.

Files touched (all under
`src/modules/ShortcutGuide/ShortcutGuide.Ui/Assets/ShortcutGuide/Manifests/`):
Adobe.Illustrator (18), Adobe.Photoshop (17), SlackTechnologies.Slack
(13), Adobe.InDesign (11), JetBrains.IntelliJIDEA.Community (11),
BlenderFoundation.Blender (3), Figma.Figma (3), Google.Chrome (3),
Microsoft.Edge (3), Microsoft.VisualStudioCode (3), Mozilla.Firefox (3),
+WindowsNT.Notepad (1), Adobe.AfterEffects (1), GIMP.GIMP (1).

## PR Checklist

- [ ] **Closes:** N/A (follow-up to #48461; contributes to #48460)
- [ ] **Communication:** discussed in #48461; @noraa-junker requested
this separate PR.
- [ ] **Tests:** N/A for data; the converter and `<N>` convention are
unit-tested in #48461. Validated here by deserializing every manifest
with YamlDotNet (see below).
- [x] **Localization:** unchanged; these are per-language manifest
files.
- [ ] **Dev docs:** N/A (the `<N>` convention is documented in the spec
via #48461).
- [ ] **New binaries:** N/A.
- [ ] **Documentation updated:** N/A.

## Detailed Description of the Pull Request / Additional comments

Each change wraps a single bare digit in angle brackets, for example:

```yaml
          Keys:
-            - 9
+            - "<9>"
```

Quoted tokens (`"<9>"`) are used to match the dominant special-token
style already in the manifests (`"<Enter>"`, `"<Down>"`, etc.).

Note (out of scope): `Adobe.Photoshop.en-US.yml` has a shortcut with an
empty `Name: ""` (around line 799). That is a pre-existing data issue
unrelated to digit rendering; the digit is still converted, and the
empty name is left as-is.

## Validation Steps Performed

- Confirmed the diff touches only the 14 manifests, 91 insertions and 91
deletions, with each changed line being a digit wrapped as `"<N>"` (no
whitespace, indentation, or encoding churn).
- Confirmed zero bare-digit `Keys` entries remain and exactly 91 new
`"<N>"` tokens exist.
- Deserialized all 32 manifests with YamlDotNet (the same library the
app uses at runtime): 0 parse errors.
- Rendering behavior for `<N>` is already verified in #48461 (the
renderer strips the brackets to show the digit).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 12:40:40 +08:00
Bryce Cindrich
a0e53de825 feat(shortcut-guide): add Postman manifest and fix numbered-key display (#48461)
## Summary of the Pull Request

Adds a Shortcut Guide manifest for **Postman** and fixes a rendering bug
where single-digit keys in manifests displayed incorrectly.

- **Fix numbered-key rendering** —
`src/modules/ShortcutGuide/ShortcutGuide.Ui/Converters/ShortcutDescriptionToKeysConverter.cs`:
a single digit (`0`–`9`) in a manifest's `Keys` was treated as a Windows
virtual-key code instead of the literal digit. Since VK `1` is the left
mouse button, VK `9` is Tab, and VK `0` is undefined, shortcuts such as
`Ctrl+0` (reset zoom) and `Ctrl+9` (last tab) rendered as
blank/incorrect glyphs. Single digits are now rendered as the literal
character.
- **Add Postman shortcuts** —
`src/modules/ShortcutGuide/ShortcutGuide.Ui/Assets/ShortcutGuide/Manifests/Postman.Postman.en-US.yml`:
new manifest for `Postman.exe` covering Tabs, Sidebar, Request,
Interface, Window and modals, and Console. Auto-included via the
existing `Manifests/*.yml` glob in `ShortcutGuide.Ui.csproj`.
- **Show tab-number ranges** — Edge, Chrome, Firefox, and Postman
manifests: the "switch to a specific tab" entry used the literal key
`1`, which (after the fix above) read as `Ctrl + 1`. It now uses a `1 -
8` range so the keycap conveys "any tab number 1 through 8". The
separate "last tab" (`9`) and "reset zoom" (`0`) entries remain literal
single keys.
- **Add unit tests** — new `ShortcutGuide.UnitTests` (MSTest) project
covering `ShortcutDescriptionToKeysConverter.GetKeysList`, including the
single-digit regression.

## PR Checklist

- [x] Closes: #48460
- [ ] **Communication:** I've discussed this with core contributors
already. <!-- Filed #48460; the v0.100 announcement invites app-shortcut
contributions via PR. -->
- [x] **Tests:** Added/updated and all pass <!-- New
ShortcutGuide.UnitTests (MSTest); 8 tests pass locally via
vstest.console. -->
- [x] **Localization:** All end-user-facing strings can be localized
<!-- Shortcut names live in per-language manifest files (`*.en-US.yml`);
other locales fall back to en-US, consistent with existing manifests.
-->
- [ ] **Dev docs:** Added/updated <!-- N/A: no behavior requiring
dev-doc changes. -->
- [ ] **New binaries:** Added on the required places <!-- N/A: the new
manifest is a data asset under an already-shipped, globbed folder. The
new test project is auto-discovered by the existing `**\*UnitTest*.dll`
VSTest glob, so no CI pipeline change is required. -->
- [ ] **Documentation updated:** <!-- N/A -->

## Detailed Description of the Pull Request / Additional comments

The Shortcut Guide displays per-app shortcuts from YAML manifests,
matched to the foreground window via `WindowFilter`. Keys are converted
to keycaps by `ShortcutDescriptionToKeysConverter`. Numeric key strings
were unconditionally parsed as virtual-key codes, so literal-digit
shortcuts rendered wrong. The fix adds a `>= 0 and <= 9` case that emits
the digit character as-is; non-digit numeric codes (arrows, etc.) are
unchanged.

The new Postman manifest exercises this with `Ctrl+0` / `Ctrl+9`. The
browser/Postman "specific tab" entries were updated from the literal `1`
to the `1 - 8` range string, rendered verbatim by `KeyVisual` (the same
path used by the existing `Number (1-9)` key in the Windows Explorer
manifest).

A new `ShortcutGuide.UnitTests` (MSTest) project covers the converter:
single digits render literally (regression test), modifier ordering,
non-numeric passthrough (e.g. `1 - 8`), and arrow-key VK mapping.

## Validation Steps Performed

Built and ran locally (x64 Debug):

- Built `ShortcutGuideModuleInterface`, `ShortcutGuide.Ui`, and
`ShortcutGuide.IndexYmlGenerator`; launched the Debug `PowerToys.exe`.
- Triggered Shortcut Guide (`Win+Shift+/`) with **Postman** focused: the
Postman section renders with all categories, and `Ctrl+1` / `Ctrl+9` /
`Ctrl+0` display correctly (previously blank/incorrect).
- Verified the "specific tab" entry renders as `Ctrl + 1 - 8` in
**Edge**, **Chrome**, **Firefox**, and **Postman**.
- Built `ShortcutGuide.UnitTests` and ran via `vstest.console.exe`:
**8/8 tests pass**.

<img width="845" height="1432" alt="PowerToys Shortcut Guide Running
Postman"
src="https://github.com/user-attachments/assets/6359617e-3e2c-48b0-8005-b3684594ec94"
/>

Co-Authored-By: Claude Opus 4.8

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 12:40:29 +08:00
Dave Rayment
eab305334b [ColorPicker] Fix the main window UI appearing in the zoomed-in view (#48762)
<!-- 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

- [x] Closes: #37801
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **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
This uses `SetWindowAffinity()` with the `WDA_EXCLUDEFROMCAPTURE`
constant on the main Color Picker window to ensure the ZoomWindow bitmap
creation excludes the corner of the picker UI. The ability to capture
the picker window is restored immediately after, so it's still visible
in Snipping Tool, Remote Desktop and other tools.

The fix uses a new helper with simple `Include()` / `Exclude()` methods
as an adapter to the native code. This may potentially be included in
Common if other utilities needed it.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
1. Repro'd the original issue on the current Color Picker release:
<img width="262" height="263" alt="image"
src="https://github.com/user-attachments/assets/bc12b2f8-b67f-4b56-a803-57ab2fe0fa17"
/>

2. Confirmed that the fix worked and normal window affinity was restored
(otherwise I would not have been able to snip this):
<img width="262" height="262" alt="image"
src="https://github.com/user-attachments/assets/bc336ad0-4114-49fa-8440-b78466de363f"
/>

3. Repeated zooming in and out over a normal session.
2026-06-22 04:00:11 +00:00
Avirup
c41ac6df87 Add Pitjantjatjara language support to Quick Accent (#48561)
Closes #45025

## [QuickAccent] Add Pitjantjatjara / Yankunytjatjara Language Support

### Summary
This pull request adds support for Pitjantjatjara/Yankunytjatjara 
to the Quick Accent feature. Pitjantjatjara/Yankunytjatjara is an
Australian Aboriginal language
spoken in the Western and South Australian deserts.

### What's New
- Users can now access Pitjantjatjara/Yankunytjatjara retroflex
consonant characters (ḻ, ṉ, ṟ, ṯ)
  through Quick Accent
- Language is available in the Quick Accent settings dropdown menu
- Full localization support for UI display

### Why This Matters
Pitjantjatjara/Yankunytjatjara uses unique retroflex consonants that are
essential for:
- **Correct pronunciation:** Retroflex sounds are phonetically distinct
from regular consonants
- **Written accuracy:** Proper character representation in educational
materials
- **Cultural preservation:** Supporting Aboriginal language
documentation and teaching
- **Accessibility:** Users without specialized keyboards can now type
these characters easily

### Character Mappings
The implementation adds 4 retroflex consonants:
- **L → ḻ** (Retroflex lateral approximant)
- **N → ṉ** (Retroflex nasal)
- **R → ṟ** (Retroflex approximant)
- **T → ṯ** (Retroflex stop)

### Files Changed
1. **Language.cs** - Added `PJT` enum value
2. **CharacterMappings.cs** - Added language entry with character
mappings and display order
3. **Resources.resw** - Added localized UI string
4. **expect.txt** - Added language name to spell-check whitelist

### Testing
-  Language appears in Quick Accent settings dropdown
-  Retroflex characters appear when holding L/N/R/T + activation key
-  No build errors or warnings
2026-06-22 03:58:43 +00:00
Eymard Silva
e1b1a8d7ed Fix VS Code Workspaces shared storage lookup (#47505)
## Summary

Fixes VS Code Workspaces recent entries discovery after VS Code 1.118
moved `history.recentlyOpenedPathsList` to the shared application
storage database.

The plugin now probes both the legacy `User/globalStorage/state.vscdb`
database and the new shared storage database for VS Code Stable,
Insiders, Exploration, VSCodium, VSCodium Insiders, and portable
installs.

It also deduplicates results that may appear in both locations.

Fixes #47445

## Validation

- Reviewed the change against the issue's documented VS Code 1.118
storage paths.
- Attempted local focused build from
`src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces`
with `tools/build/build.cmd -Platform x64 -Configuration Debug`.
- Build was blocked by local environment/tooling issues unrelated to
this C# change: missing/invalid VC tooling/Windows SDK.
2026-06-21 22:20:24 +08:00
Matthew Gan
19c2c28dd9 Added the Philippine peso symbol to Quick Accent (#48444)
<!-- 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

- [X] Closes: #48443
<!-- - [ ] 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
The PR modifies the `CharacterMappings.cs` file by adding the Philippine
peso symbol to the symbols for the "P" key under the Currency group.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
No validation steps were performed as I assume this would not create any
breaking changes.
2026-06-20 17:37:56 +00:00
Clint Rutkas
459bd56fb6 Grab and Move: tight warning-gold overlay border + Always On Top 4px default (#48474)
## Summary

Refines the **Grab and Move** drag/resize overlay so it matches the
polish of **Always on Top (AoT)**, and lowers the AoT default border
thickness. Created at the request of @crutkas.

Two related border-refinement changes, kept in one PR because the Grab
and Move "double layer" is designed around AoT's border.



https://github.com/user-attachments/assets/0b605f92-60bd-44a0-a540-70e6d425146a



### 1. Always on Top - default border thickness 15 -> 4
The default highlight border was `15px`, which is visually heavy.
Dropped to `4px` for a tighter, Fluent-style frame.
- `src/modules/alwaysontop/AlwaysOnTop/Settings.h` (C++ default)
- `src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs`
(`DefaultFrameThickness`)
- Existing users keep their configured value; only fresh installs /
"reset" pick up `4`. Slider range (1-30) is unchanged.

### 2. Grab and Move - tight, warning-gold overlay (fill + border)
Previously the overlay was a full translucent **white wash** sized to
`GetWindowRect`, which includes the invisible resize-border / shadow
margins (~7px) - so it sat *off* the visible window. It now hugs the
visible frame, mirroring AoT:

- **Keeps the translucent white wash** over the visible window (the
familiar "grabbed" feedback) and adds a tight **warning-gold border on
top**. Both hug the visible frame and are rounded to match the window
corners.
- **Tight geometry:** anchored to `DWMWA_EXTENDED_FRAME_BOUNDS` (inset
by the invisible-border margins) instead of `GetWindowRect`.
- **Corner detection:** matches the window's corner radius via
`DWMWA_WINDOW_CORNER_PREFERENCE` (same mapping AoT uses); border
thickness and radius scale with the target window DPI.
- **Distinct accent:** Fluent **warning gold `#FFB900`** - the literal
equivalent of WinUI
[`SystemFillColorCaution`](https://learn.microsoft.com/en-us/windows/apps/design/style/color)
(used as a `ThemeResource` for warnings across the Settings UI; a Win32
layered window can't resolve a `ThemeResource`, so a literal is
required). Keeps Grab and Move visually distinct from AoT's accent-blue.
- **Double layer, for free:** the Grab and Move border is drawn just
**inside** the visible edge, while AoT draws its border just **outside**
the visible edge. The two naturally stack into a clean double layer, so
Grab and Move stays a constant **4px** with no AoT detection / window
enumeration.

Rendering keeps the existing GDI + `UpdateLayeredWindow` per-pixel-alpha
path and adds **GDI+** (a Windows system library - no new third-party
dependency) for the antialiased, rounded fill and border. Frame metrics
are computed **once per drag/resize** (never in the mouse-move hot
path). The optional geometry label is unchanged.

## Before / After
| | Before | After |
|---|---|---|
| Grab and Move overlay | Full white wash, offset from the window edge |
Same wash, now tight to the visible frame + gold border, corner-matched
|
| AoT default border | 15px | 4px |
| AoT + Grab and Move together | white wash over AoT border | GM gold
inside the edge + AoT accent outside it = double layer |

## Validation
- Builds clean (exit 0, 0 warnings/errors) for **x64 Debug**:
`GrabAndMove`, `AlwaysOnTop`, and `Settings.UI.Library` (Code Analysis /
C26451 clean).
- Smoke-tested live by running the standalone module exes: tight gold
border + wash on Alt-drag / Alt-right-drag, AoT 4px border, and the
inside/outside double layer on a pinned window.
- WARNING: still **draft** pending broader visual validation (border
tightness across DPIs, the exact gold, rounded vs square corners, AoT
z-order during fast drags - AoT renders from a separate process and
follows on a ~100ms timer). Screenshots to be added.

## Follow-up (not in this PR)
AoT and Grab and Move remain **separate** overlay systems (AoT:
persistent per-window Direct2D border; Grab and Move: transient
GDI/`UpdateLayeredWindow` overlay). They can't share one runtime window,
but the frame-geometry + corner-detection + DPI helpers are worth
extracting into `src/common` (seeded by AoT's
`WindowCornersUtil`/`ScalingUtils`). Tracked separately to keep this PR
atomic (`src/common` is an ABI-careful area).

## Notes
- No IPC/JSON schema changes; no new settings.
- No new third-party dependencies (GDI+ is a system library).

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-20 05:06:39 -07:00
Gleb Khmyznikov
e3a1ee2e5b [DOCS] docmd links to source files normalization (#48754)
This pull request adds a custom `docmd` plugin to improve how
documentation links to source files are handled, ensuring that
repo-root-relative links work both when editing locally and on the
published site. The main changes include introducing the new plugin,
updating configuration to use it, and documenting its behavior.

**Plugin integration and configuration:**

* Added a new local plugin `github-source-links` in `docmd-plugins/`,
which rewrites repo-root-relative links (e.g., `/src/.../Foo.cpp`) in
Markdown files to absolute GitHub blob URLs during the documentation
build process, ensuring links remain functional on the published site.
[[1]](diffhunk://#diff-c2c746e6974a6cfdd229031c2977f2bb0dca37c6d5f598ed45dc0d9f0b74c7caR1-R52)
[[2]](diffhunk://#diff-a97f3ce59c97313aacd716b9874b445d162c45ee2e6ef9fd2db59fe17235e1cfR1-R8)
* Updated `docmd.config.json` to register the new plugin under the
`plugins` key, enabling it for documentation builds.
* Updated `package.json` to include the plugin as a dependency,
referencing the local plugin directory.

**Documentation updates:**

* Updated `README.md` to document the new `docmd-plugins/` folder,
explain the purpose of repo-root-relative links, and describe how the
plugin rewrites these links for the published site.
2026-06-19 17:09:20 -05:00
Gleb Khmyznikov
d2aa24786d [DOCS] Add auto-generated github page for dev docs (#48752)
This pull request introduces a new, automated workflow for building and
publishing the developer documentation website using
[docmd](https://docmd.io/). The static site is now generated from
`doc/devdocs`, built in the `doc/devdocs-website` folder, and deployed
to GitHub Pages via a GitHub Actions workflow. The build output is not
committed to the repository but is instead published as an artifact.
Supporting configuration files, documentation, and `.gitignore` entries
are also added to streamline local development and CI/CD.

**Automated build and deployment:**

* Added `.github/workflows/regenerate-devdocs-website.yml` to build the
static site with docmd and deploy it to GitHub Pages automatically on
changes to `doc/devdocs` or `doc/devdocs-website`, or via manual
trigger.

**Project setup and configuration:**

* Added `doc/devdocs-website/package.json` to define the Node.js
project, pin the docmd version, and provide scripts for local
development and builds.
* Added `doc/devdocs-website/docmd.config.json` to configure docmd (site
title, source, output directory, base path).
* Added `doc/devdocs-website/.npmrc` to disable lockfile generation,
ensuring fresh dependency installs each build.

**Documentation and housekeeping:**

* Added `doc/devdocs-website/README.md` with instructions for editing,
building, and publishing the docs website.
* Added `doc/devdocs-website/.gitignore` to exclude the generated
`site/` output from version control.
2026-06-19 15:59:12 -05:00
Michael Jolley
d31d0ab168 Update labeling action to support pull requests (#48733)
This pull request updates the `auto-labeler.yml` GitHub Actions workflow
to improve reliability and maintain compatibility. The most important
changes are an upgrade to the `actions/github-script` version and
improved error handling when applying labels, ensuring the workflow does
not fail due to restricted permissions.

**Dependency upgrade:**
* Upgraded `actions/github-script` from version 7 to version 9 in the
`Apply area labels with AI` step, ensuring continued support and access
to the latest features and security updates.

**Error handling improvements:**
* Added a `try/catch` block around the label application logic to
gracefully handle cases where the workflow lacks permission to write
labels (e.g., due to restricted integration tokens), logging a message
and skipping the operation instead of failing the entire workflow.
2026-06-18 19:13:01 -05:00
Mike Griese
6dcda8a6aa CmdPal: Only list available docks when pinning (#48723)
This is a totally minor nitpick.

I don't have the dock enabled on all my displays. But the current "pin
to dock" dialog lets me pin it to a display I don't have the dock on.
When that happens, it effectively results in _nothing_ happening. Not
great.

This PR mitigates that situation, but only listing the enabled docks
when pinning.
2026-06-18 19:12:45 -05:00
ABHIJEET KALE
0d12a62abb [CmdPal][Dock] Fix performance meter showing '???' after restart (#48682)
This PR fixes Issue #48680 where the CmdPal dock performance meter shows
'???' after restart.

**Root cause**: The PerformanceWidgetsPage initially returns items with
a placeholder title ('???') because ContentData hasn't loaded yet. The
DataManager timer starts when the dock subscribes to ItemsChanged, and
the Updated event fires 1 second later with real data. However, the
Updated event handlers were updating ListItem.Title directly without
calling RaiseItemsChanged() on the page, so the dock was never notified
that the items had changed and continued to display the stale '???'
title.

**Fix**: Added a RaiseItemsChanged() call to each of the 5 Updated event
handlers (CPU, Memory, Network, GPU, Battery) after the item titles are
updated. This causes the dock to re-call GetItems() and refresh the
displayed titles.

Fixes #48680
2026-06-18 17:48:30 -05:00
Michael Jolley
486015d400 Add permissions for PR labeling (#48731)
This pull request makes minor adjustments to the
`.github/workflows/auto-labeler.yml` GitHub Actions workflow, focusing
on permissions and event triggers.

Workflow configuration updates:

* Added `pull-requests: write` permission to the workflow to ensure it
has the necessary access for managing pull requests.
* Removed the `edited` event from the list of triggers for
`pull_request_target`, so the workflow will no longer run when a pull
request is edited.
2026-06-18 17:47:42 -05:00
William
2125d739a3 Changed sleep icon to hibernation icon in Command Palette (#48689)
<!-- 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
Changed the icon from the sleep icon to the hibernate icon, which fixes
the issue in #48535

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

- [x] Closes: #48535
## Detailed Description of the Pull Request / Additional comments
The icon was previously the sleep icon, and I have changed it to the
hibernate icon.

## Validation Steps Performed
I have visually checked that the icon is now updated to the correct
icon.
2026-06-18 14:12:26 -05:00
Mike Griese
fb0f4292eb CmdPal: fix initializing the Run history in AOT builds (#48463)
Regressed in one of the last two releases.

The problem was that `history.ToImmutableList()` ran on the projected
`IVector<string>`. `ImmutableList.CreateRange` checks for
`IReadOnlyCollection<string>`, and resolving that interface on a WinRT
object requires a helper type that AOT can't generate.

So we have to just _not do that_.

Closes #48445
2026-06-18 12:55:59 -05:00
Niels Laute
a294e73bb6 [CmdPal] Animated notification (#48176)
## Summary of the Pull Request

Gives the CmdPal toast notification a glow-up: it now slides in/out with
a nice fade, has acrylic + a soft shadow, and stops fighting with
`SizeToContent`.

Along the way, the toast guts got refactored into a couple of reusable
bits that live in `PowerToys.Common.UI.Controls` so other PowerToys
utilities can grab them for their own transient overlays:

- **`TransparentWindow`** — a `WindowEx`-derived host that strips the
native frame, hides from taskbar/Alt-Tab, uses `TransparentTintBackdrop`
for transparency, and runs show/hide implicit animations on its content.
Supply your own animations via `ShowAnimations` / `HideAnimations`, or
take the defaults (fade + slide).
- **`TransparentCard`** — a templated `ContentControl` with acrylic
(`AlwaysActiveDesktopAcrylicBackdrop`), rounded corners, border, and
shadow. Drop whatever XAML you want inside.
- **`AlwaysActiveDesktopAcrylicBackdrop`** — small `SystemBackdrop`
wrapper so the acrylic doesn''t go grey when the window isn''t focused
(transient overlays are never focused).

CmdPal''s `ToastWindow` is now basically a 16-line wrapper: derives from
`TransparentWindow`, drops a bound `TextBlock` inside, and handles its
own 2.5s auto-hide timer + bottom-center positioning.



https://github.com/user-attachments/assets/3a62080c-22f0-480c-ac4d-028bcc32f07d



## PR Checklist

- [x] Closes: #40886
- [x] **Communication:** I''ve discussed this with core contributors
already.
- [ ] **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

## Detailed Description of the Pull Request / Additional comments

A couple of design notes worth calling out:

- The window is sized slightly bigger than the visible card (~24px
breathing room on each side) so the shadow and slide animation have room
to render without clipping. That buffer area is transparent but NOT
click-through — kept it small on purpose. We explored `SetWindowRgn` and
`EnableWindow` tricks to make it click-through too, but neither plays
nicely with WinUI 3''s DesktopWindowXamlSource. Small transparent frame
is the pragmatic compromise.
- Animations use the Toolkit''s implicit `ShowAnimations` /
`HideAnimations` so there''s zero animation code in
`ToastWindow.xaml.cs`.
- `TransparentCard.xaml` is registered in `Themes/Generic.xaml` —
required for templated controls in a library project;
`<GenerateLibraryLayout>` alone doesn''t auto-merge per-control xaml.

## Validation Steps Performed

- Built clean (arm64 Debug).
- Triggered a CmdPal toast manually: fades + slides in, hangs for 2.5s,
fades + slides out.
- Acrylic stays active when the window isn''t focused (toasts are never
focused).
- Shadow renders fully without clipping.

---------

Co-authored-by: niels9001 <niels9001@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 12:53:51 -05:00
Anton Kesy
f87b14d5b9 Fix grammar and typos (#47539)
<!-- 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

Fixes some typos and grammar mistakes inside docs, strings, and
comments.

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

None

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-18 12:36:52 -05:00
Michael Jolley
e8fee90f49 Enhance labeling action to support pull requests (#48729)
This pull request updates the auto-labeling workflow to support both
issues and pull requests, improves clarity in logs and prompts, and
renames the workflow file for broader applicability. The changes enhance
automation for triaging by enabling area label assignment to new or
updated pull requests in addition to issues.

**Expanded triage coverage and workflow improvements:**

* The workflow now triggers on both issue and pull request events
(`opened`, `reopened`, `edited`, `synchronize`), allowing automatic area
labeling for pull requests as well as issues.
(`.github/workflows/auto-labeler.yml`,
[.github/workflows/auto-labeler.ymlL1-R7](diffhunk://#diff-f874b1d773361dc46f2496bc3ce97ee3441e91257188b27a2bd83693c3b8a82eL1-R7))
* The concurrency group logic has been updated to handle pull request
events separately from issues, ensuring that rapid updates to a PR or
issue are managed correctly. (`.github/workflows/auto-labeler.yml`,
[.github/workflows/auto-labeler.ymlR24-R26](diffhunk://#diff-f874b1d773361dc46f2496bc3ce97ee3441e91257188b27a2bd83693c3b8a82eR24-R26))

**User experience and clarity enhancements:**

* Console logs and prompt messages now refer generically to "item" or
distinguish between "Issue" and "Pull request" as appropriate, improving
clarity in workflow output and AI prompts.
(`.github/workflows/auto-labeler.yml`,
[[1]](diffhunk://#diff-f874b1d773361dc46f2496bc3ce97ee3441e91257188b27a2bd83693c3b8a82eL41-R44)
[[2]](diffhunk://#diff-f874b1d773361dc46f2496bc3ce97ee3441e91257188b27a2bd83693c3b8a82eL58-R79)
[[3]](diffhunk://#diff-f874b1d773361dc46f2496bc3ce97ee3441e91257188b27a2bd83693c3b8a82eL139-R146)
[[4]](diffhunk://#diff-f874b1d773361dc46f2496bc3ce97ee3441e91257188b27a2bd83693c3b8a82eL212-R217)
* The system prompt for the AI labeling assistant has been updated to
clarify that both issues and pull requests should be classified and
labeled. (`.github/workflows/auto-labeler.yml`,
[.github/workflows/auto-labeler.ymlL127-R133](diffhunk://#diff-f874b1d773361dc46f2496bc3ce97ee3441e91257188b27a2bd83693c3b8a82eL127-R133))

**File naming:**

* The workflow file has been renamed from
`.github/workflows/auto-label-issues.yml` to
`.github/workflows/auto-labeler.yml` to reflect its broader scope.
(`.github/workflows/auto-labeler.yml`,
[.github/workflows/auto-labeler.ymlL1-R7](diffhunk://#diff-f874b1d773361dc46f2496bc3ce97ee3441e91257188b27a2bd83693c3b8a82eL1-R7))
2026-06-18 12:05:33 -05:00
Copilot
bdc60b6415 CmdPal: Include basic SettingsManager in extension template (#46028)
Adds a `SettingsManager` scaffold to the CmdPal extension template so
new extensions have a ready-to-use settings pattern out of the box.

## Summary of the Pull Request

Most extensions need configuration. The template previously had no
settings infrastructure, leaving developers to discover the pattern from
built-in extensions. This adds a wired-up `SettingsManager` following
the same conventions used across all built-in extensions.

## PR Checklist

- [ ] Closes: #xxx
- [ ] **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

**`Helpers/SettingsManager.cs`** (new)
- Extends `JsonSettingsManager` (already bundled in
`Microsoft.CommandPalette.Extensions`)
- Uses `Utilities.BaseSettingsPath()` for correct local-state path
(packaged and unpackaged)
- Includes commented-out `ToggleSetting` example illustrating the full
add/register/expose cycle
- Auto-saves via `Settings.SettingsChanged` event

**`TemplateCmdPalExtensionCommandsProvider.cs`** (updated)
- Instantiates `SettingsManager` as a field
- Sets `Settings = _settingsManager.Settings` to advertise settings to
CmdPal
- Exposes `_settingsManager.Settings.SettingsPage` in `MoreCommands` for
in-app settings access

```csharp
// Helpers/SettingsManager.cs
internal sealed partial class SettingsManager : JsonSettingsManager
{
    private static readonly string _namespace = "templatecmdpalextension";
    private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";

    // TODO: Add your settings here. For example:
    // private readonly ToggleSetting _myToggle = new(
    //     Namespaced(nameof(MyToggle)), "My toggle setting", "Description", false);
    // public bool MyToggle => _myToggle.Value;

    public SettingsManager()
    {
        FilePath = SettingsJsonPath();
        // Settings.Add(_myToggle);
        LoadSettings();
        Settings.SettingsChanged += (_, _) => SaveSettings();
    }
}

// TemplateCmdPalExtensionCommandsProvider.cs
private readonly SettingsManager _settingsManager = new();

public TemplateCmdPalExtensionCommandsProvider()
{
    // ...
    _commands = [
        new CommandItem(new TemplateCmdPalExtensionPage())
        {
            Title = DisplayName,
            MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
        },
    ];
    Settings = _settingsManager.Settings;
}
```

The template wizard replaces `"TemplateCmdPalExtension"` throughout all
files, so `BaseSettingsPath` and the namespace string will update
automatically when scaffolding a new extension.

## Validation Steps Performed

- Verified `SettingsManager` pattern matches existing built-in
extensions (`ClipboardHistory`, `System`, `Registry`)
- No new package references required — `JsonSettingsManager`,
`ToggleSetting`, and `Utilities` are all included in the existing
`Microsoft.CommandPalette.Extensions` NuGet package

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>CmdPal: Include basic SettingsManager in
Template</issue_title>
> <issue_description>### Description of the new feature / enhancement
> 
> Include basic `SettingsManager` in template.
> 
> ### Scenario when this would be used?
> 
> I believe most extensions will require some form of configuration, so
adding basic `SettingsManager` would be helpful.
> 
> ### Supporting information
> 
> _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#39274

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

🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
Co-authored-by: Muyuan Li (from Dev Box) <muyuanli@microsoft.com>
2026-06-18 16:33:11 +00:00
ABHIJEET KALE
8dc4598786 [CmdPal][System] Add Chinese (ZH) translation guidance for Sleep vs Hibernate (#48534) (#48653)
## Summary

Fixes incorrect Chinese translation where both Sleep and Hibernate
commands showed '休眠' (hibernation) instead of using distinct terms: '睡眠'
for Sleep and '休眠' for Hibernate.

## Changes

Added \<comment>\ elements with \ZH:\ guidance to 6 resource entries in
\Microsoft.CmdPal.Ext.System\\Properties\\Resources.resx\:

| Resource Key | Current Comment | Added ZH Guidance |
|---|---|---|
| \Microsoft_plugin_command_name_hibernate\ | *(none)* | translate as
'休眠' |
| \Microsoft_plugin_sys_hibernate\ | Existing English comment | use 休眠
not 睡眠 |
| \Microsoft_plugin_sys_hibernate_confirmation\ | Existing English
comment | use 休眠 not 睡眠 |
| \Microsoft_plugin_command_name_sleep\ | *(none)* | translate as '睡眠',
do NOT use '休眠' |
| \Microsoft_plugin_sys_sleep\ | Existing English comment | use 睡眠 not
休眠 |
| \Microsoft_plugin_sys_sleep_confirmation\ | Existing English comment |
use 睡眠 not 休眠 |

## Why comments only?

Chinese translations are managed by the internal CDPX localization
pipeline. Adding \<comment>\ elements to the English \.resx\ file is the
standard way to guide translators for the next localization pass. See
similar fix in PR #48649 for Japanese translation guidance.
2026-06-18 12:26:32 +02:00
ABHIJEET KALE
fa8fbb60a1 [CmdPal][Localization] Add JA translation guidance comments for Download and Battery strings (#48649)
## Summary

Adds translation guidance comments to English .resx resource strings
that have incorrect Japanese translations in the CDPX localization
pipeline.

## Problem

- **Download/Downloading** — translated as 受け取り (implies physical
package receipt) instead of the correct IT term 受信
- **Battery** (laptop/tablet context) — translated as 電池 (dry cell)
instead of バッテリー (built-in battery)

## Fix

Added comment elements to 9 resource entries across 3 files with the
prefix JA: to guide the localization team:

| File | Strings | Comment |
|------|---------|---------|
| Microsoft.CmdPal.Ext.WinGet\Properties\Resources.resx |
winget_downloading, winget_download_progress | JA: translate as 受信 not
受け取り |
| Microsoft.CmdPal.UI.ViewModels\Properties\Resources.resx |
winget_operation_status_downloading,
winget_operation_status_downloading_percent,
gallery_item_winget_action_downloading_with_progress,
gallery_item_winget_action_downloading | JA: translate as 受信 not 受け取り |
| Microsoft.CmdPal.Ext.WindowsSettings\Properties\Resources.resx |
BatterySaver, BatterySaverSettings, BatteryUse | JA: translate as バッテリー
not 電池 |

## Background

As documented in doc/devdocs/development/localization.md, localized
.resx files are generated at build time by the CDPX pipeline from .lcl
files managed by the internal localization team. The comment elements in
English .resx files are surfaced to translators and are the correct
mechanism for providing translation guidance. The actual .lcl file fixes
must be applied by the internal team.

Closes #48598
2026-06-18 12:25:40 +02:00
Clint Rutkas
ab4947579b [QuickAccess] Suppress unhandled XAML exceptions in flyout host (#48457)
## Summary

Adds the two missing top-level exception handlers in the QuickAccess
(Preview) flyout host so that an unhandled XAML exception during launch
or page navigation no longer FailFasts `PowerToys.QuickAccess.exe`.

Spotted while reading through `App.OnLaunched` and `ShellPage` for an
unrelated review of the flyout startup path — none of the existing
handlers exist yet, so any throw during `MainWindow` construction,
`ShellHost.Initialize`, or `ContentFrame.Navigate(typeof(LaunchPage) |
typeof(AppsListPage), …)` bubbles all the way out to the Windows App SDK
runtime and is stowed as a XAML failure. Compare with
`src\settings-ui\Settings.UI\SettingsXAML\App.xaml.cs`, which already
wires `UnhandledException += App_UnhandledException`.

## Changes

**`src\settings-ui\QuickAccess.UI\QuickAccessXAML\App.xaml.cs`**

- Hook `Application.UnhandledException` in the constructor. The handler
logs the exception via `ManagedCommon.Logger.LogError` (same logger
Settings uses) and sets `e.Handled = true`. QuickAccess is a transient
launcher flyout owned by the runner, so swallowing a stray XAML error
and keeping the host alive for the next summon is the correct trade-off
— the failure is still recorded for diagnostics.
- Wrap the body of `OnLaunched` in a try/catch. If `MainWindow` (which
sets up window chrome, listener threads, the IPC coordinator, and the
XAML shell) fails to construct, log the exception and call `Exit()`
cleanly rather than letting the throw escape into the Windows App SDK
launch path.


**`src\settings-ui\QuickAccess.UI\QuickAccessXAML\Flyout\ShellPage.xaml.cs`**

- Subscribe to `ContentFrame.NavigationFailed` after
`InitializeComponent`. A page constructor or XAML-load failure in
`LaunchPage` / `AppsListPage` would otherwise bubble out of the `Frame`
and crash the launcher. The handler logs the failure
(`SourcePageType.FullName` + the exception) and marks it handled so the
next summon retries navigation.

No production behaviour changes when things work — only the failure
paths are different. No public API surface changes.

## Why both handlers, not just one

- `Application.UnhandledException` does not fire for
`Frame.NavigationFailed`. The Frame raises its own event first and, if
no handler runs or `e.Handled` is left `false`, then it rethrows on the
dispatcher.
- Conversely, `Frame.NavigationFailed` only fires for navigation
failures — not for an exception thrown directly in `OnLaunched` before
any navigation happens.

The two events are complementary, so both need a handler to fully cover
the launch + navigation paths.

## Testing

- The local NuGet feed on my dev box currently can't restore
`Microsoft.NETCore.App.Runtime.win-x64 = 10.0.9` (the feed only has
`11.0.0-preview.1.26104.118`), which fails the project restore for every
WinUI project including this one. That's the same environment issue I
called out on #48414 — pipeline restore uses a different feed and is
fine.
- All three patterns added here are copy-paste analogues of code that
already exists in `Settings.UI` (`App.xaml.cs:96, 106-109`,
`ShellViewModel.cs:86, 136`), so namespace and signature drift risk is
minimal. The only behavioural difference is `e.Handled = true`, which is
the actual goal of this PR.

## Risk

- Low. Two new event handlers and one try/catch. No behaviour change on
the success path.
- Worst-case regression is that a real, repeatable XAML failure becomes
silent in the runner's eyes (no process crash) instead of loud — but
it's logged via `Logger.LogError` so the user can still find the trace
in `%LOCALAPPDATA%\Microsoft\PowerToys\Logs\`.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---

ADO:
https://microsoft.visualstudio.com/DefaultCollection/OS/_workitems/edit/61258633/

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 12:20:38 +02:00
moooyo
d221f84d8f [Skills] Update WinUI3 migration skills to add more migration mapping item (#47043)
<!-- 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

- [x] Closes: #46464
<!-- - [ ] 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>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-18 12:17:49 +02:00
Tucker Burns
14152672c5 Add defensive error handling for Settings navigation and search (#46688)
## Summary

Addresses the **root cause chain** behind the NullReferenceException
crash in Settings navigation. Crash dump analysis with WinDbg revealed
that `Frame.Navigate()` can throw native WinUI ABI exceptions that
propagate unhandled through multiple code paths.

### Changes (3 files)

**NavigationService.cs** — Defensive try-catch for all
`Frame.Navigate()` call sites:
- `Navigate()`: Wrap in try-catch, log via `Logger.LogError()`, return
`false` on failure
- `EnsurePageIsSelected()`: Add matching try-catch for consistency (also
calls `Frame.Navigate()` unprotected)
- Protects **all** navigation in the Settings app

**ShellViewModel.cs** — Fix the crash-causing `Frame_NavigationFailed`
handler:
- Replace `throw e.Exception` with `e.Handled = true` +
`Logger.LogError()`
- The original code threw `NullReferenceException` when `e.Exception`
was null (native WinUI errors don't always marshal to managed Exception
objects)
- Mark the event as handled to prevent framework error propagation

**ShellPage.xaml.cs** — Protect `async void SearchBox_QuerySubmitted`:
- Wrap entire method body in try-catch
- `async void` methods that throw after `await` produce unhandled
exceptions that crash the process
- Covers both search indexing (`Task.Run`) and navigation failures

### Crash Dump Evidence

Analysis of `PowerToys.Settings_2025_03_26_48636.dmp` with WinDbg:
- 4 stowed exception records found at `0x000001c60d3b9100`
- Exception chain: `SearchBox_QuerySubmitted` → `Frame.Navigate()` fails
at native ABI → `NavigationFailed` fires with null Exception → `throw
null` → `NullReferenceException` → `FailFastWithStowedExceptions`
- Root failure in `Microsoft.UI.Xaml.dll!DirectUI::Frame::Navigate`

### Review Notes
- `catch (Exception)` pattern matches 92.5% of Settings.UI codebase
convention
- `Logger.LogError()` with `using ManagedCommon` matches standard import
pattern
- No security concerns: logged page type names are not sensitive data
- Build verified locally (38/38 applicable tests pass; 111 COM-dependent
tests fail identically on main)

Co-authored-by: Tucker Burns <tuck.burns@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-18 11:15:52 +02:00
Matheus Mol
5688441127 [KBM] Fix modifier key remapped to non-modifier delivering WM_SYSKEYDOWN (#47192)
## Summary of the Pull Request

When a modifier key (Ctrl/Alt/Shift) is remapped to a non-modifier key
using
Keyboard Manager, the injected key event is delivered to applications as
WM_SYSKEYDOWN instead of WM_KEYDOWN. This causes unexpected behavior —
for
example, remapping Left Alt to Backspace results in whole words being
deleted
instead of single characters, because applications interpret
WM_SYSKEYDOWN +
VK_BACK as Alt+Backspace.

The fix resets the modifier state with a suppress-flag key-up event
before
injecting the target key, consistent with the existing approach used for
the
Caps Lock remapping scenario.

## PR Checklist

- [ ] Closes: #47191
- [ ] Communication: I've discussed this with core contributors already.
If the work hasn't been agreed, this work might be rejected
- [x] 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

## Detailed Description of the Pull Request / Additional comments

The root cause is that SendInput is called inside the low-level keyboard
hook
callback before the original modifier event is suppressed. At that point
the
modifier state is still active, so the OS delivers the injected key as
WM_SYSKEYDOWN (system key with Alt context) rather than WM_KEYDOWN.

This is the same mechanism that was already fixed for the Ctrl/Alt/Shift
↔
Caps Lock case. This PR extends the fix to cover modifier → non-modifier
remaps.

## Validation Steps Performed

1. Remapped Left Alt → Backspace in Keyboard Manager
2. Opened a text editor, typed text, pressed the remapped key
3. Confirmed single characters are deleted instead of whole words
4. All 93 unit tests pass (KeyboardManager.Engine.UnitTests)
2026-06-18 16:33:56 +08:00
331 changed files with 15450 additions and 2506 deletions

View File

@@ -17,8 +17,8 @@ LIGHTTURQUOISE
NCol
OLIVEGREEN
PALEBLUE
PArgb
Pbgra
pargb
pbgra
SRGBTo
WHITEONBLACK

View File

@@ -24,7 +24,7 @@ advancedpaste
advapi
advfirewall
AFeature
affordances
affordance
afterfx
AFX
agentskills
@@ -135,6 +135,7 @@ BITMAPINFO
BITMAPINFOHEADER
BITSPERPEL
BITSPIXEL
Blackmagic
bla
BLENDFUNCTION
blittable
@@ -415,7 +416,6 @@ DISPLAYFLAGS
DISPLAYFREQUENCY
displayname
DISPLAYORIENTATION
DISPLAYPORT
divyan
DLGFRAME
dlgmodalframe
@@ -465,8 +465,8 @@ DWMNCRENDERINGCHANGED
Dwmp
DWMSENDICONICLIVEPREVIEWBITMAP
DWMSENDICONICTHUMBNAIL
DWMWA
DWMWCP
dwmwa
dwmwcp
DWMWINDOWATTRIBUTE
DWMWINDOWMAXIMIZEDCHANGE
DWORDLONG
@@ -540,6 +540,7 @@ EXTRINSICPROPERTIES
eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
Fairlight
FARPROC
fdw
fdx
@@ -622,7 +623,9 @@ GETPROPERTYSTOREFLAGS
GETSCREENSAVERRUNNING
GETSECKEY
GETSTICKYKEYS
GETTASKBARPOS
GETTEXTLENGTH
GETWORKAREA
gfx
GHND
gitmodules
@@ -677,6 +680,7 @@ HDEVNOTIFY
hdr
HDROP
hdwwiz
Headered
Helpline
helptext
hgdiobj
@@ -861,7 +865,6 @@ jjw
jobject
JOBOBJECT
jpe
JPN
jpnime
jrsoftware
Jsons
@@ -994,7 +997,6 @@ LTM
LTRREADING
luid
lusrmgr
LVDS
LWA
LZero
MAGTRANSFORM
@@ -1058,8 +1060,6 @@ MINIMIZESTART
MINMAXINFO
minwindef
Mip
Miracast
miracast
mkdn
mlcfg
mmc
@@ -1234,6 +1234,8 @@ NOTSRCCOPY
NOTSRCERASE
Notupdated
notwindows
NOTXORPEN
Nouveaut
nowarn
NOZORDER
NPH
@@ -1364,6 +1366,7 @@ pii
pinfo
pinvoke
pipename
Pitjantjatjara
PKBDLLHOOKSTRUCT
pkgfamily
PKI
@@ -1387,6 +1390,7 @@ popups
POPUPWINDOW
portfile
POSITIONITEM
Postbot
POWERBROADCAST
powerdisplay
POWERDISPLAYMODULEINTERFACE
@@ -1506,6 +1510,7 @@ RAWPATH
rbhid
Rbuttondown
rclsid
RCW
RCZOOMIT
rdp
RDW
@@ -1638,6 +1643,7 @@ SETPOWEROFFACTIVE
SETRANGE
SETREDRAW
SETRULES
SETAUTOHIDEBAREX
SETSCREENSAVEACTIVE
SETSTICKYKEYS
SETTEXT
@@ -1816,7 +1822,6 @@ svchost
SVGIn
SVGIO
svgz
SVIDEO
SVSI
SWFO
swp
@@ -1915,6 +1920,7 @@ tracerpt
trackbar
trafficmanager
transicc
transitioning
TRAYMOUSEMESSAGE
triaging
trl
@@ -2080,6 +2086,7 @@ winapi
winappsdk
windir
WINDOWCREATED
WINDOWDESTROYED
windowedge
WINDOWINFO
WINDOWNAME
@@ -2175,6 +2182,7 @@ xclip
xcopy
xdf
xfd
xhair
xmp
Xoshiro
xsi
@@ -2183,6 +2191,7 @@ XUP
XVIRTUALSCREEN
XXL
xxxxxx
Yankunytjatjara
ycombinator
yinle
yinyue

View File

@@ -30,6 +30,12 @@ These are auto-applied based on file location:
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
## Shortcut Guide V2 Manifests
When creating or editing Shortcut Guide keyboard shortcut manifest files, follow the schema and naming conventions in the spec:
- [WinGet Manifest Keyboard Shortcuts schema](<../doc/specs/WinGet Manifest Keyboard Shortcuts schema.md>) manifest file format, field definitions, file naming, and the `+` prefix convention for apps without a WinGet package
## Detailed Documentation
- [Architecture](../doc/devdocs/core/architecture.md)

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Microsoft Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,302 @@
---
name: powertoys-module-verification
description: "Verify a single PowerToys module's release checklist items end-to-end. Drive each checkbox via UIA / Named Events / settings.json edits / clipboard inspection / GPO / SendInput. Output a structured PASS / FAIL / BLOCKED verdict per item with evidence (FAIL distinguishes product defects from stale/ambiguous checklist items). Combine standard winapp ui mechanics (see references/winapp-ui-testing.md) with PT-specific recipes and the helper .ps1 files shipped with this skill."
license: Complete terms in LICENSE.txt
---
## When to use this skill
Use this skill when you need to **verify every checklist item for a single PowerToys module** for a release sign-off — e.g. "verify all 18 Color Picker items", "verify all 88 Command Palette items". Each item produces a PASS / FAIL / BLOCKED verdict with evidence (UIA enumeration, log line, settings.json diff, screenshot, etc.).
The **checklist to verify is supplied with the task** (the calling prompt points you at the module's checklist file). This skill is the *how* — the drive techniques, helpers, taxonomy, and reporting format — independent of any specific checklist.
## Required reads (in order)
1. **`references/winapp-ui-testing.md`** — the **prerequisite** UIA mechanics doc (winapp ui verbs, scripted batch testing, file pickers, accessibility audits, screenshots, click-vs-invoke, PostMessage, SendInput cb=40, stunted-UIA recovery, settings-mutation safety contract). **Read this first** — this skill assumes you know its content and only adds PT-specific extensions.
2. **This `SKILL.md`** — the PT-specific playbook: the 3-bucket drive-technique selector (Step 2), classification taxonomy, critical pitfalls, helper-script catalog.
3. **`references/modules/<module>.md` IF IT EXISTS** — per-module entry-paths, item-by-item recipes, common BLOCKED traps, fixture lists, source citations. **Always check `references/modules/` first.** If no profile exists, fall back to this SKILL.md and create one after you finish (template in `references/modules/README.md`).
4. **`references/explorer-context-menu-flow.md` IF your module registers an Explorer right-click entry** (PowerRename, File Locksmith, Image Resizer, New+, Preview Pane, RegistryPreview) — shared synthetic-right-click + UIA-invoke + multi-file-selection flow + module-caption table. Helper: `scripts/pt-explorer-contextmenu.ps1`.
5. **`references/pre-flight.md`** — pre-flight checks, bootstrap snippet, state-hygiene cleanup, final wrap-up, hard rules.
6. **`references/reporting-format.md`** — per-item table template, top-of-report summary, step-table rules, anti-patterns, worked example.
7. **`references/environment-setup.md`** — RDP/sleep/screensaver/session-attachment gotchas. Cite in BLK-ENV verdicts.
8. **`references/release-checklist/<module>.md`** — the checklist for the module under test (one file per module; see `references/release-checklist/index.md` for the full list). Each item carries `[ADMIN: …]` + `[CLARITY: …]` metadata. **This file IS the set of items to verify.**
## Helper scripts shipped with this skill
| File | Purpose |
|---|---|
| `scripts/pt-shared-events.ps1` | `Invoke-PtSharedEvent`, `Test-PtSharedEvent`, `Get-PtSharedEventCatalog` — 56-entry friendly-name map for PT Named Events (CmdPal.Show, AOT.Pin, PowerLauncher.Invoke, LightSwitch.Toggle, ZoomIt.Draw, ...). The deterministic, foreground-free, UIPI-immune way to trigger a module. |
| `scripts/pt-sendinput-chord.ps1` | `Send-PtChord`, `Wait-PtHotkeyAccepted` — last-resort SendInput hotkey injection with the cb=40 fix. Use only when the module has no Named Event and the hotkey itself is the test subject. |
| `scripts/pt-foreground-guard.ps1` | `Test-PtForeground`, `Force-PtForeground`, `Assert-PtForegroundOrAbort` — guard helpers to ensure target window IS foreground before SendInput, so keys don't leak to caller's terminal. |
| `scripts/pt-cmdpal-recycle.ps1` | `Reset-CmdPalAppX`, `Reset-CmdPalToHome`, `Test-CmdPalDegraded`, `Invoke-CmdPalQuery` — CmdPal-specific lifecycle (handles TextChanged-broken state, BackButton navigation, AppX recycle). |
| `scripts/pt-admin-probe.ps1` | `Test-PtAdmin`, `Test-ProcessElevated`, `Test-PtRunnerAdmin` — TokenElevation probes to verify your session and the PT runner have the right elevation for the test. |
| `scripts/pt-clipboard-diff.ps1` | `Get-PtClipboardFormats`, `Compare-PtClipboardFormatDiff`, `Set-PtClipboardRich` — multi-format clipboard inspection for Advanced Paste tests. |
| `scripts/pt-explorer-com.ps1` | `Get-PtExplorerWindows`, `Open-PtExplorerAtPath`, `Select-PtExplorerFiles`, `Invoke-PtPeekWithExplorerSelection`, `Test-PtInteractiveDesktop` — drive Explorer via Shell COM to set up multi-file selections, then trigger Peek/FZ/PowerRename/Image Resizer/Workspaces via their hotkeys. **Use this for Peek L706-L709, L719-L720 and any test that needs an Explorer file selection.** |
| `scripts/pt-explorer-contextmenu.ps1` | `Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems` — open Win11's real context menu via synthetic right-click (with retry), then UIA-invoke a menu item by name. **Canonical user-flow path for File Locksmith / Image Resizer / PowerRename / New+ menu-presence + launch tests.** Needs an unlocked interactive desktop. See `references/explorer-context-menu-flow.md` for the full write-up, stability notes, and per-module captions. |
| `scripts/pt-shell-verbs.ps1` | `Get-PtShellVerbs`, `Invoke-PtShellVerb`, `Reset-PtShellComCache` — enumerate + invoke CLASSIC HKCR shell verbs via Shell.Application COM. **NOT for PT context-menu modules on Win11** (PT registers via IExplorerCommand, not classic — use `pt-explorer-contextmenu.ps1` for those). Useful for non-PT verbs (Open/Edit/Send-to/third-party) and as a negative check that PT verbs are NOT classic-shadowed. |
| `scripts/pt-state.ps1` | `Get-PtSettings`, `Get-PtModuleSettings`, `Get-CmdPalSettings`, `Get-PtRunnerLogTail`, `Test-PtModuleEnabled`, `Test-PtModuleProcess`, `Restart-PtRunner`, `Backup-PtModuleSettings`, `Restore-PtModuleSettings` — common state checks. |
| `scripts/pt-nonelevated.ps1` | `Start-PtNonElevated`, `Invoke-PtNonElevatedCapture` — launch an exe at **Medium IL (non-elevated)** from an elevated agent shell via a one-shot `RunLevel Limited` scheduled task. Required for elevation-visibility tests (a non-elevated module must NOT see higher-integrity processes; e.g. File Locksmith L649/L650). Verify the result with `Test-ProcessElevated`. |
Dot-source them **all** at once in your bootstrap (the `Get-ChildItem` loop loads every helper — see **Step 1 — Bootstrap**):
```powershell
$skill = '<this skill folder>' # the folder containing SKILL.md, e.g. <PT-repo>\.github\skills\powertoys-module-verification
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
```
## Step 1 — Bootstrap
```powershell
$module = 'AdvancedPaste' # or 'CmdPal', 'FZ', 'Peek', ...
# Work out of %TEMP% during the run (keeps screenshots/scratch off OneDrive); move to the
# sign-off archive at the very end (see Step 7).
$workspace = "$env:TEMP\verify-$module-$(Get-Date -Format yyyyMMdd-HHmmss)"
New-Item -ItemType Directory -Path $workspace, "$workspace\artifacts" -Force | Out-Null
$report = "$workspace\verify-$module.md"
# Dot-source helpers
$skill = '<this skill folder>' # set once at top of your script (the folder containing SKILL.md)
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
# Verify environment
"=== Environment ===" | Tee-Object $report -Append
"IsAdmin: $(Test-PtAdmin)" | Tee-Object $report -Append
$rn = Test-PtRunnerAdmin
"PT runner: PID=$($rn.Pid) Elevated=$($rn.Elevated)" | Tee-Object $report -Append
# The checklist items to verify are supplied with the task (see the calling prompt).
# Read that module's checklist file and iterate its items (see Step 6 — Verifier loop).
```
## Step 2 — Drive techniques
Every checklist item boils down to ONE of three intents. **Pick the bucket from the verb in the item, then use the best technique inside it.** Stop at the first technique that works.
| Intent | Verb-cues in the checklist item | Bucket |
|---|---|---|
| Change a setting | "default is X", "setting persists", "is enabled/disabled by default", "value Y is accepted" | §2.A |
| Interact with a UI element | "click X", "toggle X", "type into Y", "X button is visible", "selecting Z does W" | §2.B |
| Trigger a module action | "pressing hotkey X opens Y", "module launches", "Z happens when invoked" | §2.C |
### §2.A — Change a setting (single technique)
Edit the JSON file the module reads, wait for the file-watcher debounce, assert, then restore from backup. Zero external tools.
```powershell
$bk = Backup-PtModuleSettings -ModuleDir AdvancedPaste
try {
$j = Get-PtModuleSettings -ModuleDir AdvancedPaste
$j.properties.IsAdvancedAIEnabled.value = $false
$j | ConvertTo-Json -Depth 12 | Set-Content "$env:LOCALAPPDATA\Microsoft\PowerToys\AdvancedPaste\settings.json"
Start-Sleep -Seconds 4 # debounce — runner re-reads via file-watcher
# ... assertion ...
} finally {
Restore-PtModuleSettings -ModuleDir AdvancedPaste -BackupPath $bk
}
```
> For shell-extension modules (PowerRename, File Locksmith, Image Resizer, New+) edit the **module-owned** file under `%LOCALAPPDATA%\Microsoft\PowerToys\<Module>\`, then `Restart-PtRunner` (and on stubborn handlers, restart Explorer). See pitfall #18 below.
>
> If you need to flip the *enabled* bit for a whole module, debounce isn't enough — call `Restart-PtRunner` after the write.
### §2.B — Interact with a UI element (2 techniques, most-reliable first)
#### B1. UIA invoke / set-value — **always try first**
```powershell
winapp ui invoke 'SubmitButton' -a PowerToys.Settings
winapp ui set-value 'QueryTextBox' '=2+3*4' -a PowerToys.PowerLauncher
Start-Sleep -Milliseconds 600
winapp ui inspect -a PowerToys.PowerLauncher --depth 7 -i 2>$null
```
Invoke goes through UIA InvokePattern COM IPC — no foreground steal, no UIPI. See references/winapp-ui-testing.md §CRITICAL — invoke vs click.
#### B2. PostMessage WM_KEYDOWN/CHAR — when UIA can't reach the target
For elevated targets, AppX windows with stunted UIA trees, or keystrokes that UIA `set-value` can't dispatch (arrow-key ListView nav, Enter to commit). See references/winapp-ui-testing.md §CRITICAL — Keystroke input that bypasses UIPI (PostMessage). Esc is often filtered by WinUI 3 raw-input hook — use BackButton invoke instead.
### §2.C — Trigger a module action (2 techniques, most-reliable first)
| | C1 Named Event | C2 SendInput chord |
|---|---|---|
| **Proves** | The action fires (the path *downstream* of the hotkey). **Not** that the chord is bound. | The full path: real keys → runner hook → action. The **only** method that proves the chord binding itself. |
| **Robustness** | Highest — no foreground, no input desktop, UIPI-immune; works headless / RDP-minimized. | Lowest — needs an attached input desktop (else `BLK-ENV`), steals foreground, can't inject OS-reserved chords (Win+L / Win+Tab). |
| **Precondition** | Owning module process is running (the event only exists while it is). | Attached input desktop + foreground. |
**Pick by what the item asserts:** for "does action Y happen" use C1; for "pressing chord X triggers Y" or "the rebind takes effect", C1 is insufficient (it bypasses the chord) — use C2, or C1 *plus* a runner-log line proving the chord was accepted.
#### C1. Named Event signal — preferred
```powershell
Invoke-PtSharedEvent -Name 'CmdPal.Show' # opens CmdPal without keyboard
Invoke-PtSharedEvent -Name 'AOT.Pin' # pins foreground window via AOT
Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke' # opens PT Run
Invoke-PtSharedEvent -Name 'LightSwitch.Toggle' # toggles theme
Get-PtSharedEventCatalog | Format-Table # full list
```
No synthetic input — it's a `SetEvent` on the kernel event the module waits on, the same downstream path the runner's hotkey handler signals. Verify the side effect via UIA (`winapp ui list-windows -a <module>`), a log line (`Get-PtRunnerLogTail`), or settings.json diff (`Get-PtModuleSettings`). The event only exists while the owning process runs, so `Test-PtSharedEvent` doubles as an "is the module alive" check.
#### C2. SendInput chord — last resort / chord-binding verification
Real synthetic keys. Loud (steals foreground) and fragile, but the only way to prove the activation chord is actually bound. The runner's global keyboard hook catches the chord regardless of focus, so the precondition is just an **attached input desktop** (pitfall #13; on a detached desktop `SendInput` returns `ACCESS_DENIED` and the keys vanish → mark `BLK-ENV`).
```powershell
# Precondition: input desktop attached? 0 = detached → don't bother sending, mark BLK-ENV (pitfall #13)
if ([PtFg]::GetForegroundWindow() -eq [IntPtr]::Zero) { throw 'No input desktop — BLK-ENV (pitfall #13)' }
Send-PtChord -Mods 0x5B,0x10 -Key 0x43 # Win+Shift+C → Color Picker (cb=40 fix is inside the helper)
$line = Wait-PtHotkeyAccepted -ModuleHint 'Color' -TimeoutSec 3
if (-not $line) { throw 'Runner did not log hotkey invocation' }
```
> **Rare fallback — a module that uses its own `RegisterHotKey` and exposes no Named Event.** Post `WM_HOTKEY` (`0x312`) straight to its message window (find the HWND via `EnumWindows`+`GetClassName` through `Add-Type` — same P/Invoke pattern as `pt-foreground-guard.ps1`). **No current PT module needs this:** ZoomIt — the obvious candidate — also waits on Named Events (`ZoomIt.Zoom`, `ZoomIt.Draw`, …; source: `Zoomit.cpp` `CreateEventW(ZOOMIT_ZOOM_EVENT)`), so drive it with C1.
> **Different case — sending keys *into* a specific focused window** (e.g. a CmdPal alias like `=` / `<` / `>` that `winapp ui set-value` can't trigger because it bypasses TextChanged; see pitfalls #4 and #6). Here the keystrokes go to whatever currently has focus, so you must bring the target window foreground first:
> ```powershell
> Assert-PtForegroundOrAbort -AppId Microsoft.CmdPal.UI # -AppId = the window you're typing INTO
> Send-PtChord -Key 0xBB # '=' (no modifiers) to trigger the calculator alias
> ```
> The `-AppId` is whatever window you're targeting — it's **not** CmdPal-specific. CmdPal is just the worst offender: its AppX foreground-lock drops focus after the first `SetForegroundWindow`, so without the guard the keys silently leak to your terminal.
> Verdict decisions (PASS if behavior matches spec; **FAIL** if the product is wrong *or* the checklist item is stale/ambiguous; BLOCKED if you couldn't run the check after ≥2 entry-paths) live in **Step 3 — Classification taxonomy** below. Don't put verdict logic in Step 2.
## Step 3 — Classification taxonomy
### Verdicts (assign exactly ONE per item)
| Verdict | Meaning |
|---|---|
| **PASS** | You drove/observed the behavior and it matched the spec. **A pass is a pass — there is no PASS sub-type.** Record *how* you verified in the item's **Category** field as free text, e.g. "full UIA flow + asserted popup", "settings.json round-trip", "runner-log line", "Shell COM / IExplorerCommand", "screenshot pixel-diff", "output matches fixture", "process spawn/exit", "module CLI", "admin GPO write". |
| **FAIL** | The item is **red** — something is wrong and action is required. Treat the checklist as test code: a test fails because **the product is wrong** *or* **the test/checklist is wrong**. Record the **cause** in the **Category** field: <br>• **product** — behavior contradicts a valid spec → file a product bug (repro + expected-vs-actual + screenshot/log + build version). <br>• **checklist** — the item itself is broken: *stale* (feature was removed/deprecated — cite the source grep proving it's gone) or *ambiguous* (`[CLARITY: VAGUE-*]`, no definable pass/fail criterion — quote the original wording). Fix the checklist, not the product. |
| **BLOCKED** | Couldn't run the check in this environment / with this toolset *after ≥2 entry-paths* — inconclusive, like a skipped test. **Not red against the product.** Tag exactly one concrete reason below. |
### BLOCKED reasons
Different failure reasons stay distinct because each drives a different remediation.
| Reason | When |
|---|---|
| `BLK-ENV` | This specific shell can't drive it (non-interactive / Session 0, RDP-minimized, missing Explorer windows) but a normal interactive desktop CAN. Triggers a "re-run on an interactive desktop" recommendation. Cite `references/environment-setup.md`. |
| `BLK-HARDWARE` | Needs hardware this session lacks — multi-monitor, 2 physical PCs (MWB), real camera / battery / game-mode, or live screen/device capture. State the specific shortfall in **Category**. |
| `BLK-DRAG-REQUIRED` | Needs a real mouse-drag gesture; synthetic drag is insufficient (e.g. FancyZones snap). |
| `BLK-DESTRUCTIVE` | Reboot, hibernate, install/uninstall, or mid-session AppX uninstall — would damage the run environment. |
| `BLK-VISUAL-RENDER` | The thing to verify is a rendered surface UIA can't see — WinUI3 islands, WebView2, or Explorer-side context-menu rendering/localization. Needs pixel/OCR or a manual eyeball. |
| `BLK-OVERLAY-INPUT-BLOCK` | Overlay both blocks input and excludes itself from capture (`BlockInput` + `WDA_EXCLUDEFROMCAPTURE`, e.g. ZoomIt draw mode) — can neither drive nor screenshot it. |
| `BLK-EXTERNAL-APP` | Needs a 3rd-party tool, a real API key, or a system locale change. |
**Rule of thumb**: in your report, separate the two FAIL causes — *product* FAILs are bugs to file; *checklist* FAILs are items to rewrite or prune. `BLOCKED` is only for a concrete, named obstacle (cite it), never a substitute for effort. If a large share of a module's items are checklist-FAILs, the checklist needs an overhaul before re-verifying.
## Step 4 — Report format
**See `references/reporting-format.md` for the full template** (per-item table, summary, step-table rules, anti-patterns, worked example). Don't paraphrase; copy the templates literally. This includes a mandatory **§G Retrospective** — a self-reflection on the *run itself*: list every friction encountered (classified by source — `SKILL-UNCLEAR` / `WINAPP-TOOL-BUG` / `WINAPP-DOC-UNCLEAR` / `HELPER-FLAW` / `PT-PRODUCT` / `ENVIRONMENT` — with severity + minutes/attempts cost + a suggested fix), or write `Everything was smooth — no friction encountered.` if there was none. This is how the skill improves run over run, so don't skip it.
## Step 5 — State hygiene (CRITICAL)
**See `references/pre-flight.md` §State hygiene** for the backup/restore pattern and cleanup commands. Always wrap mutations in `try { ... } finally { Restore-* }`.
## Module-specific quick reference
**Look for `references/modules/<module>.md` FIRST.** Each per-module profile contains paths, entry-paths, item-by-item recipes, common BLOCKED traps, fixture lists, and source citations specific to that module.
Catalog: see `references/modules/README.md`. Currently authored: `peek.md`, `power-rename.md`, `file-locksmith.md`, `image-resizer.md`.
If your module has NO profile yet:
1. Fall back to the generic drive-stack in §2 above.
2. **For Explorer-context-menu modules** (PowerRename / File Locksmith / Image Resizer / New+ / Preview Pane / RegistryPreview): read **`references/explorer-context-menu-flow.md`** first — it has the synthetic-right-click + UIA-invoke pattern with stability rules and module-caption table. Per-module profiles cite it and only document module-specific quirks. The canonical helper is `scripts/pt-explorer-contextmenu.ps1`.
3. After finishing the verification, **create the profile** using the template in `references/modules/README.md` so the next agent benefits from what you learned.
Quick one-liners for modules without dedicated profiles (will be moved to per-module files as they're authored):
- **Advanced Paste**: `Invoke-PtSharedEvent -Name 'AdvancedPaste.ShowUI'` + `Set-PtClipboardRich` + `Compare-PtClipboardFormatDiff` (see `scripts/pt-clipboard-diff.ps1`).
- **Command Palette**: `Invoke-PtSharedEvent -Name 'CmdPal.Show'` + `Invoke-CmdPalQuery` (auto-handles degraded state via `scripts/pt-cmdpal-recycle.ps1`). Settings file via `Get-CmdPalSettings`.
- **PowerToys Run**: `Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke'` + `winapp ui set-value QueryTextBox`. Window has 2 HWNDs — filter by width ≥ 800.
- **FancyZones**: `Invoke-PtSharedEvent -Name 'FancyZones.ToggleEditor'`. Snap-drag tests are usually `BLK-DRAG-REQUIRED`; settings verify via settings.json round-trip.
- **Light Switch**: `Invoke-PtSharedEvent -Name 'LightSwitch.Toggle' | LightSwitch.Light | LightSwitch.Dark`. Verify via `HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme`.
- **Always on Top**: `Invoke-PtSharedEvent -Name 'AOT.Pin'`. Verify `WS_EX_TOPMOST` on pinned HWND.
- **Hosts File Editor** (admin): `Invoke-PtSharedEvent -Name 'Hosts.Show' | Hosts.ShowAdmin`.
- **GPO** (admin): write `HKLM:\Software\Policies\PowerToys` + `Restart-PtRunner` + `Get-PtRunnerLogTail -Pattern 'GPO sets'`. Cleanup: `Remove-Item HKLM:\Software\Policies\PowerToys -Recurse -Force`.
- **Mouse Without Borders**: most items `BLK-HARDWARE` (need 2 physical PCs).
- **ZoomIt**: most modes inside `BlockInput + WDA_EXCLUDEFROMCAPTURE` overlay → `BLK-OVERLAY-INPUT-BLOCK`. Mode triggers: `ZoomIt.Zoom`, `ZoomIt.Draw`, `ZoomIt.Break`, etc.
- **Peek**: see `references/modules/peek.md` for the full recipe (CLI back-door + Shell.Application + Ctrl+Space).
## Step 6 — Verifier loop per checkbox
```
For each item in module:
1. Pick a bucket from the verb in the item (§2.A change a setting / §2.B interact with UI / §2.C trigger an action)
2. Walk that bucket's techniques top-to-bottom; stop at the first one that drives the item
3. Compare observed behavior to the spec:
• matches the spec → PASS (note the method in Category)
• product behaves wrong → FAIL, cause=product (repro + expected/actual + screenshot/log + build)
4. Checklist item itself is broken — feature removed from source, or spec too ambiguous to judge → FAIL, cause=checklist (cite the source proof / quote the wording)
5. Couldn't drive it after ≥2 entry-paths → BLOCKED with a concrete reason (§3)
6. Record verdict + evidence + cleanup
7. Next item
```
When done, run state hygiene cleanup, write the report **including the §G retrospective**, archive the workspace (Step 7), and exit.
## Step 7 — Archive the workspace to the sign-off folder (do this LAST)
The live run works out of `%TEMP%`, but the **final deliverable must live in the module sign-off archive** so reports persist and sync via OneDrive:
```powershell
# After the report is written AND the artifact-existence check passes:
$signoff = "$env:OneDrive\PowerToys\Module-Signoff" # e.g. C:\Users\<you>\OneDrive - Microsoft\PowerToys\Module-Signoff
New-Item -ItemType Directory -Path $signoff -Force | Out-Null
$final = Join-Path $signoff (Split-Path $workspace -Leaf)
Move-Item -Path $workspace -Destination $final -Force
# Report uses RELATIVE artifacts/… paths, so all links stay valid after the move.
Write-Host "Final report: $(Join-Path $final (Split-Path $report -Leaf))"
```
Print the **moved** report path (under `…\PowerToys\Module-Signoff\`) as the last line — never the `%TEMP%` path.
## Invocation & placeholders
This skill auto-activates when you ask to verify a PowerToys module's checklist (e.g. "verify all Color Picker items"). **One module per run** — never chain multiple modules into one report. Resolve these placeholders for the module under test:
| Placeholder | Substitute with |
|---|---|
| `<Module>` | Exact display name, e.g. `Color Picker`, `Command Palette`, `PowerToys Run`, `FancyZones` (see `references/release-checklist/index.md`). |
| `<module>` | Lowercase-kebab-case for file lookup, e.g. `color-picker`, `command-palette`, `power-rename` — used for BOTH `references/release-checklist/<module>.md` (checklist) and `references/modules/<module>.md` (profile, if any). |
| `<ModuleDir>` | settings.json sub-dir under `%LOCALAPPDATA%\Microsoft\PowerToys\` (e.g. `AdvancedPaste`, `FancyZones`, `PowerToys Run` (with space)). |
| `<N>` | Total item count for this module. |
**Execution order:** `references/pre-flight.md` → per item, the §2 drive-stack (this file) → `references/reporting-format.md` per-item table → Step 6 verifier loop → `references/pre-flight.md` §Final wrap-up → Step 7 archive → print the final report path.
## What NOT to do
- Do NOT chain multiple modules in one report — one module per run.
- Do NOT mark an item BLOCKED without a concrete, named obstacle (see §3 and `references/pre-flight.md` §Hard rules).
- Do NOT invent steps for a VAGUE checklist item — if the spec is too ambiguous to judge, that is FAIL (cause=checklist), not a guess.
- All other rules (foreground guard, always restore mutated state, etc.) live in `references/pre-flight.md` §Hard rules — follow them.
## Critical pitfalls (PT-specific)
*Reference, not a sequential step — skim before you start and consult while driving. Numbered for cross-reference only.*
1. **PT runner does NOT auto-pickup edits to master `settings.json`** (top-level `enabled.<Module>` flags). Call `Restart-PtRunner`.
2. **Each module's own `<Module>\settings.json` IS hot-reloaded** via per-module file watcher (~3s debounce). **EXCEPTION — shell-extension/context-menu modules do NOT read this file; see pitfall #18.**
3. **PT Run setting key has a space**: `"PowerToys Run"` not `PowerToysRun`.
4. **CmdPal AppX foreground from external CLI is unreliable** — Windows foreground-lock blocks `SetForegroundWindow` after the first call. SendInput keys silently leak to your terminal. **Always `Assert-PtForegroundOrAbort` before SendInput.**
5. **CmdPal AppX enters TextChanged-broken state** every ~30 probes — `Test-CmdPalDegraded` + `Reset-CmdPalAppX` to recover.
6. **CmdPal alias detection (`=`, `<`, `>`, `:`, `$`, `??`, `)`) requires real keystrokes**`winapp ui set-value` bypasses TextChanged and the alias never fires. Use Send-PtChord + `Assert-PtForegroundOrAbort` for aliases; use set-value for plain queries.
7. **CmdPal Esc handler is filtered** by WinUI 3 raw-input hook — use `winapp ui invoke BackButton` instead (see `Reset-CmdPalToHome`).
8. **GPO HKLM vs HKCU**: HKLM wins when both are set with conflicting values.
9. **HKLM `Software\Policies\PowerToys` writes require admin** — verify with `Test-PtAdmin`.
10. **`Stop-Process` is policy-blocked in this session unless you pass `-Id <int>` literally**. Always inline the PID.
11. **WinUI 3 islands are largely invisible to UIA** (QuickAccess flyout, RegistryPreview Monaco editor, Peek WebView2). For these, fall back to screenshot + OCR or settings.json diff.
12. **OS-reserved chords (Win+L, Win+Tab)** are consumed by Windows before any hook and cannot be injected via SendInput at all.
13. **RDP minimized = `SendInput` denied.** Even though `quser` shows the remote session State=Active, minimizing the mstsc client detaches the session's input desktop. `GetForegroundWindow()` returns 0; `SendInput` returns `ACCESS_DENIED (5)`; tests that need synthetic input fail. **Same applies to: closed mstsc with X (Disconnected), local PC sleep (RDP TCP drops), remote screensaver/workstation lock, remote machine sleep.** Run `scripts/pt-session-diagnose.ps1` in pre-flight to detect, and see `references/environment-setup.md` for the full per-scenario table + `powercfg` setup commands the user should run before starting the agent. The agent should call `Test-PtForeground` mid-run before each input-injection-dependent item; if it returns False, mark `BLK-ENV` with mitigation citation (an environment block — not a product FAIL).
14. **`winapp ui` arg-order quirk**: `winapp ui inspect --depth N -w $hwnd` may intermittently fail to parse `--depth` as Int64 if `-w` precedes it. **Put `-w $hwnd` AFTER `--depth N`** or as the first arg before any flag. If you see "Cannot bind argument" or numeric parse errors, swap the order and retry.
15. **`winapp ui list-windows` line wrapping**: when window titles or process names are long, output may wrap a single window's `HWND <id>: "<title>" ... (proc, PID N)` across multiple lines, breaking single-line regexes. Either pipe through `Out-String` and use a multi-line regex, or use `--json` (when supported) and parse structured output.
16. **De-elevation: launching a NON-elevated (Medium IL) child from an elevated agent shell.** The drive-stack only covers gaining *more* privilege; some items need the opposite. From a High-IL shell you cannot `Start-Process` a Medium-IL child directly. Use `scripts/pt-nonelevated.ps1` (`Start-PtNonElevated` / `Invoke-PtNonElevatedCapture`) — a one-shot `RunLevel Limited` + `LogonType Interactive` scheduled task that lands on the user's desktop at their filtered token. Confirm with `Test-ProcessElevated`. Needed for elevation-visibility pairs (File Locksmith L649/L650: non-elevated FL must not see the elevated runner; elevated FL must).
17. **Win11 packaged context menus are not observable without real Explorer.** Modern PT context-menu entries are packaged `IExplorerCommand`s (sparse MSIX, e.g. File Locksmith CLSID `{AAF1E27D-…}`). They are **NOT** enumerable via classic `Shell.Application … FolderItem.Verbs()` and **NOT** `CoCreate`-able from a non-Explorer host (`REGDB_E_CLASSNOTREGISTERED`). So "verify the entry appears / no longer appears" cannot be pixel-verified by API. Verify instead via the gate flag the entry's `GetState` reads (e.g. general `enabled.<Module>`) + a source citation that maps it to `ECS_HIDDEN`; treat the literal render as `BLK-VISUAL-RENDER` and recommend a 5-second manual right-click. (Disabling does NOT unregister the package — it stays `Status Ok`; the entry is hidden dynamically.)
18. **Shell-extension modules read a module-OWNED settings file, NOT the PT-store `<Module>\settings.json`.** PowerRename, File Locksmith, Image Resizer, and New+ context-menu handlers and exes run *outside* the runner (hosted by Explorer / launched on demand) and cannot use the PT-Settings IPC. Each reads its **own** json in `%LOCALAPPDATA%\Microsoft\PowerToys\<Module>\` *at process/handler launch* (registry-migrated `CSettings`/`Settings` classes — `lib/Settings.cpp` `Load→ParseJson`). The PT-Settings UI writes the *PT-store* `settings.json` (the `bool_*`/`int_*` file `Get-PtModuleSettings` reads); the runner's module DLL syncs PT-store→module-store **only on a Settings-UI change event** — so the PT-store file can be **stale for days** and editing it has **no effect** on the running shell handler. **To drive a settings item on these modules, edit the module-owned file directly (drive-stack §2.A) and relaunch the module (or restart runner+Explorer for the menu handlers), then restore.**
**Pitfall #18 — module-owned files + their key style** (verified 2026-06-10 against `<PT-repo>\src`):
| Module | Module-owned file (under `…\PowerToys\<Module>\`) | Key style | PT-store `settings.json` keys (UI/`Get-PtModuleSettings`) |
|---|---|---|---|
| PowerRename | `power-rename-settings.json` (+ `power-rename-last-run-data.json`, `search-mru.json`, `replace-mru.json`) | `ShowIcon`, `ExtendedContextMenuOnly`, `PersistState`, `MRUEnabled`, `MaxMRUSize`, `UseBoostLib` | `bool_show_icon_on_menu`, `bool_show_extended_menu`, `bool_persist_input`, `bool_mru_enabled`, `int_max_mru_size`, `bool_use_boost_lib` |
| File Locksmith | `file-locksmith-settings.json` | `ShowInExtendedContextMenu` | `bool_show_extended_context_menu` |
| Image Resizer | `image-resizer-settings.json` | (resize sizes/encoder/etc.) | mirrored `imageresizer*` keys |
| New+ | `NewPlus\settings.json` (sub-folder **`NewPlus`**, verified on disk + `constants.h` `powertoy_name=L"NewPlus"`) | `HideFileExtension`, `HideStartingDigits`, `TemplateLocation`, `ReplaceVariables`, `BuiltInNewHidePreference` | mirrored `newplus*` keys |
Confirm which file actually drives behavior with a quick A/B: edit the module-owned file → relaunch → observe; if behavior follows, that's the source of truth (PowerRename L394/L395/L396/L397/L409 were all driven this way).
If you find another gap during verification, update this skill (add a recipe) AND consider proposing the addition to references/winapp-ui-testing.md if it's generic enough.

View File

@@ -0,0 +1,143 @@
# Environment setup for PowerToys verification
**Audience**: human user preparing a test machine before running a verification agent.
**One-time** (per test session) — restore afterward.
## Why this matters
PowerToys release checklists test real user interactions: pressing hotkeys, dragging files, switching windows. Many tests use `SendInput` to inject keystrokes. Windows refuses `SendInput` when the calling session has **no attached input desktop** — and several common Windows states cause exactly that to happen:
- RDP client minimized
- Workstation locked (screensaver kicked in, idle timeout)
- Remote machine asleep
- Local machine asleep (RDP TCP drops)
If any of these happens mid-verification, items that need synthetic input fail with `BLK-ENV` even though the feature itself works fine. This guide eliminates the env causes so the only BLOCKED verdicts you see are real test/framework limitations.
## Per-scenario reference table
| Scenario | Remote session State | `GetForegroundWindow()` | `SendInput` | Verdict for input-injection tests |
|---|---|---|---|---|
| mstsc window focused | Active | Real HWND | Works | ✅ Drivable |
| mstsc visible but not focused (covered or alt-tabbed) | Active | Real HWND | Works | ✅ Drivable |
| **mstsc MINIMIZED** | Active | **0** | **ACCESS_DENIED (5)** | ❌ BLK-ENV |
| Local machine sleeps / RDP TCP drops | **Disconnected** | 0 | ACCESS_DENIED | ❌ BLK-ENV |
| User closes mstsc with X (no signout) | **Disconnected** | 0 | ACCESS_DENIED | ❌ BLK-ENV |
| Sign out from the remote | Session destroyed | — | — | ❌ Agent killed |
| Remote machine sleeps | Suspended | — | — | ❌ Catastrophic — timing corruption |
| Remote screensaver / auto-lock kicks in | Active but desktop locked | 0 | ACCESS_DENIED | ❌ BLK-ENV |
| **2nd RDP login as the SAME user** (you reconnect from another client) | the OLD session flips to **Disconnected** | 0 (in the old session) | ACCESS_DENIED | ❌ BLK-ENV — your running test's session got taken over |
**Key insight**: "Active" in `quser` ≠ "can inject input". Always check `GetForegroundWindow()` first (the diagnostic script `scripts/pt-session-diagnose.ps1` does this).
## Can I verify two modules at once in two RDP sessions?
Short answer on a **client edition of Windows (Windows 10/11, ProductType=1)**: **no — not as the same user, and effectively not at all.** This was investigated live on this machine (Windows 11 Enterprise, build 26200, `fSingleSessionPerUser=1` default):
- **Two monitors ≠ two sessions.** A multi-monitor setup is **one** session spanning both screens — it shares a single input desktop, foreground window, and `SendInput` queue across the monitors. Monitor count has nothing to do with session count, so "I have two monitors" does not give you two sessions to run two modules in.
- **Sessions are isolated** — each Windows session has its own input desktop, its own foreground window, and its own `SendInput` queue. So *typing in session B genuinely does NOT disturb session A's foreground or input.* Cross-session interference is **not** the problem (so if you somehow DID have two live sessions — Server/RDS — they could run in parallel without colliding).
- **The real blocker is session takeover.** Client Windows allows only **one interactive (console/owning) session at a time**, and `fSingleSessionPerUser=1` (the default) means one user gets **one** session. When you open the *second* RDP connection (as the same user), Windows **disconnects the first session** — it flips to `Disconnected`, its input desktop detaches, `GetForegroundWindow()` → 0, and any in-flight UI test there fails with `ACCESS_DENIED` → BLK-ENV. It's not your *typing* that breaks the test; it's the act of logging in the second session that evicts the first.
- A different *user* account doesn't rescue it either: client Windows still permits only one connected interactive session, so the second login still disconnects the first.
- Therefore, on client Windows, **run modules serially in one session.** True concurrent multi-session needs Windows Server + the RDS (Remote Desktop Session Host) role; unofficial multi-session patches exist but are out of scope here.
> **Verdict on the common assumption "I can run two modules in two RDP sessions because I have two monitors":** the *conclusion* (can't run two at once on client Windows) is correct, but the *reasoning* is wrong on two counts — two monitors is still one session, and you can't get two simultaneously-Active sessions on client Windows at all (the 2nd login disconnects the 1st). The limit is "can't open a 2nd Active session", not "the two sessions fight each other".
**Practical guidance:** keep a single RDP session for the whole run; don't reconnect/relogin mid-run; if you must check something elsewhere, alt-tab inside the *same* session rather than opening a new RDP connection. To detect a takeover after the fact, `qwinsta` will show your former session as `Disconnected`.
## Pre-run setup checklist
Run these BEFORE starting the verification agent.
### On the test machine (the one being verified)
```powershell
# Snapshot current power settings so you can restore after
$bk = "$env:TEMP\powercfg-backup-$(Get-Date -f yyyyMMdd-HHmmss).txt"
powercfg /query SCHEME_CURRENT SUB_SLEEP > $bk
powercfg /query SCHEME_CURRENT SUB_VIDEO >> $bk
"# Restore later with the values from $bk" | Set-Content "$bk.note"
# Disable sleep + display-off + hibernate (AC and battery)
powercfg /change standby-timeout-ac 0
powercfg /change standby-timeout-dc 0
powercfg /change monitor-timeout-ac 0
powercfg /change monitor-timeout-dc 0
powercfg /change hibernate-timeout-ac 0
powercfg /change hibernate-timeout-dc 0
# Disable screensaver
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '0'
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '0'
# Disable workstation lock-on-idle (requires admin)
# 0 = never lock. Restore your original value (commonly 600 = 10 min) afterward.
$origLock = (Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name InactivityTimeoutSecs -EA SilentlyContinue).InactivityTimeoutSecs
"$origLock" | Out-File "$bk.lock"
Set-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name InactivityTimeoutSecs -Value 0 -EA SilentlyContinue
# Confirm
powercfg /query SCHEME_CURRENT SUB_SLEEP | Select-String 'Power Setting GUID|Current AC Power Setting Index'
```
### On the local machine (the one with the RDP client)
```powershell
# Disable local sleep so RDP TCP stays alive
powercfg /change standby-timeout-ac 0
# Practical habit: put mstsc on a monitor you're NOT actively working on.
# Don't minimize. Alt-tab is fine; minimize is not.
```
## Mid-run discipline
While the agent is running:
- **Don't minimize mstsc.** Visible-but-unfocused is OK; minimized is not.
- **Don't close mstsc with the X.** If you have to step away, fine — leave it open.
- **Don't disconnect or reconnect RDP.** Stay continuously connected for the duration of the run.
- **Don't sign out** on either end.
- If you do step away and the screen locks (despite the setup above), reconnect/unlock and the agent's `Test-PtSessionStillInteractive` guard (if used) will resume; otherwise items mid-execution will be BLK-ENV.
## Post-run cleanup (restore)
```powershell
# Restore the values you captured to $bk before starting
# (e.g. typical defaults: standby 30min, monitor 15min, screensaver 600s, lock 600s)
powercfg /change standby-timeout-ac 30
powercfg /change standby-timeout-dc 15
powercfg /change monitor-timeout-ac 15
powercfg /change monitor-timeout-dc 10
powercfg /change hibernate-timeout-ac 0 # often default
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '1'
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '600'
$origLock = Get-Content "$bk.lock" -EA SilentlyContinue
if ($origLock) {
Set-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' `
-Name InactivityTimeoutSecs -Value ([int]$origLock) -EA SilentlyContinue
}
```
(Values above are typical; adjust to your environment policy.)
## Diagnostic before you start
Run `scripts/pt-session-diagnose.ps1` from the agent shell. Expected output for a GO:
```
PASS - this shell can drive interactive PowerToys tests.
```
If it prints FAIL with a `psexec -i <consoleSession> -s pwsh.exe` hint, you're in a non-console session — relaunch the agent shell as suggested before starting verification.
## Why this isn't in the global SKILL.md
These are **human prep steps**, not agent instructions. The agent needs to *detect* a bad environment (via `Test-PtInteractiveDesktop` in pre-flight + `Test-PtSessionStillInteractive` mid-run); the user needs to *prevent* one. Different audiences, different docs.
## Related
- `scripts/pt-session-diagnose.ps1` — one-shot session diagnostic
- `scripts/pt-foreground-guard.ps1``Test-PtForeground` / `Force-PtForeground` / `Assert-PtForegroundOrAbort` used by agent
- `SKILL.md` pitfall #13 — short pointer to this doc
- `references/pre-flight.md` pre-flight check #4 — agent reads this doc when it detects a bad env

View File

@@ -0,0 +1,99 @@
# Explorer context-menu flow — driving PowerToys shell-menu modules end-to-end
**Audience**: agents verifying any PowerToys module whose entry point is the **Windows Explorer right-click context menu** — i.e. **File Locksmith, Image Resizer, PowerRename, New+ (NewPlus)**, and similar.
This is the *true user flow*: open Explorer → select file(s) → right-click → click the module's menu item. Use it when an item's assertion is specifically about the **context menu** (e.g. "the entry appears / no longer appears", "right-click → X launches the module on the selection"). For the module's *internal* behavior you can still prefer a faster back-door (CLI / `last-run.log` / Named Event) — see each module profile — but the menu presence/launch itself can only be observed this way.
Helper: `scripts/pt-explorer-contextmenu.ps1` (`Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems`).
## Which approach first? (CLI / back-door vs synthetic menu)
**Pick the tool by what the item ASSERTS — not "always synthetic" or "always CLI".**
| The item asserts… | First approach | Why |
|---|---|---|
| **The menu itself** — entry *appears / no longer appears*, "right-click → select X", caption / localization of the entry | **Synthetic Explorer menu (this doc)** — the *only* valid observer | The CLI/back-door is **blind to the menu**: it runs even when the entry is correctly hidden, so it gives a false PASS (the L652 trap). If the desktop is locked → `BLK-ENV`; do **not** substitute the CLI. |
| **Module behavior** — engine finds the lockers, images get resized, files get renamed (the menu is just the trigger) | **CLI / back-door** (`FileLocksmithCLI.exe`, `last-run.log`, Named Event, DSC) | Instant, deterministic, foreground-free, works on a locked desktop. Synthetic adds ~10s + foreground/retry fragility without changing the assertion. |
**Golden-path rule (do once per module):** run **one** full synthetic right-click → invoke-the-item → confirm-launch. That proves the menu→launch wiring is actually registered *and* validates that the fast back-door is behaviorally equivalent to the real menu (e.g. File Locksmith L641 `step-04/05` did exactly this). After that one golden run, trust the back-door for the remaining behavior items.
Net: for a context-menu module, **most items are behavior → CLI-first**; the **menu-presence/absence/launch/localization items → synthetic-first**; plus one golden-path synthetic launch.
## Is it stable?
**Yes — with the robust variant below.** Verified repeatedly on Win11 (2026-06-08) launching File Locksmith via a genuine right-click + menu click. Two rules make it reliable; ignore them and it gets flaky:
1. **Invoke the menu item by UIA InvokePattern, not a coordinate left-click.** The menu item exposes `InvokePattern` (`isInvokable=True`). `winapp ui invoke <selector> -w <menuHwnd>` is robust and needs no foreground/coordinates for the *click*. A synthetic left-click at the item's pixel center also works but is the fragile part (DPI, menu repositioning near screen edges, scrolled menus).
2. **The right-click that OPENS the menu still needs synthetic input on a foregrounded window — and occasionally a retry.** The first right-click right after Explorer opens sometimes misses (foreground not settled). `Open-PtExplorerContextMenu` retries up to 3×; that removed the flakiness in testing.
**Hard prerequisite — unlocked interactive desktop.** Synthetic right-click injects into the session input stream, so it requires foreground. If the workstation is locked / RDP minimized (`GetForegroundWindow()=0`), this flow is `BLK-ENV` — there is no foreground-free way to open a context menu. `Open-PtExplorerContextMenu` throws a clear BLK-ENV error in that case. (A 4-hour idle auto-lock is the common culprit — see `references/environment-setup.md`.)
**Other constraints:**
- **Settings for these modules live in a module-OWNED file, not the PT-store `settings.json`** — see `SKILL.md` pitfall #18. The context-menu handler reads e.g. `power-rename-settings.json` / `file-locksmith-settings.json` / `image-resizer-settings.json` / `New\settings.json` at launch; editing the PT-store `<Module>\settings.json` (what `Get-PtModuleSettings` reads) often has **no effect** on the live handler. Drive icon/extended-menu/feature toggles via the module-owned file + relaunch (restart runner+Explorer for the menu handlers), then restore.
- This is the **Win11 packaged** context menu (`Microsoft.UI.Content.PopupWindowSiteBridge` / "PopupHost"). The packaged module commands appear **only** here — not in classic `Shell.Application.Verbs()` and not via `CoCreate` of the command CLSID (`REGDB_E_CLASSNOTREGISTERED`). On Win10, or under "Show more options", you'd get the classic menu instead (different structure).
- The menu exists in the UIA tree **only while open** — you must open it with real input first; you can't enumerate it cold.
- A menu-launched module UI runs **non-elevated** (Explorer's integrity), even if your agent shell is elevated. Mind elevation-visibility (e.g. a non-elevated File Locksmith can't see higher-IL processes — match locker integrity with `scripts/pt-nonelevated.ps1`).
## Recipe (robust)
```powershell
. "$skill\scripts\pt-explorer-contextmenu.ps1"
# 0) Guard: must be an unlocked desktop
if (-not (Test-PtDesktopInteractive)) { <# mark BLK-ENV, cite references/environment-setup.md #> }
# 1) Open Explorer on the target folder and grab its CabinetWClass HWND
Start-Process explorer.exe $dir; Start-Sleep 4
$hwnd = (winapp ui list-windows --json | ConvertFrom-Json |
Where-Object { $_.className -eq 'CabinetWClass' -and $_.title -match [regex]::Escape((Split-Path $dir -Leaf)) } |
Select-Object -First 1).hwnd
# 2) Open the real context menu (synthetic right-click, auto-retry)
$menu = Open-PtExplorerContextMenu -ExplorerHwnd $hwnd -FileName 'target.txt'
# 3a) ASSERT PRESENCE / ABSENCE (e.g. "entry no longer appears" when the module is disabled)
$items = Get-PtContextMenuItems -MenuHwnd $menu # all visible MenuItem names
$present = $items -contains 'Unlock with File Locksmith'
# 3b) LAUNCH the module via the real menu (UIA invoke by NAME — robust)
$ok = Invoke-PtContextMenuItem -MenuHwnd $menu -ItemName 'Unlock with File Locksmith'
# 4) Verify the module launched (its process/window appears) — e.g.:
Start-Sleep 4
$ui = Get-Process PowerToys.FileLocksmithUI -EA SilentlyContinue # or PowerToys.ImageResizer, PowerToys.PowerRename
```
To **assert absence** after disabling a module: re-open the menu and check `Get-PtContextMenuItems` no longer contains the caption (the packaged `GetState` re-reads the enabled flag live, so no Explorer restart is needed between toggles).
## Multi-file selection (Image Resizer, PowerRename)
These operate on a **selection** of files. Select first (Shell COM is reliable and foreground-free), then right-click one of the selected items:
- Use `scripts/pt-explorer-com.ps1``Open-PtExplorerAtPath` + `Select-PtExplorerFiles` to establish the multi-select.
- Then `Open-PtExplorerContextMenu` on one selected file and `Invoke-PtContextMenuItem` — the module receives the whole selection (the shell handler enumerates all selected `IShellItem`s).
## Module captions (match by NAME)
Match the **visible caption**, not the AutomationId (Explorer assigns per-session numeric IDs like `32012` whose value/order varies). Discover the exact caption at runtime with `Get-PtContextMenuItems`. Verified captions:
| Module | Launched process | Menu caption (verified ✓ / expected) |
|---|---|---|
| File Locksmith | `PowerToys.FileLocksmithUI.exe` | ✓ `Unlock with File Locksmith` (NB: **not** the checklist's "What's using this file?") |
| PowerRename | `PowerToys.PowerRename.exe` | ✓ `Rename with PowerRename` |
| Image Resizer | `PowerToys.ImageResizer.exe` | `Resize images` (verify via `Get-PtContextMenuItems` — caption shifted across versions) |
| New+ | (creates from template) | `New+` (submenu) |
> Tip: if a module's caption is unknown, enable the module, open the menu on an applicable file, and run `Get-PtContextMenuItems` to read the exact string — then hard-match it for present/absent assertions.
## Common failure modes → fixes
| Symptom | Cause | Fix |
|---|---|---|
| `BLK-ENV: ... GetForegroundWindow()=0` | desktop locked / RDP minimized | unlock & keep mstsc un-minimized (`references/environment-setup.md`); mark `BLK-ENV`, not a test failure |
| "popup not found after N attempts" | foreground not settled (esp. first right-click after Explorer opens) | the helper already retries 3×; raise `-MaxTries`, or pre-foreground the window once before calling |
| menu item `invoke` returns but nothing launches | matched the wrong node / item disabled | match `type -eq 'MenuItem'` by exact Name; confirm the module is enabled |
| caption not found though module enabled | wrong/old caption string, or it's under "Show more options" (classic menu) | enumerate with `Get-PtContextMenuItems`; for classic menu invoke `expandtoclassic` first |
| launched UI shows nothing | menu-launched UI is non-elevated and can't see higher-IL targets | match target integrity (`scripts/pt-nonelevated.ps1`) |
## Referenced by
- `references/modules/file-locksmith.md` (L641/L652 — real right-click launch + menu present/absent)
- *(future)* `references/modules/image-resizer.md`, `references/modules/power-rename.md`, `references/modules/new-plus.md` — reference this doc for their context-menu items.

View File

@@ -0,0 +1,116 @@
# Per-module verification profiles (`references/modules/`)
This folder holds **one short profile per PowerToys module**. Each profile is self-contained guidance specific to that module — paths, entry-paths, capability/control recipes, common BLOCKED traps, fixture lists, source citations.
## When to read
When this skill runs for a specific module, check whether `references/modules/<module>.md` exists here. If yes: **read it BEFORE walking the SKILL.md drive-stack** — it tells you which entry-paths actually work for this module's quirks and which BLOCKED traps to avoid.
If no profile exists, fall back to SKILL.md + the helper scripts.
## Shared cross-module flows
Some flows are common to several modules and live in their own top-level docs (not per-module):
- **`../references/explorer-context-menu-flow.md`** — driving the real Win11 Explorer right-click context menu end-to-end (open + assert present/absent + launch). Referenced by File Locksmith and any future **Image Resizer / PowerRename / New+** profiles.
## Why per-module (not just one big SKILL.md)
- Each module has its own quirks (Peek's `_isFromCli` guard, CmdPal's TextChanged-broken state, PT Run's mini-popup HWND, Workspaces' snapshot-elevation rules). Bundling all of them into the global SKILL.md bloats context and forces every verification to load 25+ KB of mostly-irrelevant text.
- A profile lets a focused verification run with only the relevant 5-10 KB.
- New gotchas discovered during a module verification round get added to that module's profile, not the global one — keeps the global doc stable.
## Profile catalog
| Module | Profile | Status |
|---|---|---|
| Peek | `peek.md` | ✅ written 2026-06-08 |
| File Locksmith | `file-locksmith.md` | ✅ written 2026-06-08 |
| Image Resizer | `image-resizer.md` | ✅ written 2026-06-09 |
| PowerRename | `power-rename.md` | ✅ written 2026-06-10 (first to cite `../context-menu-cookbook.md` for shared mechanics) |
| New+ | `new-plus.md` | ✅ written 2026-06-18 (registration-gate for menu presence; Settings-UI toggle drives template auto-copy) |
| (other modules to be added as we encounter sign-off needs) | — | — |
## For Explorer-context-menu modules: read the canonical flow doc first
If you're writing a profile for a module that registers an entry in Explorer's Win11 right-click menu (PowerRename, File Locksmith, Image Resizer, New+, Preview Pane, RegistryPreview), **read `../references/explorer-context-menu-flow.md` first**. It has the canonical synthetic-right-click + UIA-invoke recipe with:
- Which-approach-first decision rule (CLI back-door vs synthetic menu, with the false-positive trap warning)
- Stability rules (UIA InvokePattern, retry on first right-click)
- Recipe (robust 5-step flow)
- Multi-file selection notes
- Module captions table (per-module menu-item display names)
- Common failure modes
- The unlocked-desktop requirement (BLK-ENV gating)
The shared helper is `scripts/pt-explorer-contextmenu.ps1` (`Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems`).
Your module profile then only documents the **module-specific** quirks: settings.json schema keys, expected verb caption regex, capability/control recipes, source citations, ceiling.
`power-rename.md` is the model — ~9 KB despite covering 18 items because the generic mechanics live in the canonical flow doc.
## Profile template
When writing a new profile, use this skeleton:
```markdown
# <Module> — module verification profile
**PT module**: `<ModuleKey>` (one-line description)
**Source**: `<PT-repo>\src\modules\<dir>\` (PT repo)
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\<dir>\settings.json`
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\<dir>\Logs\v<ver>\log_<date>.log`
**Exes**: `<full path>`
**Default hotkey**: `<keys>` (modifiers + code, plus path to ActivationShortcut in settings)
**Named Event**: `Local\<name>` (friendly name in pt-shared-events.ps1 catalog)
**DSC resource**: `Microsoft.PowerToys/<Name>Settings`
## Entry-paths (try in order)
### 1. <fastest path>
<powershell code + when to use + source citation>
### 2. <alternate path>
<...>
### 3. <last-resort path>
<...>
## Recipes — a control/observation map, NOT a per-test-case answer key
| # | Capability | Drive (control / settings key) | Observe (where the result shows) |
|---|---|---|---|
| 1 | <a module capability, e.g. "context-menu entry present when enabled"> | <which AutomationId / control / settings key drives it> | <where the result is visible: preview column, settings.json, disk, log, menu> |
| 2 | <next capability> | <...> | <...> |
> **Mapping process** (agent at runtime): read the actual checklist item → identify the capability → find its row → drive the named control and **design your own inputs + assertions for that item**. If no row matches, it's a NEW capability — drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
> **Why a map, not an answer key**: the table must carry only **durable module knowledge** — which control drives a capability and where to observe the result. Concrete Search/Replace inputs and expected-output assertions are *per-test-case answers*; baking them in turns the profile into a cheat sheet that (a) lets the agent copy answers without understanding and (b) goes stale the moment a checklist item changes its wording or values. Keep inputs + assertions OUT. Only a real UI redesign (a renamed/moved/removed control) should force an edit to this table.
## Common BLOCKED traps
<list of mistakes prior agents made + how to avoid them>
## Fixture files needed
<list of pre-canned files the verification expects>
## Source citations
<paths in PT repo that explain module behavior or guards>
## Ceiling
<observed PASS rate / total>
## Don'ts
<list of common mistakes>
```
## Hygiene
- **Keep each profile under ~10 KB.** If it grows beyond that, the module has too many quirks — escalate to maintainer review of the upstream checklist.
- **The recipe table is a control/observation MAP, not an answer key.** Columns are *Capability → Drive (control/key) → Observe*. **Do NOT bake in concrete Search/Replace inputs or expected-output assertions** — those are per-test-case answers that go stale when a checklist item changes and let the agent copy without understanding. The agent designs inputs + assertions at runtime from the actual checklist item.
- **Tables are capability-keyed, NOT line-keyed.** Upstream checklist line numbers (`L<n>`) **must not appear** in the profile — they drift between releases (items added/removed/reordered) and turn the table into a silent mismatch trap. PT-source-code file:line citations (e.g. `dllmain.cpp:73`) ARE allowed; they're version-pinned and serve a different purpose.
- **Cite source-code line numbers** where module behavior surprises (e.g. CLI guards, debounce timings, fallback chains). Reviewers can verify your claims by reading those lines.
- **Update the profile after every verification round**; promote any new technique into the right helper script if it generalizes beyond this module.

View File

@@ -0,0 +1,103 @@
# File Locksmith — module verification profile
**PT module**: `File Locksmith` (shows which processes are using selected files/dirs and lets you kill them)
**Source**: `<PT-repo>\src\modules\FileLocksmith\` (PT repo)
**Settings file (module)**: `%LOCALAPPDATA%\Microsoft\PowerToys\File Locksmith\settings.json` (`{"properties":{"bool_show_extended_menu":{...}}}`) and `file-locksmith-settings.json` (`{"showInExtendedContextMenu":bool}`)
**Enable flag**: `%LOCALAPPDATA%\Microsoft\PowerToys\settings.json``enabled."File Locksmith"` (general settings; runner-owned)
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\File Locksmith\FileLocksmithUI\Logs\…`
**Exes**: UI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.FileLocksmithUI.exe`; CLI = `%LOCALAPPDATA%\PowerToys\FileLocksmithCLI.exe`
**Context menu**: Win11 packaged `IExplorerCommand` CLSID `{AAF1E27D-4976-49C2-8895-AAFA743C0A7E}` (sparse pkg `Microsoft.PowerToys.FileLocksmithContextMenu`); legacy `FileLocksmithExt.dll`. Caption resource = "What's using this file?".
**Named Event / DSC**: no Named Event. DSC resource `microsoft.powertoys.FileLocksmith.settings` exists (controls module settings, not the master enable flag).
**No global hotkey** — entry is the Explorer context menu only.
## The two back-doors that make this module fully drivable (no Explorer needed)
### 1. `FileLocksmithCLI.exe` — deterministic engine ground-truth (PREFER for assertions)
Accepts paths as args; `--json`, `--kill`, `--wait`, `--timeout`. Uses the **same** `find_processes_recursive` engine as the UI.
```powershell
$cli = "$env:LOCALAPPDATA\PowerToys\FileLocksmithCLI.exe"
& $cli "<file|dir|drive>" --json | ConvertFrom-Json # {processes:[{pid,name,user,files[]}]}
& $cli "<path>" --kill # terminate lockers (== End task)
```
Detection = open **File handles** + **loaded modules** under the path (exact file, exact dir, dir-prefix recursive). `FileLocksmith.cpp:18-113`.
### 2. `last-run.log` IPC + launch UI — exercises the REAL UI code path
The context-menu handler writes selected paths to `…\File Locksmith\last-run.log` then launches the UI; the UI reads them in `MainViewModel()` ctor. Reproduce it:
```powershell
# UTF-16LE, each path + WCHAR \n, trailing empty-line terminator, NO BOM
function Write-LastRun([string[]]$Paths){
$f="$env:LOCALAPPDATA\Microsoft\PowerToys\File Locksmith\last-run.log"; $ms=[IO.MemoryStream]::new()
foreach($p in $Paths){$b=[Text.Encoding]::Unicode.GetBytes($p);$ms.Write($b,0,$b.Length);$n=[Text.Encoding]::Unicode.GetBytes("`n");$ms.Write($n,0,$n.Length)}
$n=[Text.Encoding]::Unicode.GetBytes("`n");$ms.Write($n,0,$n.Length);[IO.File]::WriteAllBytes($f,$ms.ToArray())
}
Write-LastRun @("C:\path\to\file"); Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.FileLocksmithUI.exe"
```
Source: `ExplorerCommand.cpp:182-227`, `dllmain.cpp:94-159`, `IPC.cpp`, `NativeMethods.cpp:62-97`.
### UI selectors (winapp ui)
- Window title: `Administrator: File Locksmith` (elevated) vs `File Locksmith` (non-elevated).
- Header button = the path label (`btn-<pathname>-…`); top-right `btn-…` = **Reload** (tooltip "Reload").
- Per-row End task button = `btn-…` (parent of `lbl-endtask-…`); invoke it (`InvokePattern`).
- `RestartAsAdminBtn` = shield icon, **visible only when non-elevated** (`MainPage.xaml:72-82`).
- `ProcessesListView` is virtualized; use `winapp ui scroll ProcessesListView --direction down/up`.
## Recipes — a control/observation map, NOT a per-test-case answer key
> Maps each capability to **how to drive it** and **where the result shows**. No canned process counts / paths / assertions — design those at runtime from the actual checklist item.
| # | Capability | Drive (entry-path / control) | Observe (where the result shows) |
|---|---|---|---|
| 1 | A locked file lists all its locking processes | CLI + UI on one locked file (with multiple lockers) | each locker shows as a ListItem |
| 2 | "End task" kills the locker and de-lists it | `winapp ui invoke` the End-task button | locker PID dies + row removed |
| 3 | Reload rediscovers a locker started after the UI opened | start a new locker → invoke Reload | the new locker appears |
| 4 | Closing a locker externally auto-removes it | external `Stop-Process` on a locker | auto-delisted via `WatchProcess`; empty state shown |
| 5 | Directory path finds lockers recursively | CLI/UI with a directory path | lockers inside the tree are listed |
| 6 | Drive root lists many lockers without crashing | CLI/UI with a drive root | large list renders; no crash |
| 7 | Non-elevated FL does NOT see the elevated runner | run CLI/UI non-elevated via scheduled task `RunLevel Limited` | `PowerToys.exe` absent (medium-IL FL can't see elevated procs) |
| 8 | "Restart as administrator" surfaces elevated-only lockers | non-elev UI shows the button; elevated run shows them | elevated run lists `PowerToys.exe` (UAC consent click NOT automatable) |
| 9 | Scrolling a large list doesn't crash | UI on a drive root + `winapp ui scroll` | process alive + responsive after scroll |
| 10 | Disabling FL removes the Explorer context-menu entry | Settings toggle Off (winapp ui) | `enabled."File Locksmith"`→false; `GetState→ECS_HIDDEN` (source) |
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control and design your own inputs + assertions. If no row matches, drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
## Common BLOCKED traps (avoid)
- **Don't BLOCK the lock-detection / End-task / scroll-doesn't-crash items as "needs a real installer / right-click"** — both the CLI and the `last-run.log`+UI back-door fully drive them with any locked file. The context menu literally just writes `last-run.log` + launches the same exe.
- **Launching the UI from an elevated shell makes it elevated** (title "Administrator: …") and hides the `RestartAsAdminBtn`. To test the non-elevated case (Recipes 7-8), launch via a **scheduled task with `-RunLevel Limited` / LogonType Interactive** — it lands on the user's desktop at medium IL.
- **`set-value` does fire the Reload** here (TogglePattern/Invoke work); no TextChanged gotchas.
## Elevation semantics (non-elevated FL invisibility — Recipes 7-8 core)
A medium-IL File Locksmith can't `DuplicateHandle`/read modules of higher-integrity processes, so the **elevated PowerToys.exe runner is invisible** to a non-elevated FL and **visible** to an elevated FL (which also calls `SetDebugPrivilege`, `App.xaml.cs:53-61`). Per-user installs put PowerToys under `%LOCALAPPDATA%\PowerToys`, not "Program Files" — use the PT install dir as the stand-in and note the caveat.
## Context-menu disable gate (Recipe 10)
`GetEnabled()` reads general `enabled."File Locksmith"` (`Settings.cpp:53-77`). When false: Win11 `GetState→ECS_HIDDEN` (`dllmain.cpp:81-84`); legacy `QueryContextMenu→E_FAIL` (`ExplorerCommand.cpp:116-119`). Disabling does NOT unregister the sparse package (stays `Status Ok`) — the entry is hidden dynamically. The packaged `IExplorerCommand` is **not** enumerable via Shell `Verbs()` and **not** `CoCreate`-able from a non-Explorer host (`REGDB_E_CLASSNOTREGISTERED`), so the pixel-level render is the only un-automatable bit (`BLK-VISUAL-RENDER` if you need it).
## Fixture files needed
None pre-canned. Create a temp file and lock it with a helper process (pwsh holding `File.Open(path, OpenOrCreate, Read, ReadWrite)` so multiple lockers coexist).
## Source citations
- `FileLocksmithLibInterop\FileLocksmith.cpp:18-113``find_processes_recursive` (handles + modules, recursive).
- `FileLocksmithLibInterop\NativeMethods.cpp:62-140``ReadPathsFromFile`, `StartAsElevated` (runas/--elevated).
- `FileLocksmithLib\IPC.cpp`, `Constants.h` — last-run.log format/path.
- `FileLocksmithUI\…\MainViewModel.cs:80-183` — load/EndTask/WatchProcess/RestartElevated.
- `FileLocksmithUI\…\MainPage.xaml` — control layout + RestartAsAdminBtn visibility.
- `FileLocksmithExt\ExplorerCommand.cpp`, `FileLocksmithContextMenu\dllmain.cpp`, `FileLocksmithLib\Settings.cpp` — enable gate.
## Ceiling
10/10 PASS observed (2026-06-08). The lock-detection / End-task / refresh / drive-scroll items cleanly driven; the Restart-as-admin item PASS-with-caveat (UAC consent click not automatable; outcome verified). **The disable-removes-menu item PASS — behaviorally verified** by a real Explorer right-click: with FL enabled the Win11 menu shows `MenuItem "Unlock with File Locksmith"`; after disabling in Settings the same right-click menu no longer shows it (no Explorer restart needed — `GetState` re-reads `enabled."File Locksmith"` live). NB the **shipped caption is "Unlock with File Locksmith"**, not the checklist's "What's using this file?". The right-click test needs an **unlocked interactive desktop** (a 4-hour idle auto-lock makes `GetForegroundWindow()=0``BLK-ENV`).
## Real right-click verification (Recipe 10) — works on an unlocked desktop
**Use the shared flow: `references/explorer-context-menu-flow.md` + `scripts/pt-explorer-contextmenu.ps1`.** FL's caption is **"Unlock with File Locksmith"**. Quick version:
```powershell
. "$skill\scripts\pt-explorer-contextmenu.ps1"
$menu = Open-PtExplorerContextMenu -ExplorerHwnd $hwnd -FileName 'target.txt' # synthetic right-click (+retry)
# present/absent assertion:
(Get-PtContextMenuItems -MenuHwnd $menu) -contains 'Unlock with File Locksmith' # true enabled / false disabled
# real launch (UIA invoke by name):
Invoke-PtContextMenuItem -MenuHwnd $menu -ItemName 'Unlock with File Locksmith' # -> launches PowerToys.FileLocksmithUI.exe (non-elevated)
# Toggle FL off in Settings, re-open menu, assert the caption is gone. No Explorer restart needed (GetState re-reads live).
```
## Don'ts
- Don't expect `Shell.Application.Verbs()` to show the FL entry — it's a Win11 packaged command, invisible to classic verbs.
- Don't kill processes by name; use `Stop-Process -Id <pid>`.
- Don't forget to restore `enabled."File Locksmith"=true` and close test-spawned UI/Settings after the disable-removes-menu test.

View File

@@ -0,0 +1,87 @@
# Image Resizer — module verification profile
**PT module**: `Image Resizer` (resize images via Explorer right-click; WinUI 3 GUI + headless CLI)
**Source**: `<PT-repo>\src\modules\imageresizer\` (PT repo)
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\Image Resizer\settings.json` (PowerToys-wrapper shape: `{ "properties": { "imageresizer_*": { "value": … } }, "name": "Image Resizer", "version": "1" }`). A legacy `sizes.json` mirrors `imageresizer_sizes`; `image-resizer-settings.json` is `{}` (unused).
**Enable flag**: `%LOCALAPPDATA%\Microsoft\PowerToys\settings.json``enabled."Image Resizer"` (runner-owned; restart runner after toggling).
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\Image Resizer\Logs\…`
**Exes**: GUI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.ImageResizer.exe`; **CLI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.ImageResizerCLI.exe`**.
**Context menu**: Win11 packaged `IExplorerCommand` (sparse pkg `ImageResizerContextMenuPackage.msix`, dllmain.cpp) + legacy classic `ImageResizerExt.dll` (`dll/ContextMenuHandler.cpp`). **Shipped caption = "Resize with Image Resizer"** (`IDS_IMAGERESIZER_CONTEXT_MENU_ENTRY`; checklist's "Resize images" is STALE).
**No global hotkey / no Named Event / no DSC for the engine** — entry is the Explorer menu (or direct exe launch).
## The back-door that makes this module ~fully drivable (no Explorer needed)
### `PowerToys.ImageResizerCLI.exe` — the deterministic engine (PREFER for all resize-behavior items)
Shares the exact `ResizeBatch.FromCliOptions``ResizeBatch.ProcessAsync``ResizeOperation.ExecuteAsync` engine as the GUI (`ui/Cli/ImageResizerCliExecutor.cs:76-108`, `App.xaml.cs:102`). Reads live `Image Resizer\settings.json` then applies CLI overrides (`CliSettingsApplier.cs`).
```
--width/-w --height/-h --unit/-u {Centimeter|Inch|Percent|Pixel} --fit/-f {Fill|Fit|Stretch}
--size <presetIndex> --shrink-only --replace/-r --ignore-orientation --remove-metadata
--quality/-q --keep-date-modified --filename/-n "<%1..%6>" --destination/-d <dir>
--show-config --help <files…> (also accepts \\.\pipe\<name> and stdin file list)
```
`--show-config` dumps the effective settings (great pre/post check). `-d <dir>` keeps outputs isolated. Assert output dimensions with `[System.Drawing.Image]::FromFile(p)`.
**Caveat**: `--ignore-orientation`/`--shrink-only`/`--replace`/`--keep-date-modified`/`--remove-metadata` are *flags* — they can only set the value **true**; to test the **false** case, temporarily edit `settings.json` (back up + restore).
### Direct GUI launch — for the two UI-only items (gif warning, size-list populated)
`Start-Process PowerToys.ImageResizer.exe "<file>"` opens the window pre-loaded (argv/stdin via `ResizeBatch.FromCommandLine`). Behaviorally identical to the context-menu launch (`dllmain.cpp:219-245` just writes a pipe + launches the same exe). Then drive with `winapp ui`:
- Size selector: `SizeComboBox``winapp ui invoke SizeComboBox -w <hwnd>` to expand, then `inspect` shows `itm-<name>-XXXX` ListItems.
- Gif warning: `Message Text "Gif files with animations may not be correctly resized."` InfoBar, bound to `ViewModel.HasGifFiles` (set when any file ends `.gif`).
## Engine facts (verified from source — cite these for the resize items)
- `ResizeFit`: **Fill=0, Fit=1, Stretch=2** (`ResizeFit.cs`). Fit=`min(scaleX,scaleY)`; Fill=`max`+centered-crop; Stretch=independent (`ResizeOperation.cs:449-498`).
- `ResizeUnit`: **Centimeter=0, Inch=1, Percent=2, Pixel=3** (`ResizeUnit.cs`). Inch=`v*dpi`; cm=`v*dpi/2.54`; Percent=`v/100*orig`; Pixel=`v` (`ResizeSize.cs:109-123`). **Outputs depend on image DPI** — read actual DPI and compute expectations from it (a 120-DPI fixture gives 10cm→472px, 4in→480px).
- Filename `%1..%6` → original-name, size-name, selected-W, selected-H, **output**-W, **output**-H (`Settings.cs:229-239`, `ResizeOperation.cs:593-601`).
- ShrinkOnly: if target scale>1, returns `noTransformNeeded` (file copied unchanged) (`ResizeOperation.cs:462-475`).
- KeepDateModified: `SetLastWriteTimeUtc(out, GetLastWriteTimeUtc(src))` (`ResizeOperation.cs:146-149`).
- Replace: `File.Replace(out, src, backup)` then recycle backup — no copy left (`ResizeOperation.cs:151-156`).
- IgnoreOrientation swap: gated by `IgnoreOrientation && !HasAuto && Unit != Percent` (`ResizeOperation.cs:419-444`).
## Recipes — a control/observation map, NOT a per-test-case answer key
> Maps each capability to **which control/CLI flag drives it** and **where the result shows**. CLI flag *names* and fit-mode/unit/field enumerations are stable IR knowledge and stay; concrete flag *values*, fixtures, and expected outputs are per-test-case — design those at runtime.
| # | Capability | Drive (control / CLI flag) | Observe (where the result shows) |
|---|---|---|---|
| 1 | Module disabled → context-menu entry absent | toggle `enabled` off + restart runner; synthetic menu (only valid observer) | "Resize with Image Resizer" absent. Gate: `dllmain.cpp:87-91` (ECS_HIDDEN), `ContextMenuHandler.cpp:70-71,383-385`. Locked desktop → BLK-ENV |
| 2 | Module enabled → entry present (modern + classic), click launches the GUI | synthetic menu + invoke | `Get-PtContextMenuItems` shows "Resize with Image Resizer"; classic "Show more options" too; invoke → `PowerToys.ImageResizer.exe` launches |
| 3 | Remove a built-in size / add a custom size | edit `imageresizer_sizes` (INTEGER Ids!) + launch GUI | `SizeComboBox` reflects the edit (removed gone, custom present) |
| 4 | Resize one / multiple files end-to-end | CLI `--size <id> [files…]` | outputs at the size's Fit dimensions |
| 5 | GIF animation warning on `.gif` input | GUI on a `.gif` | warning InfoBar present (`winapp ui inspect`) |
| 6 | Fit modes (Fill / Fit / Stretch) | CLI `--width --height --fit <mode>` | output shape matches the mode (crop / letterbox / exact) |
| 7 | Unit conversion (cm / inch / percent / pixel) | CLI `--unit <u>` | output px = unit converted at the image's DPI |
| 8 | Custom filename format (`%1`..`%6` fields) | CLI `--filename <fmt>` | output filename follows the format fields |
| 9 | "Keep date modified" | CLI `--keep-date-modified` | output mtime == source mtime (control: without the flag, differs) |
| 10 | "Shrink only" | CLI `--shrink-only` | an already-small image is untouched (control: a large one still shrinks) |
| 11 | "Replace original" | CLI `--replace` | original replaced in place; no `(name) (1)` copy |
| 12 | "Ignore orientation" | settings (false) vs flag (true) | on a portrait target over a landscape image: false→no W/H swap, true→swap |
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control/flag and design your own inputs + assertions. If no row matches, drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
## Common BLOCKED traps (avoid)
- **Don't mark the resize-behavior items BLOCKED for "needs a real right-click".** The CLI fully drives them with the identical engine; the menu is just the trigger (prove the menu→launch wiring once with the golden path in Recipe 2).
- **PowerShell `ConvertTo-Json` writes computed numbers as doubles (`"Id": 3.0`)** → `System.Text.Json` rejects `imageresizer_sizes` and the app silently falls back to the 4 built-in default presets (Small/Medium/Large/Phone). Cast Ids to `[int]` or regex-strip `\.0`. This bit the remove-size-add-custom item (Recipe 3) the first time.
- **cm/inch outputs depend on the fixture's DPI, not 96.** System.Drawing saves at the session display DPI (here 120). Compute expectations from the actual DPI.
- **Caption is "Resize with Image Resizer", not the checklist's "Resize images"** (both menus). Hard-match the real caption.
- **Idle auto-lock = BLK-ENV for the disabled-absent + enabled-present items (Recipes 1-2)** (synthetic right-click needs foreground). Disable lock/sleep before the run (`references/environment-setup.md`).
## Fixture files needed
None pre-canned. Generate with `System.Drawing`: a landscape (e.g. 1200×800) and portrait (800×1200) JPEG, a small (100×100) PNG, a square (400×400) PNG, a `.gif` (single frame is fine — the warning is extension-based), and 3 identical images for the multi-file batch.
## Source citations
- `ui/Cli/ImageResizerCliExecutor.cs`, `ui/Models/CliOptions.cs`, `ui/Cli/CliSettingsApplier.cs`, `ui/Cli/Commands/ImageResizerRootCommand.cs` — CLI surface + engine reuse.
- `ui/Models/ResizeOperation.cs:419-501,572-617,146-156` — dimension math, filename, keep-date, replace.
- `ui/Models/ResizeSize.cs:78-124`, `ResizeFit.cs`, `ResizeUnit.cs` — unit/fit math + enum order.
- `ui/Properties/Settings.cs:65,105-111,229-239,431-552` — paths, defaults, JSON property names, FileNameFormat.
- `ImageResizerContextMenu/dllmain.cpp:49,79-133,219-245,284` — modern menu title, enable+image gate, launch, caption.
- `dll/ContextMenuHandler.cpp:21,46-48,70-71,383-385` — classic menu caption + enable gate.
- `ui/ViewModels/InputViewModel.cs:76,143`, `ImageResizerXAML/Views/InputPage.xaml:293-299`, `Strings/en-us/Resources.resw:148-149` — gif warning.
## Ceiling
**18/18 PASS** observed (2026-06-09). All 14 resize-behavior items + the enabled-entry-present-in-both-menus + the remove-size + the gif-warning items cleanly driven via CLI/GUI. The disabled-entry-absent case (in both modern + classic menus, with sibling entries remaining and the entry returning on re-enable) verified live once the desktop was unlocked. NB: an idle auto-lock will turn the menu-presence Recipes 1-2 into BLK-ENV — disable lock/sleep up front (`references/environment-setup.md`).
## Don'ts
- Don't expect `Shell.Application.Verbs()` to list the entry — it's a Win11 packaged command (classic verbs are blind; `CoCreate``REGDB_E_CLASSNOTREGISTERED`).
- Don't hardcode 96 DPI for cm/inch math.
- Don't write preset Ids as JSON doubles.
- Don't kill processes by name; use `Stop-Process -Id <pid>`.
- Don't forget to restore `enabled."Image Resizer"=true` + restart runner, and revert any `settings.json`/`sizes.json` edits.

View File

@@ -0,0 +1,77 @@
# New+ — module verification profile
**PT module**: `NewPlus` (Explorer right-click → "New+" submenu that creates files/folders from a user templates folder)
**Source**: `<PT-repo>\src\modules\NewPlus\` (shell ext) + `<PT-repo>\src\settings-ui\Settings.UI\ViewModels\NewPlusViewModel.cs` (Settings UI)
**Module-owned settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\settings.json`**folder is `NewPlus`, NOT `New`** (matches SKILL.md pitfall #18 table). Keys: `HideFileExtension`, `HideStartingDigits`, `TemplateLocation`, `ReplaceVariables`, `BuiltInNewHidePreference`.
**Templates folder (default)**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\Templates` (per `TemplateLocation`)
**Default-templates source**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\Assets\NewPlus\Templates` (also `%ProgramFiles%\PowerToys\...` on machine installs)
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\NewPlus.ShellExtension\Logs\v<ver>\log_<date>.log`
**Packaged command**: sparse MSIX `Microsoft.PowerToys.NewPlusContextMenu`; command CLSID `{FF90D477-E32A-4BE8-8CC5-A502A97F5401}`
**Named Event**: none. **DSC**: n/a.
> Read **`../references/explorer-context-menu-flow.md` first** — New+ is a Win11 packaged-IExplorerCommand context-menu module; the menu can only be eyeballed via a real synthetic right-click on an **unlocked interactive desktop**. On a locked/RDP-minimized desktop (`Test-PtDesktopInteractive=False`) all "menu appears / template appears / hidden-caption" assertions are BLK-ENV / BLK-VISUAL-RENDER, not product FAILs.
## Entry-paths (try in order)
### 1. Enable/disable + registration gate (menu presence/absence) — headless-safe
Flip `enabled.NewPlus` in master `settings.json` + `Restart-PtRunner`, **or** toggle the Settings switch (below). Observe the gate, no foreground needed:
- CLSID registered ⇒ `Test-Path "HKCU:\Software\Classes\CLSID\{FF90D477-E32A-4BE8-8CC5-A502A97F5401}"` is `True` (enabled) / `False` (disabled).
- Log lines `New+ context menu registered` / `... unregistered` + `Runtime registration completed for CLSID ...`.
- Sparse package stays `Status Ok` even when disabled (hidden dynamically — SKILL.md pitfall #17).
### 2. Settings UI toggles via UIA invoke — headless-safe, **required for template auto-copy**
`Start-Process …\PowerToys.exe --open-settings=NewPlus`, then `winapp ui invoke <btn> -w <settingsHwnd>`:
- Enable toggle: `btn-new-248c` (under `NewPlusEnableToggle`) — **the enable transition runs `CopyTemplateExamples`** (Settings-UI side, `NewPlusViewModel.IsEnabled` setter). A master-`settings.json` flip + runner restart does **NOT** copy templates.
- `btn-hidethefileexte-24a0` (Hide file extension), `btn-hideleadingdigi-24a8` (Hide leading digits). AutomationIds carry a per-session suffix — re-`inspect` to get the live id.
### 3. Synthetic right-click on the folder **BACKGROUND** (the menu-render observer) — needs unlocked desktop
New+ lives in the folder-background ("New") menu, **not** a file's context menu — so `pt-explorer-contextmenu.ps1`'s `Open-PtExplorerContextMenu` (which right-clicks a *file item*) is the wrong entry. Right-click an **empty area of the file list** instead, then expand the `New+` submenu (a separate popup window one level deeper):
```powershell
# force-foreground the CabinetWClass window, GetWindowRect, RightClick at ~45% width / 68% height (empty list area)
# -> main bg menu popup (PopupWindowSiteBridge). Then:
$np = (winapp ui search 'New+' -w $mainMenuHwnd --json).matches | ? type -eq MenuItem | select -First 1
winapp ui invoke $np.selector -w $mainMenuHwnd # expands the New+ submenu
# the submenu is the PopupWindowSiteBridge popup that contains 'Open templates' but NOT 'Sort by'
# enumerate its MenuItems (templates are 1:1 with the Templates-folder entries) / invoke one by name
```
Template items render with **caption transforms applied** (HideFileExtension strips `.txt`; HideStartingDigits strips `01. `). Selecting a template creates it in the current folder + enters rename mode. BLK-ENV only if `Test-PtDesktopInteractive` is False. **No Explorer restart needed** for setting A/B — the handler re-reads `NewPlus\settings.json` on each menu build.
## Recipes — a control/observation map, NOT an answer key
| # | Capability | Drive (control / settings key) | Observe (where the result shows) |
|---|---|---|---|
| 1 | Menu entry present when enabled | enable (master flag + restart, or `btn-new-248c`) | CLSID registered in `HKCU\…\CLSID`, log `context menu registered`; *visible submenu* = synthetic menu only (BLK-VISUAL-RENDER if locked) |
| 2 | Menu entry absent when disabled | disable | CLSID absent, log `context menu unregistered`; package still `Status Ok` |
| 3 | Templates folder created empty | shell ext `create_folder_if_not_exist(root)` on menu build (delete folder → right-click) | folder recreated **empty** — needs synthetic menu (BLK-ENV if locked) |
| 4 | Default templates copied when empty | `CopyTemplateExamples` on Settings-UI **enable** transition (`btn-new-248c` off→on) while folder empty | Templates folder repopulated from install Assets (filesystem — headless-safe) |
| 5 | A template (file/folder) shows + creates on select | put item in Templates folder; select it in the New+ submenu | submenu item (1:1 with dir entries) + `SHFileOperation FO_COPY` to target — synthetic menu only |
| 6 | Hide file extension | `HideFileExtension` / `btn-hidethefileexte-24a0` | strips ext from **menu caption only** (`get_menu_title`, `show_extension=false`); created file keeps ext — caption is BLK-VISUAL-RENDER if locked |
| 7 | Hide starting digits/spaces/dots | `HideStartingDigits` / `btn-hideleadingdigi-24a8` | strips leading digits+separator from **both** menu caption and **created filename** (`remove_starting_digits_from_filename` via `get_menu_title` + `copy_template`); needs a digit-prefixed template + render |
> Verify a setting actually drives behavior by editing the **module-owned** `NewPlus\settings.json` (not the PT-store mirror) and relaunching; the Settings toggles round-trip into this same file.
## Common BLOCKED traps
- **Master-flip + runner restart does not copy default templates** — that's a Settings-UI action (`NewPlusViewModel.IsEnabled`). Use the UIA toggle for any template-auto-copy item.
- **Menu render is invisible without a real right-click** — packaged command is not `CoCreate`-able (`REGDB_E_CLASSNOTREGISTERED`) and not in classic `Shell.Application.Verbs()`. Locked desktop ⇒ BLK-ENV; do not substitute a CLI/back-door (there isn't one, and it'd be a false PASS).
- **No template-count observable** — `saved_number_of_templates` is an in-memory static (`new_utilities.cpp`), not registry/log.
## Fixture files needed
- A plain file (e.g. `test.txt`) and a folder-with-files to drop into Templates (template-appears items).
- A digit-prefixed template (e.g. `01. Test.txt`) to exercise Hide-starting-digits.
## Source citations
- `src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp``get_menu_title` (hide-extension), `remove_starting_digits_from_filename`, `copy_object_to`.
- `src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h``copy_template`, `create_folder_if_not_exist`, `get_newplus_setting_hide_*`, `register_msix_package`.
- `src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp``create_folder_if_not_exist(root)` + template enumeration.
- `src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs``CopyTemplateExamples` (creates dir; copies examples only when files==0 && dirs==0), called from `IsEnabled` setter / `OpenNewTemplateFolder` / `DashboardViewModel`.
## Ceiling
- Unlocked interactive desktop: **9/9 PASS** (verified 2026-06-18 via background-menu synthetic right-click + submenu expansion).
- Locked/non-interactive desktop: ~3/9 (registration-gate present/absent + template auto-copy); menu-render/select items fall to BLK-ENV/BLK-VISUAL-RENDER — re-run after unlocking.
## Don'ts
- Don't edit `…\PowerToys\New\settings.json` — wrong path; the file is under `NewPlus\`.
- Don't use `Open-PtExplorerContextMenu` (file-item right-click) for New+ — it's the folder **background** ("New") menu; right-click empty list space instead.
- Don't forget to **expand the `New+` submenu** (invoke it) before enumerating templates — they live one popup deeper than the main menu.
- Don't mark menu-render items as product FAIL on a locked desktop — it's BLK-ENV.
- Don't restart Explorer to apply a setting change — the handler re-reads `NewPlus\settings.json` per menu build.

View File

@@ -0,0 +1,117 @@
# Peek — module verification profile
**PT module**: `Peek` (file previewer activated on Ctrl+Space with Explorer file selected)
**Source**: `<PT-repo>\src\modules\Peek\` (PT repo)
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\Peek\settings.json`
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\Peek\Logs\v<ver>\log_<date>.log`
**Exes**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.Peek.UI.exe`
**Default hotkey**: `Ctrl+Space` (modifiers=`ctrl`, code=32; see `settings.json``ActivationShortcut`)
**Named Event**: `Local\ShowPeekEvent` (friendly name: `Peek.Show` in `pt-shared-events.ps1` catalog)
**DSC resource**: `Microsoft.PowerToys/PeekSettings`
## Three entry-paths (try in order)
### 1. CLI back-door — fastest, no Explorer needed
```powershell
Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.Peek.UI.exe" -ArgumentList "<file>"
```
**Source**: `Peek.UI\PeekXAML\App.xaml.cs:106-134` — when last arg is not int (=runner PID) and is an existing file, it sets `_launchedFromCli=true`, builds `SelectedItemByPath`, calls `OnShowPeek()`. Bypasses hotkey + Explorer foreground.
**Use for**: single-file previewer rendering tests (Recipes 1-2) and the CLI-accepts-path assertion (Recipe 8).
**Cannot use for**: navigation tests (Recipes 4-7, 10-11) — source has `if (_isFromCli) return;` guard that disables arrow navigation, and CLI mode spawns a fresh process every call (no pin-state-across-reopen).
### 2. Shell.Application COM + Ctrl+Space — Explorer-driven, supports navigation
This is the canonical "do what a real user would do" path that drives all the navigation/pin tests.
```powershell
# Dot-source helpers first
. "$skill\scripts\pt-explorer-com.ps1"
. "$skill\scripts\pt-sendinput-chord.ps1"
# Set up multi-file selection in Explorer + trigger Peek in one call:
$peekHwnd = Invoke-PtPeekWithExplorerSelection `
-FolderPath 'D:\fixtures' `
-FileNames 'test-markdown.md','test-html.html','test-source.cs'
# Now Peek is open over a 3-file IShellItemArray. Test:
winapp ui invoke 'PinButton' -w $peekHwnd # pin
# (move window via SetWindowPos)
Send-PtChord -Key 0x27 # Right arrow → switch file
# verify the pinned position stuck
```
**Use for**: pin behavior, multi-file navigation, file switching (Recipes 4-7, 10-11).
**Requires**: interactive desktop session (`Test-PtInteractiveDesktop` must show both `ForegroundOk=True` and `ShellComOk=True`).
### 3. Named Event signal — quick smoke
```powershell
Invoke-PtSharedEvent -Name 'Peek.Show'
```
Wakes the resident Peek process (different from CLI back-door — respects current Explorer foreground selection). Used by some framework tests for the "Peek is enabled and listening" assertion.
## Recipes — a control/observation map, NOT a per-test-case answer key
> Maps each Peek *capability* to **how to drive it** and **where the result shows**. It does NOT prescribe concrete fixtures/coords/inputs or expected values — design those at runtime from the actual checklist item. Only a real UI/behavior change should force an edit here.
| # | Capability | Drive (control / command) | Observe (where the result shows) |
|---|---|---|---|
| 1 | File-type previewer renders (image / text+code / markdown / PDF / HTML / archive / unsupported) | `Peek.UI.exe <fixture>` (entry-path 1) → `winapp ui inspect -w <hwnd> --depth 7` | the type's previewer node present (`ImagePreview Image`; `PreviewBrowser Pane` for dev/text/md/HTML; archive tree for zip; File-Type/Size/Date view for unsupported). Prefer `winapp ui search` for an in-fixture marker over OCR |
| 2 | "Open with default app" via button | `winapp ui invoke LaunchAppButton` | a new editor process/window for `<file>` appears (PID diff) |
| 3 | "Open with default app" via Enter | `Assert-PtForegroundOrAbort``Send-PtChord -Key <Enter>` | same as #2 |
| 4 | Pin keeps window position when switching files | Shell COM + Ctrl+Space (entry-path 2) → `winapp ui invoke PinButton` → move window → navigate to next file | window stays at the pinned coordinates |
| 5 | Pin position persists across close + reopen | pinned → Esc to close (graceful — **don't `Stop-Process`**, it bypasses the pin-save handler) → reopen via Shell COM + Ctrl+Space | new window opens at the same pinned coordinates |
| 6 | Unpin releases the lock; switching file reverts to default | `winapp ui invoke PinButton` again (unpin) → navigate | window moves to the default position |
| 7 | Unpinned reopen uses default position | unpinned → Esc-close → reopen | new window at default, not the stale pinned coords |
| 8 | `Peek.UI.exe <file>` CLI opens Peek | entry-path 1 | covered by #1 across file types |
| 9 | Concurrent Peek sessions don't crash/interfere | launch `Peek.UI.exe` several times on different files, leaving windows open | each spawns its own process/window; no error in `Peek\Logs` |
| 10 | Arrow keys cycle between selected files | Shell COM multi-file selection → Ctrl+Space → `Send-PtChord` Right/Left | window title updates to each file in sequence, wraps at the ends |
| 11 | Multi-file selection scopes navigation | select a subset of a folder → navigate | only the selected files cycle, not the rest |
| 12 | Activation-hotkey reassignment takes effect | edit `Peek\settings.json` `properties.ActivationShortcut``Restart-PtRunner` (**not hot-reloaded** — see Gotchas) → press the new chord, then the old chord | new chord opens Peek; old chord does nothing |
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control and design your own inputs + assertions for *that* item. If no row matches, it's a NEW capability — drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
## BLOCKED triage (single source of truth)
If the agent only tried the CLI back-door and marked the pin / navigation tests BLOCKED → **misdiagnosis**, try entry-path #2 (Shell.Application COM + Ctrl+Space).
If the agent tried Shell COM + Ctrl+Space and got `GetForegroundWindow()=0` + `SendInput → ACCESS_DENIED (5)`**environment**, not framework. The session has no attached input desktop (RDP minimized, screen locked, screensaver, etc.). See `SKILL.md` pitfall #13 and `references/environment-setup.md` for the per-scenario table + powercfg setup commands. Mark BLK-ENV with mitigation citation.
Both traps were observed in 2026-06-08 sign-off runs; preventing both is now the agent's pre-flight job (`pt-session-diagnose.ps1`).
## Fixture files needed
Put these in a workspace `fixtures/` folder before starting:
- `small-image.png` (any 200x150 PNG)
- `Program.cs` (any C# file)
- `readme.md` (markdown with H1 + bold + bullet list)
- `test-pdf.pdf` (PDF with embedded text "PDF_FIXTURE_OK" + "PDF_MARKER_42")
- `page.html` (HTML with `<h1>` containing "HTMLPEEKMARKER")
- `archive.zip` (zip containing 1 small text file)
- `unsupported.xyz` (any small binary)
- 3 differently-sized images for the pin-position tests (e.g. 320x240, 800x600, 1920x1080)
## Source citations
- `<PT-repo>\src\modules\Peek\Peek.UI\PeekXAML\App.xaml.cs:106-134` — CLI arg parsing, `_isFromCli` flag, OnShowPeek call.
- `<PT-repo>\src\modules\Peek\Peek.UI\PeekXAML\Models\NavigationManager.cs``// TODO: implement navigation` + `if (_isFromCli) return;` guards.
- `<PT-repo>\src\common\interop\shared_constants.h``ShowPeekEvent` name.
## Ceiling
**18/18 = 100%** achievable from a normal interactive admin console session (verified 2026-06-08). The change-shortcut item is PASS-able via the settings.json + runner-restart path — see Recipe 12.
## Peek-specific gotchas
- **Activation-shortcut is NOT hot-reloaded.** Editing `Peek\settings.json` `ActivationShortcut` and waiting for the file-watcher debounce does nothing — the centralized keyboard hook only re-registers the chord after `Restart-PtRunner`. Restart after the change AND again after restoring.
- **PinButton spawns a `PopupHost` teaching-tip.** Invoking `PinButton` pops a small confirmation flyout (≈192x63) titled `PopupHost` that surfaces *first* in `winapp ui list-windows`. A naive "first HWND" regex grabs the popup, not Peek. Match by title suffix `- Peek` (regex like `HWND (\d+): "([^"]*- Peek)"`) and/or cache the original Peek HWND before invoking PinButton.
- **Win11 Notepad tabs/session-restore** muddy the "open-with-default-app" tests (Recipes 2-3): the spawned Notepad restores prior tabs, so the foreground Notepad's title may not show your file. Enumerate all Notepad windows and match `"<file> - Notepad"` explicitly.
## Don'ts
- **Don't `Stop-Process PowerToys.Peek.UI -Force`** to close Peek between iterations — bypasses the save handler, breaks the pin-state-persistence tests (Recipes 5, 7). Use Esc / `winapp ui invoke CloseButton`.
- **Don't assume CLI back-door supports navigation** — it doesn't (`_isFromCli` guard). For nav tests use Shell COM + Ctrl+Space.
- **Don't OCR the previewer surface** when UIA already exposes the correct nodes (`ImagePreview`, `PreviewBrowser`, `LaunchAppButton`, `PinButton`). UIA is more reliable than OCR.

View File

@@ -0,0 +1,114 @@
# PowerRename — module verification profile
**PT module**: `PowerRename` (bulk-rename UI launched via Explorer context menu on selected files/folders)
**Source**: `<PT-repo>\src\modules\PowerRename\` (PT repo)
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\settings.json`
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\Logs\v<ver>\log_<date>.log`
**Exe**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.PowerRename.exe`
**Activation**: Explorer right-click → "Rename with PowerRename" (Win11 Tier-1 menu; **no classic HKCR verb on Win11**); optional global hotkey if user-configured
**DSC resource**: `Microsoft.PowerToys/PowerRenameSettings`
## Shared mechanics
For the synthetic-right-click + context-menu-invoke flow that ALL Explorer-context-menu modules use, see **`references/explorer-context-menu-flow.md`** + **`scripts/pt-explorer-contextmenu.ps1`** (`Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems`). That doc covers stability rules, multi-file selection, BLK-ENV handling, and module-caption table. Don't duplicate; cite by section.
For the Win11 IExplorerCommand vs classic HKCR distinction, see `scripts/pt-shell-verbs.ps1` header — PR is **modern-menu-only on Win11**, so classic-verb enumeration via Shell.Application **will not find it**.
## Entry-paths (try in order)
### 1. Direct CLI launch with file args — PREFERRED for UI-driven tests (verified 2026-06-10)
```powershell
$tmp = New-Item -ItemType Directory -Path "$env:TEMP\pr-fixture-$(Get-Random)"
1..3 | ForEach-Object { 'x' | Set-Content "$($tmp.FullName)\file$_.txt" }
Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.PowerRename.exe" `
-ArgumentList "$($tmp.FullName)\file1.txt","$($tmp.FullName)\file2.txt","$($tmp.FullName)\file3.txt"
Start-Sleep -Milliseconds 1500
$pr = (winapp ui list-windows -a PowerToys.PowerRename 2>$null | Out-String) -split "`r?`n" |
ForEach-Object { if ($_ -match 'HWND (\d+):') { [int64]$matches[1] } } | Select-Object -First 1
winapp ui inspect -w $pr --depth 5 -i 2>$null | Out-String | Select-String 'CheckBox "file\d\.txt"'
# Expect 3 hits (file1/2/3.txt, [on] by default)
```
Bypasses the context menu entirely; same code path inside the exe (it parses argv as the file list). **Use for every UI-driven option/regex/preview test** (Recipes 4-12 below).
### 2. Synthetic right-click + Invoke-PtContextMenuItem — for "menu entry present/absent" assertions (Recipes 1-3)
Use the canonical flow from `references/explorer-context-menu-flow.md` Recipe. The menu-presence assertion is the ONE thing the CLI back-door cannot prove (it works even if the menu entry is correctly hidden — the false-positive trap described in that doc).
```powershell
. "$skill\scripts\pt-explorer-contextmenu.ps1"
$hwnd = Open-PtExplorerContextMenu -FolderPath 'D:\fixtures' -FileNames 'a.txt'
$items = Get-PtContextMenuItems -MenuHwnd $hwnd
$has = $items | Where-Object Name -match 'Rename with PowerRename'
# assert $has -> entry present
```
### 3. Shell COM classic verb (does NOT work on Win11 stock install)
```powershell
Invoke-PtShellVerb -Path 'D:\fixtures\a.txt' -NamePattern 'PowerRename' # -> False
```
Returns False on Win11 because PT registers PR only via IExplorerCommand, not as a classic HKCR shell verb. **Use only for negative checks** (and prefer the synthetic-menu enumeration above, which observes the actual Tier-1 menu).
## Recipes — a control/observation map, NOT a per-test-case answer key
> **What this table is (and isn't):** it maps each PowerRename *capability* to **which control drives it** (AutomationId / settings key) and **where the result shows up**. It deliberately does **NOT** prescribe specific Search/Replace inputs or expected-output assertions — those are the agent's job to design from the actual checklist item at runtime. Keeping it input/assertion-free means the table survives checklist-wording changes; only a real UI redesign (renamed/moved control) should force an edit here (as happened to rows 5 & 12 in build 0.100.0).
| # | Capability | Drive (control / settings key) | Observe (where the result shows) |
|---|---|---|---|
| 1 | Context-menu entry present when enabled, gone when disabled | master `enabled.PowerRename` flip + `Restart-PtRunner`; synthetic menu (entry-path 2) | `Get-PtContextMenuItems` includes / excludes "Rename with PowerRename" |
| 2 | "Show icon on context menu" | `ShowIcon` in `power-rename-settings.json` + relaunch | menu entry shows icon vs text-only (screenshot); or HKCR `Icon` |
| 3 | "Appear only in extended menu" | `ExtendedContextMenuOnly` + relaunch | Tier-1 menu hides PR; classic "Show more options" still lists it |
| 4 | Any search/replace option toggle (regex, match-all, case-sensitive, autocomplete, last-use) | `winapp ui invoke checkBox_regex` / `checkBox_matchAll` / `checkBox_case` (etc.); re-read `power-rename-settings.json` | the settings key flips **and** the preview behavior changes accordingly |
| 5 | Case mode (single-select) | toggle **buttons** `toggleButton_lowerCase` / `upperCase` / `titleCase` / `capitalize` (not a dropdown) | preview column shows case-transformed names |
| 6 | Scope: include/exclude Files / Folders / Subfolders | `toggleButton_includeFiles` / `includeFolders` / `includeSubfolders` | excluded row types appear disabled in the preview |
| 7 | Apply-to scope: name-only / extension-only | the "Apply to" selector | replacement affects only the name vs only the extension (preview) |
| 8 | Enumerate items | `toggleButton_enumItems`; Replace accepts `${start=,increment=,padding=}` tokens | preview shows the substituted counter |
| 9 | Datetime tokens | Replace accepts `$DD` `$MMMM` `$YYYY` `$hh` `$mm` `$ss` `$fff` | preview value matches `(Get-Item <file>).CreationTime` formatted the same way |
| 10 | Boost library (Perl regex beyond .NET, e.g. lookbehind) | `UseBoostLib`**read at process start; relaunch PR after toggling** | the Perl-only pattern matches in the preview without error |
| 11 | Per-row include/exclude in the preview | invoke a row checkbox to uncheck | the unchecked file is unchanged on disk after Rename |
| 12 | Filter preview / select-all (NOT a column-header click — headers `TxtBlock_Original`/`TxtBlock_Renamed` are non-interactive labels) | `btn-filter-XXXX``button_showAll` / `button_showRenamed`; `checkBox_selectAll` | visible row set shrinks/grows; all rows toggle on/off |
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control and design your own inputs + assertions for *that* item. If no row matches, it's a NEW capability — drive it ad-hoc and add a row (capability + control + observation point, no canned inputs).
## Fixture files needed
In a workspace `fixtures/` folder:
- `a.txt`, `b.txt`, `c.txt` — multi-select
- `IMG_001.png`, `IMG_002.png`, `IMG_003.png` — regex capture
- subfolder `subdir/` with 2 inner files — folder/subfolder exclusion
- `Foo_A_A_A.txt` — match-all
- `MIXED.txt` — case-sensitive
Always copy fixtures to a disposable temp folder before running actual rename operations.
## Gotchas
- **TWO settings files — PR reads `power-rename-settings.json`, NOT `settings.json`** (verified 2026-06-10). `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\` holds both: (1) `settings.json` = PT-store, keys `bool_mru_enabled`/`bool_persist_input`/`bool_show_icon_on_menu`/`bool_show_extended_menu`/`bool_use_boost_lib`/`int_max_mru_size` (what `Get-PtModuleSettings` + the Settings UI bind to); (2) `power-rename-settings.json` = the module's own store, keys `ShowIcon`/`ExtendedContextMenuOnly`/`PersistState`/`MRUEnabled`/`MaxMRUSize`/`UseBoostLib`**this is the file the PR UI exe and the context-menu COM handlers actually read at launch** (`lib/Settings.cpp` `CSettings::Load→ParseJson`). The runner (`dll/dllmain.cpp:301-307`) syncs PT-store→module-store only on a Settings-UI *change event*; the PT-store file can sit stale for days. **To drive ShowIcon / ExtendedContextMenuOnly / MRUEnabled / PersistState / UseBoostLib deterministically, edit `power-rename-settings.json` directly + relaunch PR (or restart runner+Explorer for the menu handlers), then restore.** Map (settings.json key → user-facing toggle): ShowIcon→"Show icon on context menu", ExtendedContextMenuOnly→"Appear only in extended menu", MRUEnabled→autocomplete, PersistState→"Show values from last use", UseBoostLib→"Use Boost library". MRU values live in `search-mru.json`/`replace-mru.json`; last-used (persist) in `power-rename-last-run-data.json`.
- **"Show icon on context menu" has no Settings-UI toggle in current builds** — drive it via `power-rename-settings.json` `ShowIcon`. Behavior is observable on the synthetic menu (icon vs text-only); source `PowerRenameContextMenu/dllmain.cpp:73` (`GetIcon→null`).
- **The "Appear only in extended menu" classic `#32768` popup is not winapp-enumerable** — assert the Tier-1 *hide* (observed; `dllmain.cpp:108` `ECS_HIDDEN`) and cite `PowerRenameExt.cpp:84` (`E_FAIL` unless `CMF_EXTENDEDVERBS`) for the "still in extended menu" half.
- **PR registers on the directory *background* menu too** — the synthetic right-click often lands on background (View/Sort by/Group by/...) yet still shows/hides `Rename with PowerRename`, which is a valid, stable surface for menu-entry / icon-visibility / extended-menu-only present-absent comparisons.
- **`set-value` on search/replace DOES fire the preview** (TextChanged works, unlike CmdPal) — Apply button enabling/disabling is a reliable match/no-match signal. The search/replace Edit AutomationIds are random per launch (`txt-textbox-XXXX`); discover them each launch by name (`Edit "Search for"` / `Edit "Replace with"`).
- **Preview-row uncheck + column-header invokes need the Preview populated first** — set Search/Replace and wait ~500 ms for the regex engine; otherwise the invokes hit an empty list.
- **Boost library is read at PR process start** — close + relaunch PR after toggling.
- **Icon-on-menu and extended-only checks prefer registry over screenshot** — read HKCR `Extended` / `Icon` REG_SZ; more reliable + locale-independent.
- **Disk mutation is real** — run renames against `$env:TEMP\pr-test-<random>`, not real fixtures.
- **COM cache staleness** when re-checking verbs after enable/disable — call `Reset-PtShellComCache` from `scripts/pt-shell-verbs.ps1`.
## Source citations
- `<PT-repo>\src\modules\PowerRename\dllmain.cpp` — IExplorerCommand registration (no classic HKCR shadow on Win11).
- `<PT-repo>\src\modules\PowerRename\PowerRenameUILib\` — XAML for main PR window (toggle/checkbox AutomationIds).
- `<PT-repo>\src\modules\PowerRename\PowerRenameLib\Settings.cpp` — settings.json schema canonical property names.
## Ceiling
Expected **18/18 = 100%** from an interactive admin console session. Direct-CLI (#1) covers UI-driven items; synthetic-menu (#2) covers menu-presence assertions.
## Don'ts
- **Don't** try `Invoke-PtShellVerb 'PowerRename'` — returns False on Win11 (no classic registration). Use synthetic menu via `Invoke-PtContextMenuItem` or direct-CLI.
- **Don't** run rename operations against reusable fixtures — copy to a disposable temp folder.
- **Don't** trust screenshot-only for icon-on-menu or extended-only checks — registry inspection is faster + locale-independent.
- **Don't** skip the synthetic-menu test for the menu-presence assertion — CLI back-door PASSes even when the menu entry is correctly hidden (false-positive trap described in `references/explorer-context-menu-flow.md`).

View File

@@ -0,0 +1,122 @@
# Pre-flight checks, bootstrap, and state hygiene
This doc covers the **agent-runtime** environment probing and lifecycle hooks. Read alongside `SKILL.md` (the playbook) and `references/environment-setup.md` (one-time user env prep).
## Pre-flight checks (do these first; abort if any fails)
1. **Admin check**`Test-PtAdmin` must return the elevation level matching `[ADMIN: YES]` items in the module's checklist. If the module contains `[ADMIN: YES]` items and `Test-PtAdmin` returns `False`, **STOP** and tell the user "this module requires an elevated session". Do NOT silently mark those items BLOCKED-LACK-ADMIN — that hides a fixable env issue.
2. **PT runner present**`Test-PtRunnerAdmin` should show the runner exists. If it doesn't exist, start PowerToys (`Start-Process "$env:LOCALAPPDATA\PowerToys\PowerToys.exe"`).
3. **Module installed**`Get-PtModuleSettings -ModuleDir <ModuleDir>` (or `Get-CmdPalSettings` for CmdPal) returns non-null.
4. **Interactive-desktop availability + session attachment** — the single most common cause of false-BLOCKED reports is a session mismatch where the agent runs in an elevated **non-console session** (e.g. RDP that's been disconnected/minimized, fast user switching, run-as-different-user, or scheduled-task-with-highest-privilege). In that scenario `Test-PtAdmin=True` but `GetForegroundWindow()=0` and `SendInput` returns `ERROR_ACCESS_DENIED (5)` — input injection cannot reach the active desktop.
```powershell
# Sessions
$agentSession = [Diagnostics.Process]::GetCurrentProcess().SessionId
$consoleSession = (Get-Process explorer -EA SilentlyContinue | Select-Object -First 1).SessionId
"Agent session=$agentSession Console explorer session=$consoleSession"
# Foreground + Shell COM probe (use scripts/pt-session-diagnose.ps1 for the full version)
Add-Type 'using System; using System.Runtime.InteropServices; public class FG4 { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }'
$hasFg = $false
for ($i = 0; $i -lt 5; $i++) { if ([FG4]::GetForegroundWindow() -ne [IntPtr]::Zero) { $hasFg=$true; break }; Start-Sleep -Milliseconds 200 }
$shellOk = $false
try { $shellOk = (@((New-Object -ComObject Shell.Application).Windows()).Count -ge 0) } catch {}
"Interactive desktop: ForegroundOk=$hasFg ShellComOk=$shellOk"
if (-not $hasFg -and $agentSession -ne $consoleSession) {
Write-Host "===========================================================" -ForegroundColor Red
Write-Host "NON-INTERACTIVE SESSION DETECTED" -ForegroundColor Red
Write-Host "Agent is in Session $agentSession but the active console is Session $consoleSession." -ForegroundColor Red
Write-Host "SendInput, global hotkeys, and arrow-key navigation will NOT work here." -ForegroundColor Red
Write-Host "Items requiring input injection will be marked BLK-ENV up-front." -ForegroundColor Red
Write-Host "Mitigation: see references/environment-setup.md, or relaunch in console session:" -ForegroundColor Yellow
Write-Host " psexec -accepteula -h -i $consoleSession -s pwsh.exe" -ForegroundColor Yellow
Write-Host "===========================================================" -ForegroundColor Red
# Continue verification — schema/UIA/CLI-based tests still produce real evidence
}
```
**Key distinction** (all rows assume `Admin=True`):
- **ForegroundOk + ShellComOk** → Everything works — interactive elevated session.
- **ShellComOk only (ForegroundOk false)** → Non-interactive (e.g. Session ≠ console, RDP minimized, screen locked, screensaver). Only schema / UIA-invoke / CLI / Named-Event tests work. Mark input-injection items as `BLK-ENV` and **cite `references/environment-setup.md` in the report** so the user can fix env and re-run.
- **Neither (ShellComOk false)** → Session 0 / service context — even Shell COM fails. Very few tests possible.
5. **Discipline: try AT LEAST 2 distinct entry-paths before marking BLOCKED.** For Peek/FZ/Workspaces/Image Resizer/PowerRename/File Locksmith specifically, the obvious entry-path is the global hotkey but Shell.Application COM driving Explorer also works — see per-module profiles under `references/modules/`. Marking BLOCKED after trying only the CLI launch (a common trap) hides easily-PASS-able items in an interactive session.
## Bootstrap (paste at start of your verification script)
```powershell
$skill = '<this skill folder>' # the folder containing SKILL.md
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
$workspace = "$env:TEMP\verify-<Module>-$(Get-Date -Format yyyyMMdd-HHmmss)"
New-Item -ItemType Directory -Path $workspace, "$workspace\artifacts" | Out-Null
$report = "$workspace\verify-<Module>.md"
"# <Module> verification — $(Get-Date -Format 'yyyy-MM-dd HH:mm')" | Set-Content $report
"" | Add-Content $report
"## Pre-flight" | Add-Content $report
"- IsAdmin: $(Test-PtAdmin)" | Add-Content $report
"- PT runner: PID=$((Test-PtRunnerAdmin).Pid) Elevated=$((Test-PtRunnerAdmin).Elevated)" | Add-Content $report
# Then proceed with pre-flight checks #4-#6 above and write their results into the report.
```
## State hygiene (CRITICAL — always restore)
Wrap any settings/registry mutation in try/finally:
```powershell
# Per-item: settings.json edits
$bk = Backup-PtModuleSettings -ModuleDir <ModuleDir>
try {
# ... mutate + assert ...
} finally {
Restore-PtModuleSettings -ModuleDir <ModuleDir> -BackupPath $bk
}
# After GPO/admin tests
Remove-Item HKLM:\Software\Policies\PowerToys -Recurse -Force -EA SilentlyContinue
Remove-Item HKCU:\Software\Policies\PowerToys -Recurse -Force -EA SilentlyContinue
Remove-Item 'C:\Windows\PolicyDefinitions\PowerToys.admx' -Force -EA SilentlyContinue
Remove-Item 'C:\Windows\PolicyDefinitions\en-US\PowerToys.adml' -Force -EA SilentlyContinue
# Spawned processes (notepad, regedit, etc.) — kill by PID, not by name
foreach ($pid in $spawnedPids) { Stop-Process -Id $pid -Force -EA SilentlyContinue }
```
## Final wrap-up (run AFTER all per-item tables are written)
1. **Run state-hygiene cleanup** above for everything that wasn't restored per-item.
2. **Write the top-of-report summary** per `references/reporting-format.md` §B.
3. **Write the §G Retrospective** — reflect on the run itself: every friction (classified by source + severity + minutes/attempts cost + suggested fix), or `Everything was smooth — no friction encountered.` See `references/reporting-format.md` §G. Don't skip it; it's how the skill improves.
4. **Verify every screenshot referenced in the report actually exists on disk** (before the move, while paths still resolve under `$workspace`):
```powershell
$missing = Get-Content $report | Select-String 'artifacts/L\d+/step-\d+-[^\.\s]+\.(png|txt|log|json|ps1)' -AllMatches |
ForEach-Object { $_.Matches.Value } | Sort-Object -Unique |
Where-Object { -not (Test-Path (Join-Path $workspace $_)) }
if ($missing) { Write-Warning "Missing artifacts: $($missing -join ', ')" }
```
5. **Move the workspace to the sign-off archive** (LAST step, after the report + artifact check pass):
```powershell
$signoff = "$env:OneDrive\PowerToys\Module-Signoff"
New-Item -ItemType Directory -Path $signoff -Force | Out-Null
$final = Join-Path $signoff (Split-Path $workspace -Leaf)
Move-Item -Path $workspace -Destination $final -Force
$report = Join-Path $final (Split-Path $report -Leaf)
```
The report uses **relative** `artifacts/…` paths, so the whole tree moves intact.
6. **Print the FINAL (moved) report path** as the very last line of your response — the `…\Module-Signoff\verify-<Module>-<timestamp>\verify-<Module>.md` path, NOT the temp path.
## Hard rules
- **Never silently send keys via SendInput** to a target window without first calling `Assert-PtForegroundOrAbort -AppId <id>`. Keys silently leak to your terminal if the target isn't foreground.
- **Never mark BLOCKED without trying at least 2 distinct entry-paths from the drive-stack** (SKILL.md §2). If you can't drive the item, name the specific obstacle (not "I can't").
- **Never assume any external repo is cloned locally.** The helpers under `scripts/` are self-contained. Use `Test-Path` guards before referencing any external path.
- **Never invent test steps for a `[CLARITY: VAGUE-*]` item** — mark it **FAIL (cause: checklist-ambiguous)** and quote the original wording so the user can fix the checklist. The checklist is test code; an undefinable test is a broken test.
- **Always restore state** before exiting (even on error). State hygiene wraps every mutation in try/finally.
- **Separate the two FAIL causes**: *product* FAILs are bugs to file; *checklist* FAILs (stale feature or ambiguous spec) are items to rewrite/prune. If a large share of a module's items are checklist-FAILs, the checklist needs an overhaul before re-verifying — don't punt drivable items into a FAIL.
- **Never continue past 3 consecutive errors against the same item** — mark it BLOCKED with the concrete symptom/obstacle and move on. Per-item budget is ~5 minutes; if stuck longer, it's BLOCKED (name the wall).

View File

@@ -0,0 +1,45 @@
# Environment Variables — PowerToys release checklist
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
## Legend
Each item is annotated with two metadata tags:
**Admin requirement**:
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
**Clarity**:
- (no marker) - clear, has explicit assert
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
---
## Environment Variables (20 items)
- [ ] **[ADMIN: YES]** (L791) Launch as administrator ON - Launch Environment Variables and confirm that SYSTEM variables ARE editable and Add variable button is enabled
- [ ] **[ADMIN: YES]** (L792) Launch as administrator OFF - Launch Environment Variables and confirm that SYSTEM variables ARE NOT editable and Add variable button is disabled
- [ ] **[ADMIN: NO]** (L795) Add new User variable. Open OS Environment variables window and confirm that added variable is there. Also, confirm that it's added to "Applied variables" list.
- [ ] **[ADMIN: NO]** (L796) Edit one User variable. Open OS Environment variables window and confirm that variable is changed. Also, confirm that change is applied to "Applied variables" list.
- [ ] **[ADMIN: NO]** (L797) Remove one User variable. Open OS Environment variables window and confirm that variable is removed. Also, confirm that variable is removed from "Applied variables" list.
- [ ] **[ADMIN: NO]** (L801) Add new profile with no variables and name it "Test_profile_1" (referenced below by name)
- [ ] **[ADMIN: NO]** (L802) Edit "Test_profile_1": Add one new variable to profile e.g. name: "profile_1_variable_1" value: "profile_1_value_1"
- [ ] **[ADMIN: NO]** (L803) Add new profile "Test_profile_2": From "Add profile dialog" add two new variables (profile_2_variable_1:profile_2_value_1 and profile_2_variable_2:profile_2_value_2). Set profile to enabled and click Save. Open OS Environment variables window and confirm that all variables from the profile are applied correctly. Also, confirm that "Applied variables" list contains all variables from the profile.
- [ ] **[ADMIN: NO]** (L804) Apply "Test_profile_1" while "Test_profile_2" is still aplpied. Open OS Environment variables window and confirm that all variables from Test_profile_2 are unapplied and that all variables from Test_profile_1 are applied. Also, confirm that state of "Applied variables" list is updated correctly.
- [ ] **[ADMIN: NO]** (L805) Unapply applied profile. Open OS Environment variables window and confirm that all variables from the profile are unapplied correctly. Also, confirm that "Applied variables" list does not contain variables from the profile.
- [ ] **[ADMIN: NO]** (L808) To "Test_profile_1" add one existing variable from USER variables, e.g. TMP. After adding, change it's value to e.g "test_TMP" (or manually add variable named TMP with value test_TMP).
- [ ] **[ADMIN: NO]** (L809) Apply "Test_profile_1". Open OS Environment variables window and confirm that TMP variable in USER variables has value "test_TMP". Confirm that there is backup variable "TMP_PowerToys_Test_profile_1" with original value of TMP var. Also, confirm that "Applied variables" list is updated correctly - there is TMP profile variable, and backup User variable..
- [ ] **[ADMIN: NO]** (L810) Unapply "Test_profile_1". Open OS Environment variables window and confirm that TMP variable in USER variable has original value and that there is no backup variable. Also, confirm that "Applied variables" list is updated correctly.
- [ ] **[ADMIN: NO]** (L813) In "Applied variables" list confirm that PATH variable is shown properly: value of USER Path concatenated to the end of SYSTEM Path.
- [ ] **[ADMIN: NO]** (L814) To "Test_profile_1" add variable named PATH with value "path1;path2;path3" and click Save. Confirm that PATH variable in profile is shown as list (list of 3 values and not as path1;path2;path3).
- [ ] **[ADMIN: NO]** (L815) Edit PATH variable from "Test_profile_1". Try different options from ... menu (Delete, Move up, Move down, etc...). Click Save.
- [ ] **[ADMIN: NO]** (L816) Apply "Test_profile_1". Open OS Environment variables window and confirm that profile is applied correctly - Path value and backup variable. Also, in "Applied variables" list check that Path variable has correct value: value of profile PATH concatenated to the end of SYSTEM Path.
- [ ] **[ADMIN: NO]** (L819) Close the app and reopen it. Confirm that the state of the app is the same as before closing.
- [ ] **[ADMIN: NO]** (L821) "Test_profile_1" should still be applied (if not apply it). Delete "Test_profile_1". Confirm that profile is unapplied (both in OS Environment variables window and "Applied variables" list).
- [ ] **[ADMIN: NO]** (L822) Delete "Test_profile_2". Check profiles.json file and confirm that both profiles are gone.

View File

@@ -0,0 +1,35 @@
# File Locksmith — PowerToys release checklist
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
## Legend
Each item is annotated with two metadata tags:
**Admin requirement**:
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
**Clarity**:
- (no marker) - clear, has explicit assert
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
---
## File Locksmith (10 items)
- [ ] **[ADMIN: COND]** (L641) Right-click the executable file, select "Unlock with File Locksmith" and verify it shows up. (2 entries will show, since the installer starts two processes)
- [ ] **[ADMIN: COND]** (L642) End the tasks in File Locksmith UI and verify that closes the installer.
- [ ] **[ADMIN: COND]** (L643) Start the installer executable again and press the Refresh button in File Locksmith UI. It should find new processes using the files.
- [ ] **[ADMIN: COND]** (L644) Close the installer window and verify the processes are delisted from the File Locksmith UI. Close the window
- [ ] **[ADMIN: COND]** (L646) Right click the directory where the executable is located, select "Unlock with File Locksmith" and verify it shows up.
- [ ] **[ADMIN: COND]** (L647) Right click the drive where the executable is located, select "Unlock with File Locksmith" and verify it shows up. You can close the PowerToys installer now.
- [ ] **[ADMIN: COND]** (L649) Right click "Program Files", select "Unlock with File Locksmith" and verify "PowerToys.exe" doesn't show up.
- [ ] **[ADMIN: YES]** (L650) Press the File Locksmith "Restart as an administrator" button and verify "PowerToys.exe" shows up.
- [ ] **[ADMIN: YES]** (L651) Right-click the drive where Windows is installed, select "Unlock with File Locksmith" and scroll down and up, verify File Locksmith doesn't crash with all those entries being shown. Repeat after clicking the File Locksmith "Restart as an administrator" button.
- [ ] **[ADMIN: COND]** (L652) Disable File Locksmith in Settings and verify the context menu entry no longer appears.

View File

@@ -0,0 +1,43 @@
# Image Resizer — PowerToys release checklist
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
## Legend
Each item is annotated with two metadata tags:
**Admin requirement**:
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
**Clarity**:
- (no marker) - clear, has explicit assert
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
---
## Image Resizer (18 items)
- [ ] **[ADMIN: NO]** (L309) Disable the Image Resizer and check that `Resize with Image Resizer` is absent in the context menu
- [ ] **[ADMIN: NO]** (L310) Enable the Image Resizer and check that `Resize with Image Resizer` is present in the context menu (both Win11 modern and old menus)
- [ ] **[ADMIN: NO]** (L311) Remove one image size and add a custom image size. Open the Image Resize window from the context menu and verify changes are populated
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L312) Resize one image
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L313) Resize multiple images
- [ ] **[ADMIN: NO]** (L314) Open image resizer to resize a .gif and verify "Gif files with animations may not be correctly resized." warning appears
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L316) Resize images with Fill option
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L317) Resize images with Fit option
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L318) Resize images with Stretch option
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L320) Resize using dimension Centimeters
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L321) Resize using dimension Inches
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L322) Resize using dimension Percents
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L323) Resize using dimension Pixels
- [ ] **[ADMIN: NO]** (L325) Change Filename format to %1 - %2 - %3 - %4 - %5 - %6 and verify applied
- [ ] **[ADMIN: NO]** (L326) Check Use original date modified and verify modified date not changed for resized
- [ ] **[ADMIN: NO]** (L327) Check Make pictures smaller but not larger and verify smaller pictures not resized
- [ ] **[ADMIN: NO]** (L328) Check Resize the original pictures (don't create copies) and verify original is resized
- [ ] **[ADMIN: NO]** (L329) Uncheck Ignore the orientation and verify swapped W/H actually resizes if W!=H

View File

@@ -0,0 +1,14 @@
# Release checklist — per-module index
One file per module; a verification run loads only its module's file.
> **Scope:** only modules that have been verified end-to-end (with a sign-off report) are checked in here so far. The remaining modules' checklists will be added as each is verified.
| Module | Items | File |
|---|---:|---|
| Environment Variables | 20 | `environment-variables.md` |
| File Locksmith | 10 | `file-locksmith.md` |
| Image Resizer | 18 | `image-resizer.md` |
| New+ | 9 | `new-plus.md` |
| Peek | 18 | `peek.md` |
| PowerRename | 18 | `power-rename.md` |

View File

@@ -0,0 +1,34 @@
# New+ — PowerToys release checklist
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
## Legend
Each item is annotated with two metadata tags:
**Admin requirement**:
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
**Clarity**:
- (no marker) - clear, has explicit assert
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
---
## New+ (9 items)
- [ ] **[ADMIN: NO]** (L969) Verify NewPlus menu is in Explorer context menu. (Windows 11 tier 1 context menu only. May need Explorer restart.)
- [ ] **[ADMIN: NO]** (L971) Verify NewPlus menu is not in Explorer context menu.
- [ ] **[ADMIN: NO]** (L973) Verify the folder is created and empty.
- [ ] **[ADMIN: NO]** (L974) Copy a file to the templates folder, verify it's added to the New+ context menu and that if you select it the file is created.
- [ ] **[ADMIN: NO]** (L975) Copy a folder with files inside to the templates folder, verify it's added to the New+ context menu and that if you select it the folder and files inside are created.
- [ ] **[ADMIN: NO]** (L976) Delete all files and folders from inside the templates folder. Verify that no templates are available in the context menu.
- [ ] **[ADMIN: NO]** (L977) Disable and re-Enable New+ while the templates folder is still empty. Verify the default templates were copied over and are available in the context menu.
- [ ] **[ADMIN: NO]** (L979) Test the "Hide template filename extension" option in Settings.
- [ ] **[ADMIN: NO]** (L980) Test the "Hide template filename starting digits, spaces and dots" option in Settings.

View File

@@ -0,0 +1,43 @@
# Peek — PowerToys release checklist
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
## Legend
Each item is annotated with two metadata tags:
**Admin requirement**:
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
**Clarity**:
- (no marker) - clear, has explicit assert
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
---
## Peek (18 items)
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L697) Image
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L698) Text or dev file
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L699) Markdown file
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L700) PDF
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L701) HTML
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L702) Archive files (.zip, .tar, .rar)
- [ ] **[ADMIN: NO]** (L703) Any other not mentioned file (.exe for example) to verify the unsupported file view is shown
- [ ] **[ADMIN: NO]** (L706) Pin the window, switch between images of different size, verify the window stays at the same place and the same size.
- [ ] **[ADMIN: NO]** (L707) Pin the window, close and reopen Peek, verify the new window is opened at the same place and the same size as before.
- [ ] **[ADMIN: NO]** (L708) Unpin the window, switch to a different file, verify the window is moved to the default place.
- [ ] **[ADMIN: NO]** (L709) Unpin the window, close and reopen Peek, verify the new window is opened on the default place.
- [ ] **[ADMIN: NO]** (L712) By clicking a button.
- [ ] **[ADMIN: NO]** (L713) By pressing enter.
- [ ] **[ADMIN: NO]** (L716) Can use peek command to peek files
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L717) Peek can work without problem when a peek session is on
- [ ] **[ADMIN: NO]** (L719) Switch between files in the folder using `LeftArrow` and `RightArrow`, verify you can switch between all files in the folder.
- [ ] **[ADMIN: NO]** (L720) Open multiple files, verify you can switch only between selected files.
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-AMBIGUOUS]** (L721) Change the shortcut, verify the new one works.

View File

@@ -0,0 +1,43 @@
# PowerRename — PowerToys release checklist
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
## Legend
Each item is annotated with two metadata tags:
**Admin requirement**:
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
**Clarity**:
- (no marker) - clear, has explicit assert
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
---
## PowerRename (18 items)
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-AMBIGUOUS]** (L393) Check if disable and enable of the module works. (On Win11) Check if both old context menu and Win11 tier1 context menu items are present when module is enabled.
- [ ] **[ADMIN: NO]** (L394) Check that with the `Show icon on context menu` icon is shown and vice versa.
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-AMBIGUOUS]** (L395) Check if `Appear only in extended context menu` works.
- [ ] **[ADMIN: NO]** (L396) Enable/disable autocomplete.
- [ ] **[ADMIN: NO]** (L397) Enable/disable `Show values from last use`.
- [ ] **[ADMIN: NO]** (L399) Make Uppercase/Lowercase/Titlecase (could be selected only one at the time)
- [ ] **[ADMIN: NO]** (L400) Exclude Folders/Files/Subfolder Items (could be selected several)
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L401) Item Name/Extension Only (one at the time)
- [ ] **[ADMIN: NO]** (L402) Enumerate Items. Test advanced enumeration using different values for every field ${start=10,increment=2,padding=4}.
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L403) Case Sensitive
- [ ] **[ADMIN: NO]** (L404) Match All Occurrences. If checked, all matches of text in the `Search` field will be replaced with the Replace text. Otherwise, only the first instance of the `Search` for text in the file name will be replaced (left to right).
- [ ] **[ADMIN: NO]** (L406) Search with an expression (e.g. `(.*).png`)
- [ ] **[ADMIN: NO]** (L407) Replace with an expression (e.g. `foo_$1.png`)
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L408) Replace using file creation date and time (e.g. `$hh-$mm-$ss-$fff` `$DD_$MMMM_$YYYY`)
- [ ] **[ADMIN: NO]** (L409) Turn on `Use Boost library` and test with Perl Regular Expression Syntax (e.g. `(?<=t)est`)
- [ ] **[ADMIN: NO]** (L411) In the `preview` window uncheck some items to exclude them from renaming.
- [ ] **[ADMIN: NO]** (L412) Use the **Filter** (funnel) button above the file list → choose "Only show files that will be renamed" / "Show all files" to filter the preview.
- [ ] **[ADMIN: NO]** (L413) Use the **Select/deselect all** checkbox above the file list to toggle all rows checked/unchecked.

View File

@@ -0,0 +1,159 @@
# Reporting format
This doc defines the **required** report shape for every per-module verification run. Modeled on `PR-validation\Round1\PR-47211-validation\report.md` style — table-driven, reproducible, no prose narratives.
## §A — Per-item table (one per checklist item)
```markdown
## Item L<line_num> — <verbatim description from the module's checklist> — **<PASS|FAIL|BLOCKED>** <emoji>
**Admin**: <NO|COND|YES> | **Clarity**: <CLEAR|VAGUE-*|REWRITTEN> | **Category**: <PASS: verification method (free text) · FAIL: cause = product | checklist-stale | checklist-ambiguous · BLOCKED: a BLK-* reason>
### Verification steps performed
| # | Step | winapp / probe commands | Evidence / result |
|---|---|---|---|
| 1 | <what step 1 does> | `<exact command>`<br>`<another command if multiple>` | <what you observed; reference artifact filename> |
| 2 | <what step 2 does> | `<command>` | <evidence>; screenshot: `artifacts/L<line>/step-02-<name>.png` |
| 3 | ... | ... | ... |
### Artifacts produced
- `artifacts/L<line>/step-01-<name>.png` — <one-line description>
- `artifacts/L<line>/step-02-<name>.txt` — full inspect dump
- ...
### Verdict reasoning
- ✅ <assertion 1 that PASSed, with reference to the line of code / settings key / log line that proves it>
- ✅ <assertion 2>
- ❌ <if BLOCKED, the specific obstacle: "BLK-HARDWARE because MWB needs 2 physical PCs; this session has 1 ([System.Windows.Forms.Screen]::AllScreens.Count = 1)">
### Caveats (optional)
- <Any deviation from the user-documented flow, e.g. "Tested via settings.json write rather than UI checkbox because SelectionItemPattern.Select clobbers other selections in ListView.">
```
## §B — Top-of-report summary (write LAST, after all per-item tables)
```markdown
# <Module> verification report — <YYYY-MM-DD HH:MM>
## Summary
- **PASS**: <n> · **FAIL (product)**: <n> · **FAIL (checklist)**: <n> · **BLOCKED**: <n> · **Total**: <n> · **PASS%**: <n>
- **Top blocker categories**: <category>: <count>, <category>: <count>, ...
- **Items needing follow-up**: L<line> (<reason>), L<line> (<reason>), ...
- **State mutations performed + restored**: <count> settings.json edits restored, <count> registry keys removed, <count> fixture files deleted
## Pre-flight
- IsAdmin: <true|false>
- PT runner: PID=<n> Elevated=<true|false>
- <Module> settings file: <path> (exists=<true|false>)
- Interactive desktop: ForegroundOk=<true|false> ShellComOk=<true|false>
## Items
<all per-item tables here, in line_num order>
## Cleanup performed
- <list of every restore action taken>
## Retrospective (self-reflection on the run — write LAST)
<Per §G. If the whole run was frictionless, write exactly: **Everything was smooth — no friction encountered.**>
```
## §C — Required rules for step tables
1. **Every `winapp ui ...` command goes in the "winapp / probe commands" cell, verbatim, in backticks**, including `-w <hwnd>` / `-a <appId>` arguments and full selector strings. Reviewers will paste these into their own shell to reproduce.
2. **Every screenshot path goes in the "Evidence" cell** of the step that produced it, formatted as `screenshot: artifacts/L<line>/step-NN-<name>.png`. Never embed screenshots as `![...](...)` in the table body (breaks GitHub markdown rendering inside cells); just give the path.
3. **If a step has multiple commands**, separate them in the same cell with `<br>` so they render as one cell with multiple lines.
4. **PowerShell scriptlets > 3 lines**: write them to a separate `.ps1` in the artifacts folder and reference as ``script: `artifacts/L<line>/step-NN.ps1` `` in the cell. Keep the table cell to 1-3 lines.
5. **`—` (em dash) is allowed for non-CLI steps** like "Read sign-off entry + diff", "Create validation folder", "Cleanup notepad". Don't fabricate a command for steps that were purely cognitive or file-system level.
6. **Numbered steps must be contiguous** (1, 2, 3, ...). Don't skip numbers.
7. **At least one screenshot per PASS item if the item is a user-visible behavioral test**. Schema-only assertions (settings.json key check) don't need screenshots; behavioral tests (popup shown, dialog appeared, theme switched) do.
## §D — Reporting style
- Be specific. "Verified via UIA inspect returned `itm-calculator-XXXX`" beats "verified UIA".
- Include exact UIA selectors, log line text, settings.json keys, and screenshot filenames so the user can audit.
- For BLOCKED items, the 1-sentence reason should name **what specifically blocks**, e.g.:
- "BLK-HARDWARE: requires 2nd monitor; session has 1 (verified via `[System.Windows.Forms.Screen]::AllScreens.Count`)."
- "BLK-DRAG-REQUIRED: synthetic mouse drag insufficient for FZ snap-and-drag; needs real cursor motion."
- "BLK-ENV: SendInput returned ACCESS_DENIED (5) because Session $agentSession ≠ console Session $consoleSession. See `references/environment-setup.md`."
- "BLK-EXTERNAL-APP: requires real OpenAI API key; no key provisioned in test env."
## §E — Reporting anti-patterns (extra strict)
- Do NOT collapse multiple probe commands into a single English sentence like "verified via UIA". List every `winapp ui ...` command verbatim in a step row.
- Do NOT skip the step table for "trivial" items. Even a 1-step item (e.g. "Get-CmdPalSettings shows EnableDock=true") gets a 1-row table.
- Do NOT write screenshot references as `![alt](path)` inside table cells (GitHub renders markdown images poorly in cells). Write them as plain text path: `screenshot: artifacts/L<line>/step-NN-<name>.png`.
- Do NOT use "the test passed" as a screenshot caption — describe what's visible (e.g. "Settings page with FZ template grid showing 7 templates").
- Do NOT reference screenshots that you didn't actually capture. The final wrap-up `Test-Path` loop (see `references/pre-flight.md` §Final wrap-up step 3) will catch missing files; failing that check means the report is invalid.
- Do NOT cite source code line numbers (e.g. `CharacterMappings.cs:273`) without having actually read that line. If you cite source, the path must be real and the line number must contain what you claim.
## §F — Example item (reference: PR-47211 validation report style)
```markdown
## Item L455 — Activate Quick Accent (left Alt + arrow key) on a character, verify accents popup — **PASS** ✅
**Admin**: NO | **Clarity**: CLEAR | **Category**: drove full UIA flow + asserted accents popup
### Verification steps performed
| # | Step | winapp / probe commands | Evidence / result |
|---|---|---|---|
| 1 | Locate Settings window | `winapp ui list-windows --json` | `hwnd=263304`, `PowerToys.Settings` PID 31740 |
| 2 | Navigate to Quick Accent + expand language flyout | `winapp ui invoke QuickAccentNavItem -w 263304`<br>`winapp ui invoke btn-choosecharacter-1c4d -w 263304` | Page loaded; flyout expanded |
| 3 | Enumerate language list + screenshot | `winapp ui inspect btn-choosecharacter-1c4d -w 263304 --depth 5`<br>`winapp ui screenshot -w 263304 -o "artifacts/L455/step-03-language-list.png"` | 38 spoken + 6 special languages, alphabetic. screenshot: `artifacts/L455/step-03-language-list.png` |
| 4 | Single-language (French) popup test | `winapp ui invoke itm-french-1cac -w 263304`<br>`winapp ui inspect characters -w <popupHwnd> --depth 3`<br>`winapp ui screenshot -w <popupHwnd> -o "artifacts/L455/step-04-popup-FR-E.png"` | Popup chars for **E** = `é è ê ë €` (5), matches `FR.VK_E` in `CharacterMappings.cs:273`. screenshot: `artifacts/L455/step-04-popup-FR-E.png` |
| 5 | Restore baseline | — | settings.json reverted to `selected_lang="ALL"` |
### Artifacts produced
- `artifacts/L455/step-03-language-list.png` — Settings page with expanded language flyout
- `artifacts/L455/step-03-language-list.txt` — full UIA inspect dump of the list
- `artifacts/L455/step-04-popup-FR-E.png` — Popup with French only: `é è ê ë €`
### Verdict reasoning
- ✅ Popup characters match `CharacterMappings.cs` entries exactly (5/5 for FR.VK_E)
- ✅ Popup appeared within 500ms of hold-A; no crash
- ✅ Language list ordering is alphabetic by localized name
```
## §G — Retrospective (self-reflection)
After the run, reflect on the **process** (not the product) so the skill itself gets better over time. **If nothing slowed you down, write exactly one line: `Everything was smooth — no friction encountered.`** Otherwise, list each friction as a row and assign a source + severity.
```markdown
## Retrospective
| # | Friction (what slowed you / what was wrong) | Source | Severity | Cost | Suggested fix |
|---|---|---|---|---|---|
| 1 | <concrete description — what you expected vs what happened> | <one source tag below> | <HIGH/MED/LOW> | <~min wasted · N attempts> | <the doc line / helper function / tool behavior to change> |
```
**Source** — classify each friction into exactly one bucket so the right owner can fix it:
| Source tag | Meaning |
|---|---|
| `SKILL-UNCLEAR` | This skill's `SKILL.md` / `references/pre-flight.md` / module profile guidance was missing, ambiguous, or wrong. |
| `WINAPP-TOOL-BUG` | The `winapp` CLI itself misbehaved (crash, wrong output, flag not honored) — a product defect in the tool. |
| `WINAPP-DOC-UNCLEAR` | `references/winapp-ui-testing.md` was unclear/incorrect about how to use the tool (the tool worked; the docs misled you). |
| `HELPER-FLAW` | A shipped `scripts/*.ps1` had a logic bug, bad default, or wrong assumption. Name the function. |
| `PT-PRODUCT` | A PowerToys behavior/quirk made driving hard (distinct from a product **FAIL** — this is friction, not a checklist failure). |
| `CHECKLIST` | The checklist item itself was wrong/stale/ambiguous (e.g. describes a renamed or removed control). Note: this usually *also* produces a `FAIL (cause: checklist-*)` verdict on the item; log it here too so the checklist owner sees it as a process-improvement signal. |
| `ENVIRONMENT` | RDP/session/desktop/elevation friction not already covered by `references/environment-setup.md`. |
**Severity** — judge by *impact on future agents*, not just yourself:
- **HIGH** — most agents will hit it; blocks progress or wastes >10 min, or you needed a non-obvious workaround.
- **MED** — many agents may hit it; cost a few minutes or 2-3 retries; workaround exists once known.
- **LOW** — edge case or cosmetic; <1 min; noted for completeness.
**Cost** — be concrete: approximate minutes wasted **and** number of attempts (e.g. `~8 min · 3 attempts`). This is the raw signal for prioritizing skill fixes.
**Suggested fix** — point at the specific artifact to change: a doc line/section, a helper function name, or a `winapp` behavior to file. Vague reflections ("docs could be clearer") are not actionable — cite the line.
Example:
```markdown
## Retrospective
| # | Friction | Source | Severity | Cost | Suggested fix |
|---|---|---|---|---|---|
| 1 | `winapp ui inspect --depth 7 -w $hwnd` threw "Cannot bind argument" until I moved `-w` after `--depth`. | `WINAPP-TOOL-BUG` | MED | ~6 min · 3 attempts | Already noted in pitfall #14, but the tool should parse flag order — file against winapp. |
| 2 | SKILL.md §2.A says "wait 4s debounce" but PowerRename needed a full `Restart-PtRunner`; the module-owned-file note (pitfall #18) wasn't cross-linked from §2.A. | `SKILL-UNCLEAR` | HIGH | ~12 min · 4 attempts | Add an explicit "shell-ext modules → see pitfall #18" pointer inside §2.A. |
```

View File

@@ -0,0 +1,531 @@
# WinUI UI-testing mechanics (winapp ui)
> **Provenance:** Adapted from the `winui-ui-testing` skill in [microsoft/win-dev-skills](https://github.com/microsoft/win-dev-skills) (MIT, © Microsoft Corporation and Contributors), with PowerToys-specific edits. This is a **reference doc** for the `powertoys-module-verification` skill — it is intentionally not a standalone skill (no frontmatter), so it is not separately discovered.
Automated UI testing for WinUI 3 apps — generate a batch test script, run all tests in one pass, read results. Covers element assertions, interactions, value checking (TextBox, ComboBox, ToggleSwitch), file pickers, flyouts, dialogs, persistence, and accessibility audits.
### Approach
The goal of this skill is to validate UI and app functionality automatically, without manual interaction, by exercising the app's UI elements, verifying their state, and asserting that the app behaves as expected under test conditions.
There are two main approaches:
1. Interactive exploration — manually run the app, use `winapp ui <command>` to explore the UI tree, find AutomationIds, verify element properties, and test functionality interactively. This is useful for discovery, but slow and expensive if repeated for every test iteration.
2. Scripted batch testing — generate a `ui-tests.ps1` script that exercises all UI elements and asserts expected behavior in one pass. This allows you to run the tests automatically, capture results, and iterate quickly without manually interacting with the app each time.
Unless the user asked for interactive exploration, or you are unfamiliar with the code/app or need to explore the UI tree to discover AutomationIds for hidden or dynamically generated elements (flyouts, dialogs, lazy-loaded content), **prefer scripted batch testing** — it is faster, repeatable, and produces a record of pass/fail results that can be reviewed and acted on.
### `winapp ui` Verbs
`status`, `inspect`, `search`, `get-property`, `get-value`, `screenshot`, `invoke`, `click`, `set-value`, `focus`, `scroll`, `scroll-into-view`, `wait-for`, `list-windows`, `get-focused`. Run `winapp ui --cli-schema` for the complete command structure as JSON, or `winapp ui <verb> --help` for any single verb.
### Step 1: Use the Running App
If the app is already running, use its PID. **Do NOT relaunch** — use the PID already captured from the build step. If the app is not running, build and launch it using the guidance in the winui-dev-workflow skill.
### Step 2: Write the Test Script
**If you wrote the code:** Skip inspect — you already know all the AutomationIds and control structure from the XAML and code-behind. Write tests directly from that knowledge. Inspect misses popups, flyouts, dialogs, and lazy-loaded content anyway.
**If you're verifying code you didn't write:** Run inspect first to discover the UI:
```powershell
winapp ui inspect -a <PID> --interactive
```
Then read the XAML files to find AutomationIds that aren't currently visible (flyout items, dialog buttons, secondary pages).
Create a `ui-tests.ps1` file that tests all the app's requirements in one pass:
```powershell
# ui-tests.ps1
param([Parameter(Mandatory)][int]$AppPid)
# NOTE: Do NOT name the parameter $Pid — it's read-only in PowerShell
$ErrorActionPreference = 'Continue'
$pass = 0; $fail = 0; $results = @()
# Get main window HWND (avoids PopupHost interference with JSON parsing)
$windows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json
$hwnd = ($windows | Where-Object { $_.title -ne "PopupHost" } | Select-Object -First 1).hwnd
function Test-UI {
param([string]$Name, [scriptblock]$Script)
# IMPORTANT: Inside $Script, use 'throw' to signal failure — NOT 'exit 1'
# (exit terminates the entire script, not just the test)
try {
$output = & $Script 2>&1
if ($LASTEXITCODE -eq 0) {
$script:pass++; $script:results += @{ name = $Name; status = "PASS" }
} else {
$script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$output" }
}
} catch {
$script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$_" }
}
}
# ─── Element Existence ───
Test-UI "NavHome exists" { winapp ui wait-for "NavHome" -a $AppPid -t 3000 }
Test-UI "NavSettings exists" { winapp ui wait-for "NavSettings" -a $AppPid -t 3000 }
# ─── Navigation ───
Test-UI "Navigate to Settings" { winapp ui invoke "NavSettings" -a $AppPid }
Test-UI "Settings page loaded" { winapp ui wait-for "TxtUserName" -a $AppPid -t 3000 }
# ─── Interactions ───
Test-UI "Set username" { winapp ui set-value "TxtUserName" "TestUser" -a $AppPid }
Test-UI "Click Save" { winapp ui invoke "BtnSave" -a $AppPid } # commits the TextBox binding
Test-UI "Username value set" {
winapp ui wait-for "TxtUserName" -a $AppPid --value "TestUser" -t 2000
}
# ─── Value assertions for different control types ───
Test-UI "Theme is System default" {
winapp ui wait-for "CmbTheme" -a $AppPid --value "System default" -t 2000
}
Test-UI "Logging is off" {
winapp ui wait-for "TglLogging" -a $AppPid --value "Off" -t 2000
}
# ─── Accessibility Audit ───
# Only audit controls in the app's main window (exclude OS picker/popup controls)
$allElements = (winapp ui inspect -a $AppPid --interactive --json 2>$null | ConvertFrom-Json).elements
$appElements = @($allElements | Where-Object {
$_.type -match 'Button|TextBox|ComboBox|CheckBox|ToggleSwitch|TabItem|Edit' -and
$_.name -notmatch 'Minimize|Maximize|Close|System' -and # window chrome
$_.className -notmatch 'PickerHost|#32770|CabinetWClass' # OS dialogs
})
$missingId = @($appElements | Where-Object { -not $_.automationId })
if ($missingId.Count -eq 0) {
$pass++; $results += @{ name = "All app controls have AutomationId"; status = "PASS" }
} else {
$fail++
$names = ($missingId | ForEach-Object { "$($_.type) '$($_.name)'" }) -join ", "
$results += @{ name = "AutomationId coverage"; status = "FAIL"; detail = "Missing: $names" }
}
# ─── State Screenshots (capture each meaningful state for visual review) ───
New-Item -ItemType Directory -Force -Path "screenshots" | Out-Null
winapp ui screenshot -a $AppPid -o "screenshots/01-initial.png" 2>$null
# ...take more screenshots after key interactions above (mode switches, dialogs opened, etc.)
# ─── Final Screenshot ───
winapp ui screenshot -a $AppPid -o "test-screenshot.png" 2>$null
# ─── Results ───
Write-Host "`nPassed: $pass | Failed: $fail"
$results | Where-Object { $_.status -eq "FAIL" } | ForEach-Object {
Write-Host " FAIL: $($_.name)$($_.detail)" -ForegroundColor Red
}
$results | ConvertTo-Json | Out-File "test-results.json"
if ($fail -gt 0) { exit 1 } else { exit 0 }
```
### What to Test
Write tests for **every requirement** from the user's prompt:
| Requirement type | Test approach |
|---|---|
| "Has a button that does X" | `search` to verify exists, `invoke` to click, `wait-for --value` to check result |
| "Text field shows value" | `wait-for "TxtName" --value "expected"` — works for TextBox, TextBlock, labels |
| "Status bar contains text" | `wait-for "StatusBar" --value "words" --contains` — substring match for dynamic content |
| "Dropdown is set to X" | `wait-for "CmbTheme" --value "Dark"` — reads the selected item automatically |
| "Toggle is on/off" | `wait-for "TglFeature" --value "On"` — reads the toggle state |
| "Navigation between pages" | `invoke` nav item, `wait-for` a page-specific element to appear |
| "Open file dialog" | `invoke` trigger, `list-windows` to find picker HWND, interact with `-w` |
| "Save file dialog" | Same as open — find picker with `list-windows`, `set-value` filename, `invoke` Save |
| "Right-click context menu" | `click --right` on element, `invoke` the flyout MenuItem |
| "Confirmation dialog" | `invoke` trigger, `search` for dialog buttons, `invoke` Primary/Secondary/Close |
| "Data persists" | Set values, `invoke` a button (to commit bindings), verify data file on disk (`Get-Content` + `ConvertFrom-Json`) |
| "All controls accessible" | `inspect --interactive --json` + check all have AutomationId |
### Step 3: Run and Read Results
```powershell
.\ui-tests.ps1 -AppPid <PID>
```
Read `test-results.json` for structured pass/fail. Only fix code if tests fail.
### Step 3.5: Look at the Screenshots
UIA assertions don't see clipping, overlap, wrong theming, or controls bleeding past their container — UIA returns `PASS` while the app is visually broken. **Capture screenshots with `winapp ui screenshot` and view each PNG.**
Capture the initial state and any state after a major interaction (the State Screenshots block in the script template above handles this).
**Visual checklist — fail the run if any item is `no`:**
- [ ] No unintended scrollbars
- [ ] No text ending in `…` that shouldn't be
- [ ] Hero elements fully visible (not sliced)
- [ ] Right-edge controls fully visible
- [ ] No overlapping rows
- [ ] Content uses the available width — no asymmetric dead zones (e.g. content pinned to one edge leaving empty space on the other)
- [ ] Spacing intentional — not cramped, not unintentionally vast
- [ ] Theming matches the user's ask (Light/Dark/HighContrast if relevant)
- [ ] Focus/hover/error states render if tested
If the checklist fails, it's a bug — fix before declaring done. Window too small → grow per `winui-design` Step 4.
### Step 4: Fix and Rerun (if the user asked for it)
If tests fail:
1. Read the failure details from `test-results.json`
2. Batch-fix all issues in one pass
3. Rebuild with `.\BuildAndRun.ps1` (blocking mode — shows crash info if the fix broke something)
4. Rerun `.\ui-tests.ps1 -AppPid <PID>` (parse PID from the `launched (PID: XXXXX)` output)
**Maximum 2 fix-and-rerun cycles.** If the same tests keep failing after 2 cycles, report them as known issues and move on — do not keep iterating.
### Assertion Reference
Use `wait-for --value` as the primary assertion — it uses a smart fallback chain that reads the right value for any control type:
| Control type | `--value` reads from | Example |
|---|---|---|
| TextBlock / Label | Name property | `wait-for "LblTitle" --value "Home"` |
| TextBox / NumberBox | ValuePattern | `wait-for "TxtName" --value "John"` |
| RichEditBox | TextPattern | `wait-for "Editor" --value "Hello"` |
| ComboBox | Selected item (SelectionPattern) | `wait-for "CmbTheme" --value "Dark"` |
| ToggleSwitch | Toggle state (On/Off) | `wait-for "TglDark" --value "On"` |
| CheckBox | Toggle state (On/Off) | `wait-for "ChkAgree" --value "On"` |
**Full assertion commands:**
| Assertion | Command |
|---|---|
| Element exists | `winapp ui wait-for "Id" -a PID -t 3000` |
| Element has exact value | `winapp ui wait-for "Id" -a PID --value "expected" -t 3000` |
| Value contains text | `winapp ui wait-for "Id" -a PID --value "words" --contains -t 3000` |
| Element gone | `winapp ui wait-for "Id" -a PID --gone -t 3000` |
| Specific property | `winapp ui wait-for "Id" -a PID -p IsEnabled --value "True" -t 3000` |
| Button clickable | `winapp ui invoke "Id" -a PID` (exit code 0) |
| Set then verify | `winapp ui set-value "Id" "text" -a PID` then `wait-for --value` |
| Screenshot | `winapp ui screenshot -a PID -o path.png` |
| Dialog appeared | `winapp ui list-windows -a PID --json` (check window count) |
| Right-click menu | `winapp ui click "Id" -a PID --right` then `wait-for` menu item |
| Read raw property | `winapp ui get-property "Id" -a PID -p IsEnabled --json` |
| Read current value (no wait) | `(winapp ui get-value "Id" -a PID --json \| ConvertFrom-Json).text` — always pass `--json` when capturing into a variable (plain stdout can include advisory text like "Auto-selected HWND … from N windows"); otherwise prefer `wait-for --value` |
| Scroll item into view | `winapp ui scroll-into-view "Id" -a PID` — call before `wait-for` on virtualized ListView/repeater items below the fold |
| Set keyboard focus | `winapp ui focus "Id" -a PID` — cleaner than clicking another control to trigger a TextBox `LostFocus` commit |
### Testing File Pickers
File/folder pickers (FileOpenPicker, FileSavePicker, FolderPicker) run in a separate `PickerHost` process but are fully interactable. The picker appears as an owned dialog window.
```powershell
# 1. Trigger the picker
winapp ui invoke "BtnOpenFile" -a $AppPid
# 2. Find the picker window (it's a dialog owned by the app window)
Start-Sleep 1
$allWindows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json
$picker = $allWindows | Where-Object { $_.title -match "Open|Save" }
$pickerHwnd = $picker.hwnd
# 3. Interact with the picker using -w <HWND>
# Type a filename:
winapp ui set-value "FileNameControlHost" "test.txt" -w $pickerHwnd
# Click Open/Save:
winapp ui invoke "Open" -w $pickerHwnd # or "Save", "Cancel"
# Or cancel:
winapp ui invoke "Cancel" -w $pickerHwnd
# 4. Verify the app processed the file
winapp ui wait-for "StatusBar" -a $AppPid -p Name --value "opened" -t 3000
```
**Tip:** Use `winapp ui inspect -w <pickerHwnd> --interactive` to discover the picker's controls — they include the folder tree, file list, filename textbox, and Open/Cancel buttons.
### Testing Context Menus and Flyouts
MenuFlyouts and ContextFlyouts are fully testable. They appear in the UI automation tree when open.
```powershell
# 1. Right-click to open a ContextFlyout
winapp ui click "LstItems" -a $AppPid --right
Start-Sleep 0.5
# 2. The flyout MenuItems appear in the tree immediately
# Find them with inspect or search:
winapp ui inspect -a $AppPid --interactive # shows MnuCopy, MnuDelete, etc.
# 3. Click a flyout item
winapp ui invoke "MnuCopy" -a $AppPid
# 4. Verify the action
winapp ui wait-for "StatusText" -a $AppPid -p Name --value "Copied" -t 2000
```
**For MenuBar flyouts** (File, Edit, View menus):
```powershell
# Click the menu header to open
winapp ui invoke "FileMenu" -a $AppPid
Start-Sleep 0.5
# Click the sub-item
winapp ui invoke "MenuSaveAs" -a $AppPid
```
### Testing ContentDialogs
ContentDialogs are in-app controls (same window) — they appear directly in the UI tree when shown.
```powershell
# 1. Trigger the dialog
winapp ui invoke "BtnDelete" -a $AppPid
Start-Sleep 0.5
# 2. The dialog buttons appear in the tree
# For a standard confirmation dialog:
winapp ui search "Primary" -a $AppPid --json # finds the primary button
winapp ui invoke "Primary" -a $AppPid # click "Yes"/"Delete"/"Save"
# Or:
winapp ui invoke "Secondary" -a $AppPid # click "No"/"Don't Save"
winapp ui invoke "Close" -a $AppPid # click "Cancel"
# 3. Wait for dialog to dismiss
winapp ui wait-for "Primary" -a $AppPid --gone -t 3000
```
**Tip:** ContentDialog buttons often don't have custom AutomationIds — use `inspect` to find the actual selector (slug or text match).
### Key Gotchas
- **`set-value` does NOT commit default TextBox bindings** — WinUI 3 `x:Bind TwoWay` on TextBox.Text updates the ViewModel on `LostFocus` by default. UIA `set-value` changes the text but doesn't trigger focus events. **Fix:** apps should use `UpdateSourceTrigger=PropertyChanged` on TextBox bindings (see design skill). If the app doesn't, `invoke` a button or `click` another element after `set-value` to trigger `LostFocus`.
- **Verify persistence via the data file, not UI relaunch** — killing and relaunching a packaged app from a test script is fragile (MSIX registration timing, PID issues). Instead, check the data file on disk: `Get-Content $dataFile | ConvertFrom-Json` and verify expected values.
- **Use `$AppPid` not `$Pid`** — `$Pid` is a read-only automatic variable in PowerShell
- **Use `--value` without `-p`** — it auto-detects the right UIA pattern (TextPattern → ValuePattern → TogglePattern → SelectionPattern → Name). Only use `-p PropertyName --value` when you need a specific property like `IsEnabled`
- **File pickers need `-w <HWND>`** — they run in a separate PickerHost process, so `-a PID` won't find them. Use `list-windows` to discover the picker HWND first
- **Flyouts need a short `Start-Sleep`** after triggering — the menu items appear in the tree asynchronously
### CRITICAL — `invoke` vs `click`: choose the right verb
**`winapp ui invoke <sel>`** dispatches through UIA's **`InvokePattern` via COM IPC**:
- ✅ Bypasses Windows UIPI (User Interface Privilege Isolation)
- ✅ Works even when your test runs elevated and the target is non-elevated AppX
- ✅ Does NOT steal foreground / does NOT trigger focus-loss handlers
- ✅ Works on Buttons, ListItems, ToggleSwitches, CheckBoxes — anything that exposes `InvokePattern` or `TogglePattern`
- ❌ Does NOT work on elements without an UIA action pattern (plain Grid, Text, Pane) — error message says "does not support any invoke pattern"
**`winapp ui click <sel>`** uses Win32 **`SendInput`** under the hood:
-**BLOCKED by UIPI** when source is elevated and target is non-elevated (or any AppX) — error: `SendInput failed — the target window may be elevated`
- ❌ Triggers foreground change → can dismiss popups, dialogs, AppX windows that hide on deactivation
- ✅ Only use when you genuinely need a synthetic mouse click (e.g. testing mouse hover/right-click flyouts where InvokePattern is unavailable)
- ✅ Subject to your process having interactive desktop access
**Rule of thumb**: try `invoke` first; only fall back to `click` if the target lacks InvokePattern AND you have a non-elevated test runner.
### CRITICAL — DataTemplate AutomationId vs ListItem InvokePattern
When XAML binds `AutomationProperties.AutomationId="{x:Bind <DataProperty>}"` inside a `ListView.ItemTemplate`'s `<DataTemplate>`, the AutomationId lives on the **inner Grid (Group)** the template produces — NOT on the outer ListItem the ListView wraps around it. The outer ListItem is what carries `InvokePattern`.
Concrete example (CmdPal PR #48033 binds Command.Id this way):
```powershell
# This FAILS with "does not support any invoke pattern":
winapp ui invoke 'com.microsoft.cmdpal.calculator' -w $hwnd
# Element grp-commicrosoftcmd-XXXX (Group) does not support any invoke pattern.
# No invokable ancestor was found.
# This WORKS — find by Name (matches all 3 siblings), pick the ListItem child:
$r = winapp ui search 'Calculator' -w $hwnd --json | ConvertFrom-Json
$li = $r.matches | Where-Object type -eq 'ListItem' | Select-Object -First 1
winapp ui invoke $li.selector -w $hwnd # selector like 'itm-calculator-7e3f'
```
If you encounter "does not support any invoke pattern" while trying to use a data-bound AutomationId, this is almost always the cause. The fix is to search by Name and invoke the sibling ListItem.
### CRITICAL — Keystroke input that bypasses UIPI (PostMessage)
`winapp ui` has no `send-keys` verb. For keystroke input into elevated/AppX targets where SendInput fails, use **inline Win32 `PostMessage WM_KEYDOWN/WM_KEYUP`** which goes through the target's message queue without UIPI checks:
```powershell
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class K {
[DllImport("user32.dll", CharSet=CharSet.Auto)]
public static extern bool PostMessage(IntPtr h, uint msg, IntPtr wp, IntPtr lp);
public const uint WM_KEYDOWN = 0x0100;
public const uint WM_KEYUP = 0x0101;
}
"@
function Send-KeyToHwnd {
param([IntPtr]$Hwnd, [byte]$Vk)
[void][K]::PostMessage($Hwnd, [K]::WM_KEYDOWN, [IntPtr]$Vk, [IntPtr]0)
Start-Sleep -Milliseconds 30
[void][K]::PostMessage($Hwnd, [K]::WM_KEYUP, [IntPtr]$Vk, [IntPtr]0)
}
# Common VK codes:
# 0x08 Backspace 0x09 Tab 0x0D Enter 0x1B Escape
# 0x25 Left 0x26 Up 0x27 Right 0x28 Down
Send-KeyToHwnd -Hwnd $h -Vk 0x28 # Down arrow
Send-KeyToHwnd -Hwnd $h -Vk 0x0D # Enter
```
**Caveats**:
- WinUI3 apps' raw-input hooks may NOT process some keys via WM_KEYDOWN — `Esc` in particular often goes ignored (use BackButton invoke instead). Arrow keys + Enter typically work for ListView navigation.
- PostMessage returns immediately; allow 50-200 ms before reading state.
- Repeat `Send-KeyToHwnd` calls work for multi-step navigation (Down × 5 to scroll, then Enter).
### CRITICAL — Global hotkeys / PowerToys activation chords (SendInput, verified working)
`PostMessage` above targets a specific window's queue. To fire a **global hotkey** (e.g. a PowerToys activation chord like `Win+Shift+C`) you must inject into the **system input stream** with `SendInput` so the low-level keyboard hook (`WH_KEYBOARD_LL`) sees it. This **works for Win+ chords** — the common belief that "Win+ chords can't be injected" is false; it's almost always a **marshaling bug** (`SendInput` returns `0`, `GetLastError()==87`) from building the `INPUT[]` array in PowerShell. Build the array in C#:
```powershell
Add-Type @"
using System; using System.Runtime.InteropServices; using System.Collections.Generic;
public static class Inj {
[StructLayout(LayoutKind.Sequential)]
struct INPUT { public uint type; public KEYBDINPUT ki; public int p1; public int p2; } // p1/p2 pad the union -> cb=40 on x64
[StructLayout(LayoutKind.Sequential)]
struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
[DllImport("user32.dll", SetLastError=true)] static extern uint SendInput(uint n, INPUT[] p, int cb);
const uint KEYUP = 0x0002;
static INPUT K(ushort vk, bool up){ INPUT i=new INPUT(); i.type=1; i.ki.wVk=vk; i.ki.dwFlags=up?KEYUP:0; return i; }
public static uint Chord(ushort[] mods, ushort key){ // mods down -> key tap -> mods up (reverse)
var l=new List<INPUT>();
foreach(var m in mods) l.Add(K(m,false));
l.Add(K(key,false)); l.Add(K(key,true));
for(int i=mods.Length-1;i>=0;i--) l.Add(K(mods[i],true));
var a=l.ToArray(); return SendInput((uint)a.Length,a,Marshal.SizeOf(typeof(INPUT)));
}
}
"@
# LWIN=0x5B CTRL=0x11 SHIFT=0x10 ALT=0x12 ; main key VK from the module's settings.json "code"
$sent = [Inj]::Chord([uint16[]]@(0x5B,0x10), [uint16]0x43) # Win+Shift+C (Color Picker)
if ($sent -eq 0) { throw "SendInput failed err=$([Runtime.InteropServices.Marshal]::GetLastWin32Error())" }
```
**Caveats**:
- The injector must run at the **same or higher integrity level** as the hook owner (PowerToys runner). Default per-user installs run the runner at Medium IL, so a normal shell works; if the runner is elevated, run the injector elevated too (otherwise UIPI silently drops the injection).
- Must run in the interactive desktop session.
- OS-reserved chords (Win+L, Win+Tab) are consumed by Windows before any hook and cannot be injected this way.
- Verify the result via the runner trace log line `… hotkey is invoked from Centralized keyboard hook` (`%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs\runner-log_<date>.log`) and/or the module's observable side-effect (overlay window, spawned editor process).
### CRITICAL — Verify foreground BEFORE every SendInput targeting a specific window
`SendInput` injects into the **session-wide** input stream — it goes to whatever IS foreground at the moment. If your target window has lost foreground (very common with AppX windows), the keys silently land in another window (often your own terminal) with no error returned.
Always check the foreground state immediately before calling `SendInput`. For winapp ui's output, the literal substring `foreground` appears in the line for the foreground window:
```powershell
function Test-AppForeground {
param([Parameter(Mandatory)][string]$AppId)
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
return ($r -match 'foreground')
}
# Force foreground (works ONCE per session reliably; subsequent attempts may be blocked by
# Windows foreground-lock):
function Force-AppForeground {
param([Parameter(Mandatory)][IntPtr]$Hwnd, [int]$ProcessId)
Add-Type -TypeDefinition @'
using System; using System.Runtime.InteropServices;
public static class Fg {
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
[DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int pid);
}
'@ -EA SilentlyContinue
[Fg]::AllowSetForegroundWindow($ProcessId) | Out-Null
[Fg]::ShowWindow($Hwnd, 9) | Out-Null # SW_RESTORE
$fg = [Fg]::GetForegroundWindow(); $fgPid = 0
$fgThread = [Fg]::GetWindowThreadProcessId($fg, [ref]$fgPid)
$curThread = [Fg]::GetCurrentThreadId()
if ($fgThread -ne 0 -and $fgThread -ne $curThread) { [Fg]::AttachThreadInput($curThread, $fgThread, $true) | Out-Null }
[Fg]::BringWindowToTop($Hwnd) | Out-Null
[Fg]::SetForegroundWindow($Hwnd) | Out-Null
if ($fgThread -ne 0 -and $fgThread -ne $curThread) { [Fg]::AttachThreadInput($curThread, $fgThread, $false) | Out-Null }
Start-Sleep -Milliseconds 400
}
# Guard pattern: abort instead of silently sending keys to wrong window
if (-not (Test-AppForeground -AppId 'Microsoft.CmdPal.UI')) {
Force-AppForeground -Hwnd $h -ProcessId $pid
if (-not (Test-AppForeground -AppId 'Microsoft.CmdPal.UI')) {
throw 'Cannot force CmdPal foreground; aborting SendInput batch'
}
}
# ... now safe to SendInput ...
```
**Tip**: when foreground cannot be reliably maintained, prefer `winapp ui set-value` (UIA-IPC, no foreground required) or `winapp ui invoke` (UIA InvokePattern, no foreground required) instead of SendInput.
### CRITICAL — `set-value` bypasses TextChanged for some apps (CmdPal alias detection)
`winapp ui set-value` writes the value through UIA's ValuePattern, which fires a programmatic value-change event. **It does NOT raise the `TextBox.TextChanged` event** the way real keystrokes do. For apps whose logic listens to `TextChanged` rather than to property changes — most notably CmdPal's alias detection (typing `=`, `<`, `>`, `:`, `$`, `??`, `)` in MainSearchBox triggers navigation to a provider sub-page) — `set-value` will set the text but the alias will NOT activate.
Workarounds:
- For plain queries: `winapp ui set-value` works fine (CmdPal still re-runs all providers on value change).
- For alias-triggered navigation: use **real keystrokes** via Force-AppForeground + SendInput, typing one character at a time with ~60-100ms delay so the alias detector sees the TextChanged sequence.
- Alternative: invoke the provider tile directly by its stable AutomationId (e.g. `winapp ui invoke 'com.microsoft.cmdpal.calculator' -w $hwnd`) when you only need the destination page, not the alias path.
### CRITICAL — Stunted UIA tree recovery
After ~30+ rapid `set-value` calls or after AppX has been interactive too long, an AppX window's UIA tree can degrade to a "stunted" state where `winapp ui inspect -w $h --depth 6` returns only ~5 elements (TitleBar / Close / Min / Max / RootPane) — even though the app looks fine visually.
Probe + recover pattern:
```powershell
# Probe: any healthy ListView-based AppX has >50 UIA nodes at depth 6
$probe = winapp ui inspect -w $h --depth 6 --json | ConvertFrom-Json
$nodes = 0
$stack = [System.Collections.Stack]::new()
if ($probe.windows[0].elements) { foreach ($e in $probe.windows[0].elements) { $stack.Push($e) } }
while ($stack.Count -gt 0) {
$n = $stack.Pop(); $nodes++
if ($n.PSObject.Properties['children']) { foreach ($c in $n.children) { $stack.Push($c) } }
}
if ($nodes -lt 6) {
Write-Warning "UIA tree stunted ($nodes nodes); restarting AppX"
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | ForEach-Object {
Stop-Process -Id $_.Id -Force
Wait-Process -Id $_.Id -Timeout 5 -EA SilentlyContinue
}
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
Start-Sleep 5
# Re-resolve HWND with list-windows
}
```
### Settings.json mutation safety contract
When the only realistic way to reach a needed test state is editing the app's persistent settings (e.g. multi-select that the UI's `SelectionItemPattern.Select` clobbers), wrap mutations with **byte-identical backup + restore-on-exit**:
```powershell
$settings = "$env:LOCALAPPDATA\Packages\Microsoft.CommandPalette_8wekyb3d8bbwe\LocalState\settings.json"
$backup = "$env:TEMP\settings-backup-$(Get-Random).json"
$origBytes = [System.IO.File]::ReadAllBytes($settings)
[System.IO.File]::WriteAllBytes($backup, $origBytes)
try {
# 1. Stop the AppX so we can write the file (apps usually hold it open)
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | Stop-Process -Force
Start-Sleep 1
# 2. Mutate
$j = $origBytes | ForEach-Object { [char]$_ } | Join-String | ConvertFrom-Json
$j.SomeKey = 'TestValue'
[System.IO.File]::WriteAllBytes($settings, [System.Text.Encoding]::UTF8.GetBytes(($j | ConvertTo-Json -Depth 10)))
# 3. Restart AppX so it re-reads the mutated settings
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
Start-Sleep 5
# 4. ... run your test ...
} finally {
# ALWAYS restore — verify byte-identical via length + SHA256
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue
Start-Sleep 1
[System.IO.File]::WriteAllBytes($settings, $origBytes)
$check = [System.IO.File]::ReadAllBytes($settings)
if ($check.Length -ne $origBytes.Length) { Write-Error "Restore length mismatch!" }
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
}
```
**Important**: this should be used ONLY when the UI route is unreachable. Any setting flippable through the AppX Settings UI should be flipped that way instead (it's the documented user flow and tests real binding code).

View File

@@ -0,0 +1,76 @@
# scripts/pt-admin-probe.ps1
# Verify the current session is elevated AND that PT runner inherits the admin token.
if (-not ('PtTok' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class PtTok {
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool OpenProcessToken(IntPtr h, uint da, out IntPtr t);
[DllImport("advapi32.dll", SetLastError=true)]
public static extern bool GetTokenInformation(IntPtr t, uint c, IntPtr ti, uint l, out uint rl);
[DllImport("kernel32.dll")] public static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll")] public static extern IntPtr OpenProcess(uint da, bool inh, int pid);
[DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h);
}
'@
}
function Test-PtAdmin {
<#
.SYNOPSIS
Check whether the current session is elevated by reading the process token's TokenElevation
information class (20). Returns $true if elevated.
#>
[CmdletBinding()] param()
$t = [IntPtr]::Zero
[PtTok]::OpenProcessToken([PtTok]::GetCurrentProcess(), 8, [ref]$t) | Out-Null
$ti = [Runtime.InteropServices.Marshal]::AllocHGlobal(4)
$rl = 0
try {
[PtTok]::GetTokenInformation($t, 20, $ti, 4, [ref]$rl) | Out-Null
return ([Runtime.InteropServices.Marshal]::ReadInt32($ti) -eq 1)
} finally {
[Runtime.InteropServices.Marshal]::FreeHGlobal($ti)
[PtTok]::CloseHandle($t) | Out-Null
}
}
function Test-ProcessElevated {
<#
.SYNOPSIS
Check whether a specific PID is elevated (TokenElevation = 1).
#>
[CmdletBinding()] param([Parameter(Mandatory)][int]$ProcessId)
$proc = [PtTok]::OpenProcess(0x1000, $false, $ProcessId) # PROCESS_QUERY_LIMITED_INFORMATION
if ($proc -eq [IntPtr]::Zero) { return $null }
try {
$t = [IntPtr]::Zero
if (-not [PtTok]::OpenProcessToken($proc, 8, [ref]$t)) { return $null }
try {
$ti = [Runtime.InteropServices.Marshal]::AllocHGlobal(4)
$rl = 0
try {
[PtTok]::GetTokenInformation($t, 20, $ti, 4, [ref]$rl) | Out-Null
return ([Runtime.InteropServices.Marshal]::ReadInt32($ti) -eq 1)
} finally { [Runtime.InteropServices.Marshal]::FreeHGlobal($ti) }
} finally { [PtTok]::CloseHandle($t) | Out-Null }
} finally { [PtTok]::CloseHandle($proc) | Out-Null }
}
function Test-PtRunnerAdmin {
<#
.SYNOPSIS
Check whether the PT runner (PowerToys.exe) is currently running elevated.
.OUTPUTS
PSCustomObject with .Found (bool), .Pid (int), .Elevated (bool|$null)
#>
$pt = Get-Process PowerToys -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $pt) { return [pscustomobject]@{ Found=$false; Pid=$null; Elevated=$null } }
[pscustomobject]@{
Found = $true
Pid = $pt.Id
Elevated = (Test-ProcessElevated -ProcessId $pt.Id)
}
}

View File

@@ -0,0 +1,61 @@
# scripts/pt-clipboard-diff.ps1
# Multi-format clipboard inspection. Used to assert that AdvancedPaste plain-paste actually strips
# rich formats while preserving UnicodeText (and similar before/after assertions).
Add-Type -AssemblyName System.Windows.Forms
function Get-PtClipboardFormats {
<#
.SYNOPSIS
Return the list of format names currently on the clipboard (e.g. UnicodeText, HTML Format,
Rich Text Format, FileDrop, DeviceIndependentBitmap, etc.).
#>
$obj = [System.Windows.Forms.Clipboard]::GetDataObject()
if (-not $obj) { return @() }
return $obj.GetFormats()
}
function Get-PtClipboardText {
[System.Windows.Forms.Clipboard]::GetText()
}
function Compare-PtClipboardFormatDiff {
<#
.SYNOPSIS
Diff helper. Given a 'before' formats list (from Get-PtClipboardFormats), return:
- Added: formats present in current clipboard but not in before
- Removed: formats present in before but not in current
- Common: formats present in both
.EXAMPLE
$before = Get-PtClipboardFormats # e.g. UnicodeText + HTML Format + RTF
# ... user/script triggers AP plain-paste ...
$diff = Compare-PtClipboardFormatDiff -Before $before
# $diff.Removed should contain 'HTML Format' and 'Rich Text Format'
# $diff.Common should still contain 'UnicodeText'
#>
param([Parameter(Mandatory)][string[]]$Before)
$current = Get-PtClipboardFormats
[pscustomobject]@{
Before = $Before
Current = $current
Added = @($current | Where-Object { $_ -notin $Before })
Removed = @($Before | Where-Object { $_ -notin $current })
Common = @($current | Where-Object { $_ -in $Before })
}
}
function Set-PtClipboardRich {
<#
.SYNOPSIS
Put HTML + UnicodeText on the clipboard so plain-paste detection has something to strip.
Useful as test fixture before invoking AdvancedPaste.PasteAsPlainText.
#>
param(
[string]$Text = 'Hello world',
[string]$Html = '<html><body><b>Hello</b> <i>world</i></body></html>'
)
$obj = New-Object System.Windows.Forms.DataObject
$obj.SetText($Text, [System.Windows.Forms.TextDataFormat]::UnicodeText)
$obj.SetText($Html, [System.Windows.Forms.TextDataFormat]::Html)
[System.Windows.Forms.Clipboard]::SetDataObject($obj, $true)
}

View File

@@ -0,0 +1,99 @@
# scripts/pt-cmdpal-recycle.ps1
# Recover CmdPal AppX from "stuck" states (TextChanged-broken, sub-page hang, foreground-lock).
# The helper Microsoft.CmdPal.Ext.PowerToys is kept alive so the CmdPal.Show event listener wiring
# is not lost on recycle.
function Reset-CmdPalAppX {
<#
.SYNOPSIS
Kill the Microsoft.CmdPal.UI process and relaunch the AppX. Returns the new HWND or 0 on failure.
.NOTES
Symptoms requiring this:
- set-value MainSearchBox echoes the text but ZERO ListItems appear within 1.5s
- winapp ui invoke <button> hangs subsequent inspect calls
- Force-PtForeground returns false repeatedly
#>
$cp = Get-Process Microsoft.CmdPal.UI -ErrorAction SilentlyContinue
if ($cp) {
Stop-Process -Id $cp.Id -Force
$deadline = (Get-Date).AddSeconds(5)
while ((Get-Process -Id $cp.Id -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) {
Start-Sleep -Milliseconds 200
}
}
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
$deadline = (Get-Date).AddSeconds(10)
do {
Start-Sleep -Milliseconds 300
$r = winapp ui list-windows -a Microsoft.CmdPal.UI 2>$null | Out-String
if ($r -match 'HWND (\d+):') { return [IntPtr][int64]$matches[1] }
} while ((Get-Date) -lt $deadline)
return [IntPtr]::Zero
}
function Reset-CmdPalToHome {
<#
.SYNOPSIS
Navigate CmdPal back to the home page from any sub-page by invoking BackButton via UIA.
CmdPal's Esc handler is unreachable via SendInput from elevated sessions (UIPI), and Esc-via-
PostMessage is filtered by the WinUI 3 raw-input hook. BackButton invoke via UIA InvokePattern
works regardless.
#>
$homePlaceholder = 'Search for apps, files and commands'
for ($i = 0; $i -lt 6; $i++) {
$cur = winapp ui get-value 'MainSearchBox' -a Microsoft.CmdPal.UI 2>$null
if ($cur -and ($cur -match [regex]::Escape($homePlaceholder))) { break }
winapp ui invoke 'BackButton' -a Microsoft.CmdPal.UI 2>$null | Out-Null
Start-Sleep -Milliseconds 200
}
# Re-signal Show in case BackButton dismissed the window
if (Get-Command Invoke-PtSharedEvent -ErrorAction SilentlyContinue) {
try { Invoke-PtSharedEvent -Name 'CmdPal.Show' | Out-Null } catch {}
}
Start-Sleep -Milliseconds 500
}
function Test-CmdPalDegraded {
<#
.SYNOPSIS
Probe the AppX with a known-good query ('notepad') and verify >=1 ListItem appears within
1500ms. Returns $true if degraded (TextChanged-broken).
#>
Reset-CmdPalToHome
winapp ui set-value 'MainSearchBox' 'notepad' -a Microsoft.CmdPal.UI 2>$null | Out-Null
$deadline = (Get-Date).AddMilliseconds(1500)
do {
$insLines = (winapp ui inspect -a Microsoft.CmdPal.UI --depth 7 -i 2>$null) -split "`n"
$items = $insLines | Where-Object { $_ -match 'itm-' -and $_ -match 'ListItem' }
if ($items.Count -gt 0) {
winapp ui set-value 'MainSearchBox' '' -a Microsoft.CmdPal.UI 2>$null | Out-Null
return $false
}
Start-Sleep -Milliseconds 150
} while ((Get-Date) -lt $deadline)
return $true
}
function Invoke-CmdPalQuery {
<#
.SYNOPSIS
Type a query into MainSearchBox after returning to home. Auto-recovers if AppX is degraded.
Returns the result items as an array of strings (text lines starting with itm-).
.EXAMPLE
$items = Invoke-CmdPalQuery -Query 'notepad'
if ($items | Where-Object { $_ -match 'Notepad' }) { 'PASS' } else { 'FAIL' }
#>
param([Parameter(Mandatory)][string]$Query, [int]$WaitMs = 800)
Reset-CmdPalToHome
winapp ui set-value 'MainSearchBox' $Query -a Microsoft.CmdPal.UI 2>$null | Out-Null
Start-Sleep -Milliseconds $WaitMs
$out = winapp ui inspect -a Microsoft.CmdPal.UI --depth 7 -i 2>$null | Out-String
$items = ($out -split "`r?`n" | Where-Object { $_ -match 'itm-' -and $_ -match 'ListItem' })
if ($items.Count -eq 0) {
if (Test-CmdPalDegraded) {
Reset-CmdPalAppX | Out-Null
return (Invoke-CmdPalQuery -Query $Query -WaitMs $WaitMs)
}
}
return $items
}

View File

@@ -0,0 +1,136 @@
# scripts/pt-explorer-com.ps1
# Drive Explorer windows via Shell.Application COM to set up file selections, then trigger
# PT modules that read IShellItemArray from the foreground Explorer window (Peek, Image Resizer,
# PowerRename, File Locksmith, Workspaces).
#
# This bypasses needing a real mouse / interactive selection — Shell COM does the selection
# programmatically, then the PT hotkey (e.g. Ctrl+Space for Peek) fires the centralized hook
# which reads Explorer's selection at the moment of activation.
#
# Requires an interactive desktop session. If GetForegroundWindow() returns 0 or no Explorer
# windows are open, the functions return $null/$false instead of throwing — callers should
# treat that as a BLK-ENV signal (an environment block, not a product FAIL).
function Get-PtExplorerWindows {
<#
.SYNOPSIS
Return all open Explorer windows as Shell COM objects (with .LocationName, .Document.Folder, etc.).
Returns @() if no Explorer windows are open.
#>
try {
$shell = New-Object -ComObject Shell.Application
return @($shell.Windows() | Where-Object { $_.Name -eq 'File Explorer' -or $_.FullName -match 'explorer\.exe$' })
} catch { return @() }
}
function Open-PtExplorerAtPath {
<#
.SYNOPSIS
Open a fresh Explorer window at the given path. Returns the Shell COM window object.
Useful when no Explorer is open yet.
#>
[CmdletBinding()] param([Parameter(Mandatory)][string]$Path)
if (-not (Test-Path $Path)) { throw "Path not found: $Path" }
Start-Process explorer.exe -ArgumentList $Path
Start-Sleep -Milliseconds 1500
$wins = Get-PtExplorerWindows
# Note: the -replace must be wrapped in its own parens, otherwise the ',' in -replace '\\','/'
# is parsed as a second argument to [regex]::Escape() (overload error: "argument count: 2").
$needle = [regex]::Escape(((Resolve-Path $Path).Path -replace '\\','/'))
return ($wins | Where-Object { $_.LocationURL -match $needle } | Select-Object -First 1)
}
function Select-PtExplorerFiles {
<#
.SYNOPSIS
Select 1+ files in an open Explorer window via Shell COM. The window comes to foreground.
.DESCRIPTION
Uses Shell.Application's SelectItem(item, flags) API. Flags:
0x01 = SVSI_SELECT
0x04 = SVSI_DESELECTOTHERS (apply to the first item only when selecting multiple)
0x08 = SVSI_ENSUREVISIBLE
0x20 = SVSI_FOCUSED
Returns $true on success, $false if any file wasn't found in the folder.
.EXAMPLE
$win = Get-PtExplorerWindows | Select-Object -First 1
Select-PtExplorerFiles -ExplorerWindow $win -FileNames 'test-markdown.md','test-html.html','test-source.cs'
Send-PtChord -Mods 0x11 -Key 0x20 # Ctrl+Space → Peek opens on 3 selected files
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]$ExplorerWindow,
[Parameter(Mandatory)][string[]]$FileNames
)
if (-not $ExplorerWindow.Document) { return $false }
$folder = $ExplorerWindow.Document.Folder
$first = $true
foreach ($name in $FileNames) {
$item = $folder.ParseName($name)
if (-not $item) { Write-Warning "File not found in folder: $name"; return $false }
# First item: SELECT + DESELECTOTHERS + ENSUREVISIBLE + FOCUSED = 0x2D
# Subsequent items: SELECT + ENSUREVISIBLE = 0x09
$flags = if ($first) { 0x2D } else { 0x09 }
$ExplorerWindow.Document.SelectItem($item, $flags)
$first = $false
}
Start-Sleep -Milliseconds 300
return $true
}
function Invoke-PtPeekWithExplorerSelection {
<#
.SYNOPSIS
Set up an Explorer multi-file selection and trigger Peek via Ctrl+Space.
Returns the new Peek window HWND, or $null on failure.
.EXAMPLE
$h = Invoke-PtPeekWithExplorerSelection -FolderPath D:\fixtures -FileNames 'a.png','b.md','c.cs'
winapp ui invoke PinButton -w $h
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$FolderPath,
[Parameter(Mandatory)][string[]]$FileNames
)
$win = Get-PtExplorerWindows | Where-Object { $_.LocationURL -match [regex]::Escape(($FolderPath -replace '\\','/')) } | Select-Object -First 1
if (-not $win) { $win = Open-PtExplorerAtPath -Path $FolderPath }
if (-not $win) { return $null }
if (-not (Select-PtExplorerFiles -ExplorerWindow $win -FileNames $FileNames)) { return $null }
# Capture pre-state Peek HWND list to detect the new window
$beforeHwnds = @(Get-Process PowerToys.Peek.UI -EA SilentlyContinue | ForEach-Object MainWindowHandle)
# Fire Ctrl+Space (Peek default). Requires pt-sendinput-chord.ps1 to be dot-sourced first.
if (-not (Get-Command Send-PtChord -EA SilentlyContinue)) {
throw "Send-PtChord not loaded. Dot-source scripts/pt-sendinput-chord.ps1 first."
}
Send-PtChord -Mods 0x11 -Key 0x20 | Out-Null # Ctrl+Space
Start-Sleep -Milliseconds 1200
# Find the new Peek window HWND
$afterHwnds = @(Get-Process PowerToys.Peek.UI -EA SilentlyContinue | ForEach-Object MainWindowHandle)
$new = $afterHwnds | Where-Object { $_ -ne 0 -and $_ -notin $beforeHwnds } | Select-Object -First 1
if (-not $new) { $new = $afterHwnds | Where-Object { $_ -ne 0 } | Select-Object -First 1 }
return $new
}
function Test-PtInteractiveDesktop {
<#
.SYNOPSIS
Probe whether the current session is interactive (foreground + Shell COM both working).
Returns a PSCustomObject with .ForegroundOk and .ShellComOk.
.EXAMPLE
$env = Test-PtInteractiveDesktop
if (-not $env.ForegroundOk -or -not $env.ShellComOk) {
Write-Warning "Non-interactive session — Explorer-driven techniques will fail."
}
#>
Add-Type 'using System; using System.Runtime.InteropServices; public class FG3 { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }' -EA SilentlyContinue
$hasFg = $false
for ($i = 0; $i -lt 5; $i++) {
if ([FG3]::GetForegroundWindow() -ne [IntPtr]::Zero) { $hasFg = $true; break }
Start-Sleep -Milliseconds 200
}
$shellOk = $false
try { @((New-Object -ComObject Shell.Application).Windows()).Count | Out-Null; $shellOk = $true } catch {}
[pscustomobject]@{ ForegroundOk = $hasFg; ShellComOk = $shellOk }
}

View File

@@ -0,0 +1,96 @@
# pt-explorer-contextmenu.ps1 — drive any Explorer (Win11) context-menu PowerToys module
# end-to-end the way a real user does: open Explorer, select file(s), synthetic right-click
# to OPEN the menu, then UIA-invoke the module's menu item by NAME (robust — no coordinate
# click). Used by File Locksmith, Image Resizer, PowerRename, New+, etc.
#
# See explorer-context-menu-flow.md for the full write-up, stability notes, and per-module captions.
#
# Requires an UNLOCKED interactive desktop (synthetic right-click needs foreground). Check first:
# if ([PtFg]::GetForegroundWindow() -eq [IntPtr]::Zero) -> desktop locked -> BLK-ENV.
Add-Type -TypeDefinition @'
using System; using System.Runtime.InteropServices;
public static class PtCtx {
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int c);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")] public static extern void mouse_event(uint f, uint dx, uint dy, uint d, IntPtr e);
public const uint RIGHTDOWN=0x0008, RIGHTUP=0x0010, LEFTDOWN=0x0002, LEFTUP=0x0004;
public static void ForceForeground(IntPtr h) {
IntPtr fg = GetForegroundWindow(); uint fp;
uint ft = GetWindowThreadProcessId(fg, out fp); uint ct = GetCurrentThreadId();
ShowWindow(h, 9);
if (ft != 0 && ft != ct) AttachThreadInput(ct, ft, true);
BringWindowToTop(h); SetForegroundWindow(h);
if (ft != 0 && ft != ct) AttachThreadInput(ct, ft, false);
}
public static void RightClick(int x, int y) {
SetCursorPos(x, y); System.Threading.Thread.Sleep(250);
mouse_event(RIGHTDOWN,0,0,0,IntPtr.Zero); System.Threading.Thread.Sleep(70); mouse_event(RIGHTUP,0,0,0,IntPtr.Zero);
}
}
'@ -ErrorAction SilentlyContinue
function Test-PtDesktopInteractive {
# Polls up to $TimeoutSec for a foreground window. A momentary 0 is common for a few seconds
# right after Restart-PtRunner / Explorer restart — without the poll that blip is misclassified
# as a locked desktop (false BLK-ENV). A genuinely locked/non-interactive desktop stays 0 for
# the whole window and still returns $false.
param([int]$TimeoutSec = 5)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
do {
if ([PtCtx]::GetForegroundWindow() -ne [IntPtr]::Zero) { return $true }
Start-Sleep -Milliseconds 250
} while ((Get-Date) -lt $deadline)
return $false
}
# Opens the Win11 context menu for a file in an already-open Explorer window and returns the
# menu popup HWND. $ExplorerHwnd = the CabinetWClass window; $FileName = item to right-click.
function Open-PtExplorerContextMenu {
param([Parameter(Mandatory)][int]$ExplorerHwnd, [Parameter(Mandatory)][string]$FileName, [int]$MaxTries = 3)
if (-not (Test-PtDesktopInteractive)) { throw 'BLK-ENV: desktop is locked / no foreground (GetForegroundWindow()=0). Unlock and retry.' }
for ($try = 1; $try -le $MaxTries; $try++) {
[PtCtx]::ForceForeground([IntPtr]$ExplorerHwnd); Start-Sleep -Milliseconds 500
$item = (winapp ui search $FileName -w $ExplorerHwnd --json 2>$null | ConvertFrom-Json).matches |
Where-Object { $_.type -eq 'ListItem' } | Select-Object -First 1
if (-not $item) { throw "File item '$FileName' not found in Explorer window $ExplorerHwnd" }
# Right-click near the row's LEFT edge (on the filename), not the geometric center:
# in Details view the ListItem rect spans ~full row width (measured 71% of window), so
# x+width/2 lands far right over other columns / empty canvas → background menu or missed
# click. x + min(80, width/2) is on the filename in Details AND on the tile in Icons view.
[PtCtx]::RightClick([int]($item.x + [Math]::Min(80, $item.width/2)), [int]($item.y + $item.height/2))
Start-Sleep -Seconds 2
# The Win11 menu is its own top-level popup window:
$menu = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
Where-Object { $_.className -match 'PopupWindowSiteBridge' } | Sort-Object height -Descending | Select-Object -First 1
if ($menu) { return $menu.hwnd }
Start-Sleep -Milliseconds 500 # retry: foreground/menu wasn't ready (common on the first attempt right after Explorer opens)
}
throw "Context-menu popup window not found after $MaxTries right-click attempts"
}
# Invokes a context-menu item by its visible NAME (robust — UIA InvokePattern, no coord click).
# Returns $true if invoked. Match the module caption, e.g.:
# File Locksmith -> 'Unlock with File Locksmith' PowerRename -> 'Rename with PowerRename'
# Image Resizer -> 'Resize images' (verify by enumerating) New+ -> 'New+'
function Invoke-PtContextMenuItem {
param([Parameter(Mandatory)][int]$MenuHwnd, [Parameter(Mandatory)][string]$ItemName)
$m = (winapp ui search $ItemName -w $MenuHwnd --json 2>$null | ConvertFrom-Json).matches |
Where-Object { $_.type -eq 'MenuItem' } | Select-Object -First 1
if (-not $m) { return $false } # caller can treat $false as "entry absent" (e.g. module disabled)
winapp ui invoke $m.selector -w $MenuHwnd 2>$null | Out-Null
return $true
}
# Lists all context-menu item names (for discovering a module's caption or asserting absence).
function Get-PtContextMenuItems {
param([Parameter(Mandatory)][int]$MenuHwnd)
winapp ui inspect -w $MenuHwnd --depth 8 2>$null | Out-String |
Select-String 'MenuItem "([^"]+)"' -AllMatches | ForEach-Object { $_.Matches } | ForEach-Object { $_.Groups[1].Value }
}

View File

@@ -0,0 +1,100 @@
# scripts/pt-foreground-guard.ps1
# Verify and force a window to foreground BEFORE sending SendInput.
# Without this guard, SendInput keys silently leak to the caller's terminal when
# the target window has lost foreground (common with CmdPal AppX where Windows
# foreground-lock blocks SetForegroundWindow after the first attempt).
#
# Use winapp ui set-value for UIA-friendly inputs (no foreground required).
# Use this guard ONLY when you need real keystrokes (e.g. CmdPal alias detection).
if (-not ('PtFg' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class PtFg {
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
[DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int pid);
}
'@
}
function Test-PtForeground {
<#
.SYNOPSIS
Check whether the target AppX is currently foreground by parsing winapp ui list-windows output
for the literal substring 'foreground'.
#>
param([Parameter(Mandatory)][string]$AppId)
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
return ($r -match 'foreground')
}
function Get-PtHwnd {
<#
.SYNOPSIS
Return the first HWND for the given AppX/exe. Returns [IntPtr]::Zero if none.
#>
param([Parameter(Mandatory)][string]$AppId)
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
if ($r -match 'HWND (\d+):') { return [IntPtr][int64]$matches[1] }
return [IntPtr]::Zero
}
function Force-PtForeground {
<#
.SYNOPSIS
Force the target AppX window to foreground using the AttachThreadInput + AllowSetForegroundWindow
trick. Returns $true if window is foreground after this attempt; $false otherwise.
.NOTES
Windows foreground-lock will block subsequent SetForegroundWindow calls in the same session if
a real interactive event hasn't fired recently. If this returns $false repeatedly, the only
reliable recovery is to recycle the AppX (kill + relaunch via shell:AppsFolder URI).
#>
param([Parameter(Mandatory)][string]$AppId)
$h = Get-PtHwnd -AppId $AppId
if ($h -eq [IntPtr]::Zero) { return $false }
# Permission grant
$proc = Get-Process | Where-Object { $_.MainWindowHandle -eq $h } | Select-Object -First 1
if ($proc) { [PtFg]::AllowSetForegroundWindow($proc.Id) | Out-Null }
[PtFg]::ShowWindow($h, 9) | Out-Null # SW_RESTORE
Start-Sleep -Milliseconds 150
# AttachThreadInput trick
$fg = [PtFg]::GetForegroundWindow()
$fgPid = 0
$fgThread = [PtFg]::GetWindowThreadProcessId($fg, [ref]$fgPid)
$curThread = [PtFg]::GetCurrentThreadId()
if ($fgThread -ne 0 -and $fgThread -ne $curThread) {
[PtFg]::AttachThreadInput($curThread, $fgThread, $true) | Out-Null
}
[PtFg]::BringWindowToTop($h) | Out-Null
[PtFg]::SetForegroundWindow($h) | Out-Null
[PtFg]::ShowWindow($h, 5) | Out-Null # SW_SHOW
if ($fgThread -ne 0 -and $fgThread -ne $curThread) {
[PtFg]::AttachThreadInput($curThread, $fgThread, $false) | Out-Null
}
Start-Sleep -Milliseconds 400
return (Test-PtForeground -AppId $AppId)
}
function Assert-PtForegroundOrAbort {
<#
.SYNOPSIS
Guard helper. Throws if the target AppX is NOT foreground. Use this immediately before any
SendInput call to ensure keys don't leak to the wrong window.
#>
param([Parameter(Mandatory)][string]$AppId)
if (-not (Test-PtForeground -AppId $AppId)) {
if (-not (Force-PtForeground -AppId $AppId)) {
throw "ABORT: $AppId is not foreground and cannot be forced foreground. SendInput would leak to wrong window."
}
}
}

View File

@@ -0,0 +1,76 @@
# pt-nonelevated.ps1 — launch a process at MEDIUM integrity (non-elevated) from an
# already-elevated agent shell. Needed for tests that assert elevation-dependent
# visibility (e.g. File Locksmith L649/L650: a non-elevated module must NOT see
# higher-integrity processes; an elevated one must).
#
# The drive-stack in SKILL.md only covers gaining MORE privilege. De-elevation is the
# opposite problem: from a High-IL shell you cannot simply CreateProcess a Medium-IL
# child. The robust, dependency-free way is a one-shot Scheduled Task registered with
# RunLevel=Limited + LogonType=Interactive, which lands on the logged-on user's desktop
# at their filtered (medium) token. (The classic explorer-shell-injection trick is more
# fragile across sessions.)
#
# Functions:
# Start-PtNonElevated -Exe <path> [-Arguments <str>] -> launches a GUI/console exe non-elevated, returns the spawned PID(s)
# Invoke-PtNonElevatedCapture -Exe <path> -Arguments <str> -OutFile <path> -> runs a console exe non-elevated, redirects stdout/err to a file, waits, returns the file path
#
# Verify elevation of the result with Test-ProcessElevated (scripts/pt-admin-probe.ps1).
function Start-PtNonElevated {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Exe,
[string]$Arguments = '',
[int]$WaitSeconds = 5,
[string]$MatchProcessName # optional: base name to enumerate spawned PIDs (e.g. 'PowerToys.FileLocksmithUI')
)
if (-not (Test-Path $Exe)) { throw "Exe not found: $Exe" }
$taskName = "PtNonElev_$([guid]::NewGuid().ToString('N').Substring(0,8))"
$before = @()
if ($MatchProcessName) { $before = @(Get-Process -Name $MatchProcessName -EA SilentlyContinue | Select-Object -Expand Id) }
try {
$action = New-ScheduledTaskAction -Execute $Exe -Argument $Arguments
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -RunLevel Limited -LogonType Interactive
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Force | Out-Null
Start-ScheduledTask -TaskName $taskName
Start-Sleep -Seconds $WaitSeconds
if ($MatchProcessName) {
$after = @(Get-Process -Name $MatchProcessName -EA SilentlyContinue | Select-Object -Expand Id)
return ($after | Where-Object { $_ -notin $before })
}
return $null
}
finally {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -EA SilentlyContinue
}
}
function Invoke-PtNonElevatedCapture {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Exe,
[string]$Arguments = '',
[Parameter(Mandatory)][string]$OutFile,
[int]$TimeoutSeconds = 30
)
if (-not (Test-Path $Exe)) { throw "Exe not found: $Exe" }
Remove-Item $OutFile -EA SilentlyContinue
$wrap = [IO.Path]::ChangeExtension($OutFile, '.cmd')
"`"$Exe`" $Arguments > `"$OutFile`" 2>&1" | Set-Content -Encoding ascii $wrap
$taskName = "PtNonElev_$([guid]::NewGuid().ToString('N').Substring(0,8))"
try {
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/c `"$wrap`""
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -RunLevel Limited -LogonType Interactive
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Force | Out-Null
Start-ScheduledTask -TaskName $taskName
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
do { Start-Sleep 1; $info = Get-ScheduledTaskInfo -TaskName $taskName }
while ($info.LastTaskResult -eq 267009 -and (Get-Date) -lt $deadline) # 267009 = task still running
Start-Sleep 1
return $OutFile
}
finally {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -EA SilentlyContinue
Remove-Item $wrap -EA SilentlyContinue
}
}

View File

@@ -0,0 +1,94 @@
# scripts/pt-sendinput-chord.ps1
# Inject a global hotkey chord (e.g. Win+Shift+/) into the system input stream.
# Critical: INPUT struct MUST be cb=40 on x64 (with padding for the MOUSEINPUT union member).
# The common bug "Win+ hotkeys can't be injected" is a marshaling error producing 32-byte struct
# and SendInput returns 0 with GetLastError()==87 (ERROR_INVALID_PARAMETER).
#
# This SHOULD be a last resort. Prefer Named Events (Invoke-PtSharedEvent) when the module exposes one.
# Use this only for: (a) explicit hotkey-trigger verification tests, (b) modules without Named Events,
# (c) UI keystrokes inside an already-foreground window (use Send-KeyToHwnd via PostMessage instead
# for elevated -> non-elevated AppX, see references/winapp-ui-testing.md).
if (-not ('PtChord' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
public static class PtChord {
[StructLayout(LayoutKind.Sequential)]
struct INPUT { public uint type; public KEYBDINPUT ki; public int pad1; public int pad2; } // pad to 40 bytes
[StructLayout(LayoutKind.Sequential)]
struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
[DllImport("user32.dll", SetLastError=true)]
static extern uint SendInput(uint n, INPUT[] p, int cb);
const uint KEYUP = 0x0002;
static INPUT K(ushort vk, bool up) { INPUT i=new INPUT(); i.type=1; i.ki.wVk=vk; i.ki.dwFlags=up?KEYUP:0; return i; }
public static uint Chord(ushort[] mods, ushort key) {
var l=new List<INPUT>();
foreach(var m in mods) l.Add(K(m,false));
l.Add(K(key,false)); l.Add(K(key,true));
for(int i=mods.Length-1;i>=0;i--) l.Add(K(mods[i],true));
var a=l.ToArray();
return SendInput((uint)a.Length, a, Marshal.SizeOf(typeof(INPUT)));
}
public static uint Tap(ushort key) { return Chord(new ushort[0], key); }
}
'@
}
# Common VK codes for chord mods:
# LWIN=0x5B RWIN=0x5C CTRL=0x11 SHIFT=0x10 ALT=0x12
# Main key VKs:
# 0x08 Backspace 0x09 Tab 0x0D Enter 0x1B Escape 0x20 Space
# 0x25 Left 0x26 Up 0x27 Right 0x28 Down
# 0x30..0x39 0..9 0x41..0x5A A..Z
function Send-PtChord {
<#
.SYNOPSIS
Inject a hotkey chord. Returns number of inputs Windows accepted (0 = failed; check GetLastError).
.EXAMPLE
Send-PtChord -Mods 0x5B,0x10 -Key 0x43 # Win+Shift+C (Color Picker)
Send-PtChord -Mods 0x5B,0x11 -Key 0x52 # Win+Ctrl+R (PowerOcr)
Send-PtChord -Mods 0x5B,0xA4 -Key 0x20 # Win+Alt+Space (CmdPal default)
Send-PtChord -Key 0x0D # plain Enter (no mods)
#>
[CmdletBinding()]
param(
[uint16[]]$Mods = @(),
[Parameter(Mandatory)][uint16]$Key
)
$sent = [PtChord]::Chord($Mods, $Key)
if ($sent -eq 0) {
$err = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
throw "SendInput failed (returned 0, GetLastError=$err). Likely caller is at lower integrity than PT runner, or chord is OS-reserved (Win+L, Win+Tab)."
}
return $sent
}
function Wait-PtHotkeyAccepted {
<#
.SYNOPSIS
After Send-PtChord, verify the PT runner saw it by tailing its log for the centralized-hook line.
Returns the matching log line (if any) within $TimeoutSec.
.EXAMPLE
Send-PtChord -Mods 0x5B,0x10 -Key 0x43
$line = Wait-PtHotkeyAccepted -ModuleHint 'Color' -TimeoutSec 3
if (-not $line) { throw "Runner did not log hotkey invocation" }
#>
[CmdletBinding()]
param([string]$ModuleHint = '', [int]$TimeoutSec = 3)
$log = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\PowerToys\RunnerLogs" -Filter 'runner-log_*.log' -EA SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $log) { return $null }
$start = (Get-Date).AddSeconds(-2)
$deadline = (Get-Date).AddSeconds($TimeoutSec)
do {
$line = Get-Content $log.FullName -Tail 50 -EA SilentlyContinue |
Where-Object { $_ -match 'hotkey is invoked from Centralized keyboard hook' -and ($ModuleHint -eq '' -or $_ -match $ModuleHint) } |
Select-Object -Last 1
if ($line) { return $line }
Start-Sleep -Milliseconds 200
} while ((Get-Date) -lt $deadline)
return $null
}

View File

@@ -0,0 +1,47 @@
# pt-session-diagnose.ps1
# Diagnose whether the current shell can drive interactive PowerToys tests.
# Tells you in one go: am I on the active console session, can I see foreground windows,
# and can I use Shell COM. If not, prints the exact psexec mitigation command.
Add-Type 'using System; using System.Runtime.InteropServices;
public class WTS { [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); }
public class FG { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }'
Write-Host "--- Logged-on users + sessions ---" -ForegroundColor Cyan
quser 2>&1
Write-Host "`n--- This shell's session ---" -ForegroundColor Cyan
$me = [Diagnostics.Process]::GetCurrentProcess()
" PID: $($me.Id)"
" Session: $($me.SessionId)"
Write-Host "`n--- Console Explorer session(s) ---" -ForegroundColor Cyan
$exps = Get-Process explorer -ErrorAction SilentlyContinue
if ($exps) {
$exps | Select-Object Id, SessionId, @{N='StartTime';E={$_.StartTime}} | Format-Table -AutoSize
} else {
Write-Host " (no explorer.exe running)" -ForegroundColor Yellow
}
Write-Host "`n--- Windows active console + foreground + Shell COM ---" -ForegroundColor Cyan
$activeConsole = [WTS]::WTSGetActiveConsoleSessionId()
$fg = [FG]::GetForegroundWindow()
$shellOk = $false
try { @((New-Object -ComObject Shell.Application).Windows()).Count | Out-Null; $shellOk = $true } catch {}
" WTSGetActiveConsoleSessionId() = $activeConsole"
" GetForegroundWindow() = $fg"
" Shell.Application available = $shellOk"
Write-Host "`n--- Verdict ---" -ForegroundColor Cyan
$consoleSession = ($exps | Select-Object -First 1).SessionId
if ($me.SessionId -eq $consoleSession -and $fg -ne 0 -and $shellOk) {
Write-Host " PASS - this shell can drive interactive PowerToys tests." -ForegroundColor Green
} elseif ($me.SessionId -eq $consoleSession -and $fg -eq 0) {
Write-Host " WARN - same session as explorer but no foreground (workstation locked?). Unlock and re-run." -ForegroundColor Yellow
} elseif (-not $shellOk) {
Write-Host " FAIL - Shell COM unavailable (likely Session 0 / service context). Very few tests possible." -ForegroundColor Red
} else {
Write-Host " FAIL - shell in Session $($me.SessionId), console explorer in Session $consoleSession. Input injection denied." -ForegroundColor Red
Write-Host " Mitigation: relaunch in the console session with:" -ForegroundColor Yellow
Write-Host " psexec -accepteula -h -i $consoleSession -s pwsh.exe" -ForegroundColor Yellow
}

View File

@@ -0,0 +1,133 @@
# scripts/pt-shared-events.ps1
# Signal PowerToys modules via Win32 named events.
# Catalog source: PowerToys repo src/common/interop/shared_constants.h
# (Friendly-name mapping was originally surfaced by community frameworks; the values themselves
# are stable PT public IPC names. This file is self-contained — no external repo required.)
# Reason: instead of pressing a hotkey (which is racey, foreground-sensitive, and UIPI-fragile),
# directly SetEvent on the kernel event the module is waiting on. Same code path as the hotkey.
if (-not ('PtEv' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class PtEv {
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
private static extern IntPtr OpenEventW(uint dwAccess, bool bInherit, string lpName);
[DllImport("kernel32.dll", SetLastError=true)]
private static extern bool SetEvent(IntPtr h);
[DllImport("kernel32.dll", SetLastError=true)]
private static extern bool CloseHandle(IntPtr h);
private const uint EVENT_MODIFY_STATE = 0x0002;
private const uint SYNCHRONIZE = 0x00100000;
public static bool Signal(string fullName) {
IntPtr h = OpenEventW(EVENT_MODIFY_STATE | SYNCHRONIZE, false, fullName);
if (h == IntPtr.Zero) {
int err = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(err,
"OpenEvent failed for '" + fullName + "' (err=" + err + "). Owning module process may not be running.");
}
try { return SetEvent(h); } finally { CloseHandle(h); }
}
public static bool Exists(string fullName) {
IntPtr h = OpenEventW(SYNCHRONIZE, false, fullName);
if (h == IntPtr.Zero) return false;
CloseHandle(h); return true;
}
}
'@
}
# Friendly-name -> full event name map (per Local\ namespace).
# Source: <PT-repo>\src\common\interop\shared_constants.h
$script:PtSharedEvents = @{
# ── Hotkey-activated module triggers ──
'AOT.Pin' = 'Local\AlwaysOnTopPinEvent-892e0aa2-cfa8-4cc4-b196-ddeb32314ce8'
'AOT.IncreaseOpacity' = 'Local\AlwaysOnTopIncreaseOpacityEvent-a1b2c3d4-e5f6-7890-abcd-ef1234567890'
'AOT.DecreaseOpacity' = 'Local\AlwaysOnTopDecreaseOpacityEvent-b2c3d4e5-f6a7-8901-bcde-f12345678901'
'AdvancedPaste.ShowUI' = 'Local\PowerToys_AdvancedPaste_ShowUI'
'CmdPal.Show' = 'Local\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a'
'ColorPicker.Show' = 'Local\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525'
'CropAndLock.Reparent' = 'Local\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36'
'CropAndLock.Thumbnail' = 'Local\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434'
'CursorWrap.Trigger' = 'Local\CursorWrapTriggerEvent-1f8452b5-4e6e-45b3-8b09-13f14a5900c9'
'EnvVars.Show' = 'Local\PowerToysEnvironmentVariables-ShowEnvironmentVariablesEvent-1021f616-e951-4d64-b231-a8f972159978'
'EnvVars.ShowAdmin' = 'Local\PowerToysEnvironmentVariables-EnvironmentVariablesAdminEvent-8c95d2ad-047c-49a2-9e8b-b4656326cfb2'
'FancyZones.ToggleEditor' = 'Local\FancyZones-ToggleEditorEvent-1e174338-06a3-472b-874d-073b21c62f14'
'FindMyMouse.Trigger' = 'Local\FindMyMouseTriggerEvent-5a9dc5f4-1c74-4f2f-a66f-1b9b6a2f9b23'
'Hosts.Show' = 'Local\Hosts-ShowHostsEvent-5a0c0aae-5ff5-40f5-95c2-20e37ed671f0'
'Hosts.ShowAdmin' = 'Local\Hosts-ShowHostsAdminEvent-60ff44e2-efd3-43bf-928a-f4d269f98bec'
'LightSwitch.Toggle' = 'Local\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a'
'LightSwitch.Light' = 'Local\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca'
'LightSwitch.Dark' = 'Local\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368'
'MeasureTool.Trigger' = 'Local\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199'
'MouseCrosshairs.Trigger' = 'Local\MouseCrosshairsTriggerEvent-0d4c7f92-0a5c-4f5c-b64b-8a2a2f7e0b21'
'MouseHighlighter.Trigger' = 'Local\MouseHighlighterTriggerEvent-1e3c9c3d-3fdf-4f9a-9a52-31c9b3c3a8f4'
'MouseJump.Show' = 'Local\MouseJumpEvent-aa0be051-3396-4976-b7ba-1a9cc7d236a5'
'NewKeyboardManager.Open' = 'Local\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f'
'Peek.Show' = 'Local\ShowPeekEvent'
'PowerDisplay.Toggle' = 'Local\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c'
'PowerLauncher.Invoke' = 'Local\PowerToysRunInvokeEvent-30f26ad7-d36d-4c0e-ab02-68bb5ff3c4ab'
'PowerOcr.Show' = 'Local\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a'
'RegistryPreview.Trigger' = 'Local\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687'
'ShortcutGuide.Trigger' = 'Local\ShortcutGuide-TriggerEvent-d4275ad3-2531-4d19-9252-c0becbd9b496'
'TextExtractor.Show' = 'Local\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a'
'Workspaces.Hotkey' = 'Local\PowerToys-Workspaces-HotkeyEvent-2625C3C8-BAC9-4DB3-BCD6-3B4391A26FD0'
'Workspaces.LaunchEditor' = 'Local\Workspaces-LaunchEditorEvent-a55ff427-cf62-4994-a2cd-9f72139296bf'
'ZoomIt.Zoom' = 'Local\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393'
'ZoomIt.Draw' = 'Local\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975'
'ZoomIt.Break' = 'Local\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b'
'ZoomIt.LiveZoom' = 'Local\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d'
'ZoomIt.Snip' = 'Local\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30'
'ZoomIt.SnipOcr' = 'Local\PowerToysZoomIt-SnipOcrEvent-a7c3b1d2-9e4f-4a6b-8d5c-1f2e3a4b5c6d'
'ZoomIt.Record' = 'Local\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512'
# ── Termination triggers (clean shutdown without process kill) ──
'AOT.Terminate' = 'Local\AlwaysOnTopTerminateEvent-cfdf1eae-791f-4953-8021-2f18f3837eae'
'Awake.Exit' = 'Local\PowerToysAwakeExitEvent-c0d5e305-35fc-4fb5-83ec-f6070cfaf7fe'
'CmdPal.Exit' = 'Local\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd'
'ColorPicker.Terminate' = 'Local\TerminateColorPickerEvent-3d676258-c4d5-424e-a87a-4be22020e813'
'CropAndLock.Exit' = 'Local\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a'
'FZE.Exit' = 'Local\PowerToys-FZE-ExitEvent-ca8c73de-a52c-4274-b691-46e9592d3b43'
'Hosts.Terminate' = 'Local\Hosts-TerminateHostsEvent-d5410d5e-45a6-4d11-bbf0-a4ec2d064888'
'KBM.Terminate' = 'Local\TerminateKBMSharedEvent-a787c967-55b6-47de-94d9-56f39fed839e'
'MouseJump.Terminate' = 'Local\TerminateMouseJumpEvent-252fa337-317f-4c37-a61f-99464c3f9728'
'Peek.Terminate' = 'Local\TerminatePeekEvent-267149fe-7ed2-427d-a3ad-9e18203c037c'
'PowerAccent.Exit' = 'Local\PowerToysPowerAccentExitEvent-53e93389-d19a-4fbb-9b36-1981c8965e17'
'PowerOcr.Terminate' = 'Local\TerminatePowerOCREvent-08e5de9d-15df-4ea8-8840-487c13435a67'
'PowerDisplay.Terminate' = 'Local\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a'
'Run.Exit' = 'Local\PowerToysRunExitEvent-3e38e49d-a762-4ef1-88f2-fd4bc7481516'
'ShortcutGuide.Exit' = 'Local\ShortcutGuide-ExitEvent-35697cdd-a3d2-47d6-a246-34efcc73eac0'
'Settings.Terminate' = 'Local\PowerToysRunnerTerminateSettingsEvent-c34cb661-2e69-4613-a1f8-4e39c25d7ef6'
'ZoomIt.Exit' = 'Local\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220'
'GrabAndMove.Exit' = 'Local\PowerToysGrabAndMove-ExitEvent-b8c4d2e3-5f6a-7b8c-9d0e-1f2a3b4c5d6e'
}
function Invoke-PtSharedEvent {
<#
.SYNOPSIS
Signal a PowerToys named kernel event by friendly name (e.g. 'CmdPal.Show')
or by full event path (e.g. 'Local\PowerToys_AdvancedPaste_ShowUI').
Returns $true on success; throws if event doesn't exist or owner not running.
.EXAMPLE
Invoke-PtSharedEvent -Name 'CmdPal.Show'
Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke'
Invoke-PtSharedEvent -Name 'AOT.Pin'
#>
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Name)
$eventName = if ($script:PtSharedEvents.ContainsKey($Name)) { $script:PtSharedEvents[$Name] } else { $Name }
return [PtEv]::Signal($eventName)
}
function Test-PtSharedEvent {
[CmdletBinding()] param([Parameter(Mandatory)][string]$Name)
$eventName = if ($script:PtSharedEvents.ContainsKey($Name)) { $script:PtSharedEvents[$Name] } else { $Name }
return [PtEv]::Exists($eventName)
}
function Get-PtSharedEventCatalog {
$script:PtSharedEvents.GetEnumerator() | Sort-Object Name |
ForEach-Object { [pscustomobject]@{ Name = $_.Key; Event = $_.Value } }
}

View File

@@ -0,0 +1,66 @@
# scripts/pt-shell-verbs.ps1
# Enumerate Windows classic shell verbs (HKCR-registered) via Shell.Application COM.
#
# SCOPE WARNING: this does NOT find PowerToys context-menu items on Win11. PT registers
# PowerRename, Image Resizer, File Locksmith, New+ etc. via IExplorerCommand (Tier-1 modern
# menu), which is invisible to Shell.Application.Verbs(). For PT-context-menu drives, use
# `pt-explorer-contextmenu.ps1` (synthetic right-click + UIA invoke). See
# `explorer-context-menu-flow.md` for the canonical pattern.
#
# Useful for: enumerating non-PT classic verbs (Open, Edit, Send-to, third-party shell extensions),
# and as a negative check that PT verbs are NOT classic-shadowed.
function Get-PtShellVerbs {
<#
.SYNOPSIS
Enumerate classic HKCR shell verbs on a file or folder. Returns Name + the underlying Verb COM object.
.EXAMPLE
Get-PtShellVerbs -Path 'D:\fixtures\image.png' | Format-Table Name
#>
[CmdletBinding()] param([Parameter(Mandatory)][string]$Path)
if (-not (Test-Path $Path)) { throw "Path not found: $Path" }
$abs = (Resolve-Path $Path).Path
$folder = Split-Path -Parent $abs
$leaf = Split-Path -Leaf $abs
$shell = New-Object -ComObject Shell.Application
$ns = $shell.NameSpace($folder)
if (-not $ns) { throw "Cannot open folder namespace: $folder" }
$item = $ns.ParseName($leaf)
if (-not $item) { throw "File not in folder: $leaf" }
return @($item.Verbs()) | ForEach-Object {
[pscustomobject]@{ Name = $_.Name; Verb = $_ }
}
}
function Invoke-PtShellVerb {
<#
.SYNOPSIS
Invoke a classic shell verb on a file by name-regex match. Returns $true on success.
Does NOT work for PT Win11 modern-menu items — see SCOPE WARNING at top.
.EXAMPLE
Invoke-PtShellVerb -Path 'D:\fixtures\img.png' -NamePattern '^Edit$'
#>
[CmdletBinding()] param(
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$NamePattern
)
$verb = Get-PtShellVerbs -Path $Path | Where-Object { $_.Name -match $NamePattern } | Select-Object -First 1
if (-not $verb) {
Write-Warning "No classic shell verb matching '$NamePattern' on '$Path'. (Win11 PT modern-menu items are NOT visible here — use pt-explorer-contextmenu.ps1 instead.)"
return $false
}
$verb.Verb.DoIt()
return $true
}
function Reset-PtShellComCache {
<#
.SYNOPSIS
Release current Shell.Application COM instance + force a fresh one on next call.
Use when you've installed/registered a shell handler mid-test and the cached verb list
still reflects the old state.
#>
[System.Runtime.InteropServices.Marshal]::CleanupUnusedObjectsInCurrentContext()
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}

View File

@@ -0,0 +1,111 @@
# scripts/pt-state.ps1
# Common state-verification helpers: settings.json diff, runner log grep, GPO log check,
# process spawn detection, AppX probe.
function Get-PtSettings {
<#
.SYNOPSIS
Read the master PT settings.json (enabled.<Module> flags + run_elevated + theme + language).
#>
$f = "$env:LOCALAPPDATA\Microsoft\PowerToys\settings.json"
if (-not (Test-Path $f)) { return $null }
Get-Content $f -Raw | ConvertFrom-Json
}
function Get-PtModuleSettings {
<#
.SYNOPSIS
Read a single module's settings.json (e.g. AdvancedPaste, FancyZones, etc.).
These ARE auto-reloaded by the per-module file watcher (~3s debounce).
#>
param([Parameter(Mandatory)][string]$ModuleDir)
$f = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
if (-not (Test-Path $f)) { return $null }
Get-Content $f -Raw | ConvertFrom-Json
}
function Get-CmdPalSettings {
<#
.SYNOPSIS
Read CmdPal AppX settings.json (sandboxed path). Contains 19 ProviderSettings, DockSettings,
GalleryFeedUrl, EscapeKeyBehaviorSetting, AutoGoHomeInterval, Hotkey, Aliases, etc.
#>
$f = "$env:LOCALAPPDATA\Packages\Microsoft.CommandPalette_8wekyb3d8bbwe\LocalState\settings.json"
if (-not (Test-Path $f)) { return $null }
Get-Content $f -Raw | ConvertFrom-Json
}
function Get-PtRunnerLogTail {
<#
.SYNOPSIS
Tail the latest runner-log_<date>.log file for matching lines.
.EXAMPLE
Get-PtRunnerLogTail -Pattern 'hotkey is invoked' -TailLines 100
Get-PtRunnerLogTail -Pattern 'GPO sets' -TailLines 50
#>
param([string]$Pattern = '.*', [int]$TailLines = 50)
$log = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\PowerToys\RunnerLogs" -Filter 'runner-log_*.log' -EA SilentlyContinue |
Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $log) { return @() }
Get-Content $log.FullName -Tail $TailLines -EA SilentlyContinue | Where-Object { $_ -match $Pattern }
}
function Test-PtModuleEnabled {
<#
.SYNOPSIS
Check whether a specific module is enabled in master settings.json.
Note: PT Run uses the key "PowerToys Run" (with space).
#>
param([Parameter(Mandatory)][string]$ModuleKey)
$s = Get-PtSettings
if (-not $s) { return $false }
return [bool]$s.enabled.$ModuleKey
}
function Test-PtModuleProcess {
<#
.SYNOPSIS
Return the process(es) for a module exe name (e.g. 'PowerToys.AdvancedPaste').
Returns empty array if not running.
#>
param([Parameter(Mandatory)][string]$ExeName)
@(Get-Process $ExeName -EA SilentlyContinue)
}
function Restart-PtRunner {
<#
.SYNOPSIS
Kill the runner and relaunch to force fresh load of master settings.json.
The runner does NOT auto-pickup edits to the top-level enabled.<Module> flags.
#>
$pt = Get-Process PowerToys -EA SilentlyContinue | Select-Object -First 1
if ($pt) { Stop-Process -Id $pt.Id -Force; Start-Sleep -Milliseconds 800 }
Start-Process "$env:LOCALAPPDATA\PowerToys\PowerToys.exe"
Start-Sleep -Seconds 3
}
function Backup-PtModuleSettings {
<#
.SYNOPSIS
Snapshot a module's settings.json to TEMP for restore-on-exit. Returns the backup path.
.EXAMPLE
$bk = Backup-PtModuleSettings -ModuleDir AdvancedPaste
try { ... mutate ... } finally { Restore-PtModuleSettings -ModuleDir AdvancedPaste -BackupPath $bk }
#>
param([Parameter(Mandatory)][string]$ModuleDir)
$src = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
if (-not (Test-Path $src)) { return $null }
$bk = Join-Path $env:TEMP ("ptbk-$ModuleDir-$(Get-Random -Maximum 9999).json")
Copy-Item -Path $src -Destination $bk -Force
return $bk
}
function Restore-PtModuleSettings {
param(
[Parameter(Mandatory)][string]$ModuleDir,
[Parameter(Mandatory)][string]$BackupPath
)
$dst = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
Copy-Item -Path $BackupPath -Destination $dst -Force
Remove-Item $BackupPath -Force -EA SilentlyContinue
}

View File

@@ -31,7 +31,9 @@ Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.U
## Migration Strategy
### Recommended Order
### Phase-by-Phase Scope
Work on bounded problems, not the entire codebase at once. Each phase should compile before moving to the next.
1. **Project file** — Update TFM, NuGet packages, set `<UseWinUI>true</UseWinUI>`
2. **Data models and business logic** — No UI dependencies, migrate first
@@ -45,79 +47,213 @@ Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.U
10. **Installer & build pipeline** — Update WiX, signing, build events
11. **Tests** — Adapt for WinUI 3 runtime, async patterns
### Key Principles
### Migration Contract: Prohibited Patterns
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different application lifecycle boilerplate. Merge your resources and initialization code into the generated WinUI 3 App class.
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. This causes phantom build artifacts.
- **Use `Lazy<T>` for resource-dependent statics** — `ResourceLoader` is not available at class-load time in all contexts.
These rules capture human judgment and must be applied consistently across every file. Do NOT deviate.
**Architecture prohibitions:**
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different lifecycle boilerplate. Merge resources and init code into the generated WinUI 3 App class.
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. Causes phantom build artifacts.
- **Do NOT instantiate services directly** — Use DI and CommunityToolkit.Mvvm patterns.
- **Do NOT create a `Window` subclass for every dialog or sub-page** — use `ContentDialog` for in-app dialogs and `Frame`/`Page` navigation for sub-views. Separate `Window` classes are reserved for distinct top-level surfaces (e.g., FancyZones editor, OOBE).
- **Do NOT omit `WindowsPackageType=None` and `WindowsAppSDKSelfContained=true`** — Both are mandatory in the csproj for every WinUI 3 module in PowerToys. Without them the app crashes at startup with `COMException: ClassFactory cannot supply requested class` because the WinUI 3 runtime DLLs are not found.
**XAML prohibitions:**
- **Do NOT use `{DynamicResource}`** — Replace with `{ThemeResource}` (theme-reactive) or `{StaticResource}`.
- **Do NOT use `{Binding}` in `Setter.Value`** — Not supported in WinUI 3. Use `{StaticResource}`.
- **Do NOT use `{x:Static}`** — Replace with `{x:Bind}`, `x:Uid`, or code-behind.
- **Do NOT use `{x:Type}`** — Not supported. Use `x:DataType` for DataTemplate, or code-behind.
- **Do NOT use `clr-namespace:`** — Replace with `using:` in all xmlns declarations.
- **Do NOT use `Style.Triggers` / `DataTrigger` / `EventTrigger`** — Replace with `VisualStateManager`.
- **Do NOT use `MultiBinding`** — Replace with `x:Bind` function binding or computed ViewModel property.
- **Do NOT use `Visibility="Hidden"`** — WinUI only has `Visible` and `Collapsed`. Use `Opacity="0"` if layout must be preserved.
- **Do NOT use `IsDefault` / `IsCancel`** — Use `AccentButtonStyle` for primary button; handle Enter/Escape in code-behind.
- **Do NOT omit `BasedOn` when overriding default styles** — Without it, your style replaces the entire default. Always use `BasedOn="{StaticResource DefaultButtonStyle}"` etc.
- **Do NOT omit `XamlControlsResources` as first merged dictionary** — It provides default Fluent styles. Without it, controls have no visual appearance.
**Code-behind prohibitions:**
- **Do NOT use `Application.Current.Dispatcher`** — Store `DispatcherQueue` in a static field explicitly.
- **Do NOT use `Window.Current`** — Not supported. Use a custom `App.Window` static property.
- **Do NOT put `DataContext`, `Resources`, or `VisualStateManager` on `Window`** — WinUI 3 `Window` is NOT a `DependencyObject`. Use a root `Page`/`UserControl`/`Grid`.
- **Do NOT use tunneling/preview events** (`PreviewMouseDown`, `PreviewKeyDown`) — WinUI has no tunneling. Use bubbling equivalents with `Handled` property or `AddHandler(handledEventsToo: true)`.
**Resource prohibitions:**
- **Do NOT use `Properties.Resources.MyString`** — Replace with `ResourceLoaderInstance.ResourceLoader.GetString("MyString")`.
- **Do NOT initialize `ResourceLoader`-dependent values as static fields** — Wrap in `Lazy<T>` or null-coalescing property.
- **Do NOT use `pack://` URIs** — Replace with `ms-appx:///` scheme.
## Quick Reference Tables
### Namespace Mapping
| WPF | WinUI 3 |
|-----|---------|
| `System.Windows` | `Microsoft.UI.Xaml` |
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) |
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
| `System.Windows.Interop` | `WinRT.Interop` |
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `System.Windows` | `Microsoft.UI.Xaml` | Root namespace |
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` | Core controls |
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` | Low-level primitives |
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` | Brushes, transforms |
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` | Storyboard, animations |
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) | Split by purpose |
| `System.Windows.Media.Media3D` | **No equivalent** | Use Win2D or Composition APIs |
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` | Rectangle, Ellipse, Path |
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` | Pointer, keyboard, focus |
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` | Binding, IValueConverter |
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` | Limited — RichTextBlock + Paragraph |
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` | XAML parsing, markup extensions |
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` | Accessibility / UI Automation |
| `System.Windows.Navigation` | **No direct equivalent** | Use `Frame.Navigate()` |
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` | Dispatcher → DispatcherQueue |
| `System.Windows.Interop` | `WinRT.Interop` / `Microsoft.UI.Xaml.Hosting` | HWND interop |
### Control Replacements (No 1:1 Mapping)
These WPF controls have no direct counterpart and require a different control or third-party package:
| WPF Control | WinUI 3 Replacement | Notes |
|-------------|---------------------|-------|
| `DataGrid` | [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) | Community library; the Toolkit `DataGrid` is no longer maintained. Legacy code may still pin v7 `CommunityToolkit.WinUI.UI.Controls.DataGrid` 7.1.2 |
| `Ribbon` | `CommandBar` / `NavigationView`, or [Toolkit Labs Ribbon](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/Ribbon) | No first-party Ribbon in WinUI; Labs component is experimental/partial |
| `Menu` / `MenuItem` | `MenuBar` / `MenuBarItem` / `MenuFlyout` | `MenuBar` for classic menu, `MenuFlyout` for context |
| `ContextMenu` | `MenuFlyout` | Assign to `ContextFlyout` property |
| `ToolBar` / `ToolBarTray` | `CommandBar` + `AppBarButton` | |
| `StatusBar` | Custom `Grid`/`StackPanel` or `InfoBar` | No StatusBar control |
| `TabControl` | `TabView` or `NavigationView` (top mode) | `TabView` for closeable tabs |
| `DocumentViewer` | `WebView2` | Render PDFs/XPS inside WebView2 |
| `FlowDocument` | `RichTextBlock` | Partial replacement only |
| `RichTextBox` | `RichEditBox` | Rich text editing |
| `GroupBox` | `Expander` (built-in) or `HeaderedContentControl` (Toolkit) | See [Layout & Header Controls from CommunityToolkit.WinUI](#layout--header-controls-from-communitytoolkitwinui) below |
| `Label` | `TextBlock` | WPF `Label` is a `ContentControl`; use `TextBlock` + `AccessKey` |
| `TreeView` | `TreeView` (native) | Available natively, but data binding model differs significantly |
| `MessageBox` | `ContentDialog` | Must set `XamlRoot` before `ShowAsync()` |
| `MediaElement` | `MediaPlayerElement` | Different API |
| `AccessText` | Not available | Use `AccessKey` property on target control |
### Layout & Header Controls from CommunityToolkit.WinUI
These WPF controls have no built-in WinUI 3 equivalent — install the corresponding CommunityToolkit package. **The NuGet package id and the XAML namespace differ intentionally**: package names end in `.Primitives` / `.HeaderedControls`, but the registered XAML namespace is the shorter `CommunityToolkit.WinUI.Controls` (confirmed in the [official Microsoft Q&A](https://learn.microsoft.com/en-us/answers/questions/5746230/why-does-communitytoolkit-uwp-controls-primitive-u)).
| WPF Control | WinUI 3 Replacement | NuGet Package | XAML Namespace |
|-------------|---------------------|---------------|----------------|
| `WrapPanel` | `WrapPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
| `UniformGrid` | `UniformGrid` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
| `DockPanel` | `DockPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
| `GroupBox` (alt.) | `HeaderedContentControl` | `CommunityToolkit.WinUI.Controls.HeaderedControls` | `using:CommunityToolkit.WinUI.Controls` |
### No Equivalent — Requires Architectural Rework
These WPF features have no WinUI counterpart and require redesign, not find-and-replace:
| WPF Feature | WinUI 3 Replacement Strategy |
|-------------|------------------------------|
| `Style.Triggers` / `DataTrigger` | `VisualStateManager` with `StateTrigger` — see [XAML Migration](./references/xaml-migration.md) |
| `MultiBinding` | `x:Bind` function binding: `{x:Bind local:Converters.Format(VM.A, VM.B), Mode=OneWay}` |
| `RoutedUICommand` / `CommandBinding` | `ICommand` / `[RelayCommand]` from CommunityToolkit.Mvvm. WinUI also has `StandardUICommand` / `XamlUICommand` for platform commands. |
| `AdornerLayer` / `Adorner` | Depends on use case: `TeachingTip`/`InfoBar` (validation), `Popup` (overlays), `PlaceholderText` (watermarks), Canvas overlay (decorations) |
| `Visibility.Hidden` | `Opacity="0"` with `Visibility="Visible"` (preserves layout space) |
| `Window.Resources` / `Window.DataContext` | Move to root `Grid.Resources` / root `Page`/`UserControl` — WinUI `Window` is NOT a DependencyObject |
| Tunneling events (`Preview*`) | Use bubbling equivalents + `Handled` property or `AddHandler(handledEventsToo: true)` |
### Critical API Replacements
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`) |
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`), async by default |
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` | Property vs method |
| `Application.Current.Dispatcher` | Store `DispatcherQueue` in static field | See [Threading](./references/threading-and-windowing.md) |
| `Window.Current` | Custom `App.Window` static property | Not supported in Windows App SDK |
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
| `MessageBox.Show()` | `ContentDialog` | Must set `XamlRoot` |
| `System.Windows.Clipboard` | `Windows.ApplicationModel.DataTransfer.Clipboard` | Different API surface |
| `RoutedUICommand` / `CommandBinding` | `ICommand` / `[RelayCommand]` | Remove `CommandBinding`; bind `ICommand` directly |
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
| `DynamicResource` | `ThemeResource` | Theme-reactive only |
| `clr-namespace:` | `using:` | XAML namespace prefix |
| `{x:Static props:Resources.Key}` | `x:Uid` or `ResourceLoader.GetString()` | .resx → .resw |
| `DataType="{x:Type m:Foo}"` | Remove or use code-behind | `x:Type` not supported |
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
| `DataType="{x:Type m:Foo}"` | `x:DataType="m:Foo"` | `x:Type` not supported |
| `SizeToContent="Height"` | Custom `SizeToContent()` via `AppWindow.Resize()` | See [Windowing](./references/threading-and-windowing.md) |
| `MouseLeftButtonDown` | `PointerPressed` | Mouse → Pointer events |
| `Pack URI (pack://...)` | `ms-appx:///` | Resource URI scheme |
| `Observable` (custom base) | `ObservableObject` + `[ObservableProperty]` | CommunityToolkit.Mvvm |
| `RelayCommand` (custom) | `[RelayCommand]` source generator | CommunityToolkit.Mvvm |
| `JpegBitmapEncoder` | `BitmapEncoder.CreateAsync(JpegEncoderId, stream)` | Async, unified API |
| `encoder.QualityLevel = 85` | `BitmapPropertySet { "ImageQuality", 0.85f }` | int 1-100 → float 0-1 |
### Event Replacements (Mouse → Pointer)
| WPF Event | WinUI 3 Event | Notes |
|-----------|--------------|-------|
| `MouseLeftButtonDown` | `PointerPressed` | Check `IsLeftButtonPressed` on args |
| `MouseLeftButtonUp` | `PointerReleased` | Check pointer properties |
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` with right button check |
| `MouseMove` | `PointerMoved` | `MouseEventArgs``PointerRoutedEventArgs` |
| `MouseWheel` | `PointerWheelChanged` | Different event args |
| `MouseEnter` / `MouseLeave` | `PointerEntered` / `PointerExited` | |
| `MouseDoubleClick` | `DoubleTapped` | Different event args |
| `PreviewMouseDown` | `PointerPressed` | No tunneling — use `Handled` or `AddHandler` |
| `PreviewKeyDown` | `KeyDown` | `KeyEventArgs``KeyRoutedEventArgs` |
### Property Replacements
| WPF | WinUI 3 | Context |
|-----|---------|---------|
| `Visibility.Hidden` | `Visibility.Collapsed` or `Opacity="0"` | Use `Opacity="0"` to preserve layout |
| `TextWrapping.WrapWithOverflow` | `TextWrapping.Wrap` | WinUI doesn't distinguish |
| `Focusable="True"` | `IsTabStop="True"` | Different property name |
| `ContextMenu=` | `ContextFlyout=` | On any `UIElement` |
| `MediaElement` | `MediaPlayerElement` | Different API |
| `SnapsToDevicePixels` | Not available | WinUI handles pixel snapping internally |
### NuGet Package Migration
| WPF | WinUI 3 |
|-----|---------|
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` |
| `WPF-UI` (Lepo) | Remove — use native WinUI 3 controls |
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) |
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` |
| (none) | `Microsoft.WindowsAppSDK` |
| (none) | `Microsoft.Windows.SDK.BuildTools` |
| (none) | `WinUIEx` (optional, for window helpers) |
| (none) | `CommunityToolkit.WinUI.Converters` |
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` | |
| `WPF-UI` (Lepo) | **Remove** — use native WinUI 3 controls | |
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) | |
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` | |
| (none) | `Microsoft.WindowsAppSDK` | Required |
| (none) | `Microsoft.Windows.SDK.BuildTools` | Required |
| (none) | `WinUIEx` | Optional, window helpers |
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
| (none) | `CommunityToolkit.WinUI.Controls.Primitives` | Optional — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox` |
| (none) | `CommunityToolkit.WinUI.Controls.HeaderedControls` | Optional — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView` |
| (none) | `CommunityToolkit.WinUI.Controls.SettingsControls` | Optional — `SettingsCard`, `SettingsExpander` |
| (none) | `CommunityToolkit.WinUI.Controls.Sizers` | Optional — `GridSplitter` |
| (none) | `CommunityToolkit.WinUI.UI.Controls.DataGrid` | Legacy v7 — only for migrating existing `DataGrid` code; prefer `WinUI.TableView` |
### XAML Syntax Changes
| WPF | WinUI 3 |
|-----|---------|
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` |
| `{DynamicResource Key}` | `{ThemeResource Key}` |
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind |
| `{x:Type local:MyType}` | Not supported |
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` |
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` |
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` |
| `<ui:FluentWindow>` / `<ui:Button>` (WPF-UI) | Native `<Window>` / `<Button>` |
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` |
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` |
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / handle via KeyDown |
| `<AccessText>` | Not available — use `AccessKey` property |
| `<behaviors:Interaction.Triggers>` | Migrate to code-behind or WinUI behaviors |
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` | CLR → using syntax |
| `{DynamicResource Key}` | `{ThemeResource Key}` | Re-evaluates on theme change |
| `{StaticResource Key}` | `{StaticResource Key}` | Same — resolved once at load |
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind | |
| `{x:Type local:MyType}` | Not supported | Use `x:DataType` for DataTemplate |
| `{x:Array}` | Not supported | Create collections in code-behind |
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` | See [XAML Migration](./references/xaml-migration.md) |
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` | |
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` | |
| `sys:String` / `sys:Int32` / etc. | `x:String` / `x:Int32` / etc. | XAML intrinsic types |
| `<ui:FluentWindow>` (WPF-UI) | `<Window>` | Native + `ExtendsContentIntoTitleBar` |
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` | |
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` | Named style keys |
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / KeyDown | |
| `<AccessText>` | Not available — use `AccessKey` property | |
| `<behaviors:Interaction.Triggers>` | Code-behind or WinUI behaviors | |
| `Window.Resources` | Root container's `Resources` (e.g. `Grid.Resources`) | Window is not a DependencyObject |
### Binding: {Binding} vs {x:Bind}
Both work in WinUI 3. Prefer `{x:Bind}` for new/migrated code.
| Feature | `{Binding}` | `{x:Bind}` |
|---------|------------|------------|
| Default mode | `OneWay` | **`OneTime`** — add `Mode=OneWay` explicitly! |
| Default source | `DataContext` | Page/UserControl code-behind |
| Compile-time validation | No | Yes |
| Function binding | No | Yes (replaces `MultiBinding`) |
| Performance | Reflection-based | Compiled, no reflection |
| `MultiBinding` support | No (not in WinUI) | Use function binding |
## Detailed Reference Docs
@@ -151,6 +287,8 @@ Read only the section relevant to your current task:
| JPEG quality value wrong after migration | WPF: int 1-100; WinRT: float 0.0-1.0 |
| MSIX packaging fails in PreBuildEvent | Move to PostBuildEvent; artifacts not ready at PreBuild time |
| RC file icon path with forward slashes | Use double-backslash escaping: `..\\ui\\Assets\\icon.ico` |
| `COMException: ClassFactory cannot supply requested class` at startup | Missing `<WindowsPackageType>None</WindowsPackageType>` and/or `<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>` in csproj. Without these, the app tries to locate the Windows App SDK framework package (not installed) instead of using bundled runtime DLLs. **Both properties are mandatory for every WinUI 3 module in PowerToys.** |
| `CombinedGeometry` not available in WinUI 3 | WinUI 3 `UIElement.Clip` only accepts `RectangleGeometry`. For overlay hole effects (exclude region), use a `Path` element with `GeometryGroup FillRule="EvenOdd"` containing two `RectangleGeometry` children — the EvenOdd rule creates a transparent hole where geometries overlap. |
## Troubleshooting
@@ -163,3 +301,4 @@ Read only the section relevant to your current task:
| NuGet restore failures | Run `build-essentials.cmd` after adding `Microsoft.WindowsAppSDK` package |
| `Parallel.ForEach` compilation error | Migrate to `Parallel.ForEachAsync` for async imaging operations |
| Signing check fails on leaked artifacts | Run `generateAllFileComponents.ps1`; verify only `WinUI3Apps\\` paths in signing config |
| `COMException` / `ClassFactory` error at app launch | Ensure csproj has `<WindowsPackageType>None</WindowsPackageType>` and `<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>`. These are required for all unpackaged WinUI 3 apps in PowerToys — without them the WinUI 3 COM runtime cannot be found. |

View File

@@ -4,24 +4,26 @@ Complete reference for mapping WPF types to WinUI 3 equivalents, based on the Im
## Root Namespace Mapping
| WPF Namespace | WinUI 3 Namespace |
|---------------|-------------------|
| `System.Windows` | `Microsoft.UI.Xaml` |
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` |
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` |
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` |
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` |
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` |
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` |
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` |
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` |
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` |
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
| `System.Windows.Interop` | `WinRT.Interop` |
| WPF Namespace | WinUI 3 Namespace | Notes |
|---------------|-------------------|-------|
| `System.Windows` | `Microsoft.UI.Xaml` | Root namespace |
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` | Accessibility / UI Automation |
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` | |
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` | Core controls |
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` | Low-level primitives |
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` | Binding, IValueConverter |
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` | Limited — RichTextBlock + Paragraph only |
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` | Pointer, keyboard, focus |
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` | XAML parsing, markup extensions |
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` | Brushes, transforms |
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` | Storyboard, animations |
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` | UI display only — use `Windows.Graphics.Imaging` for processing |
| `System.Windows.Media.Media3D` | **No equivalent** | Use Win2D or Composition APIs |
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` | For Frame navigation events; no `NavigationService` |
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` | Rectangle, Ellipse, Path |
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` | Dispatcher → DispatcherQueue |
| `System.Windows.Interop` | `WinRT.Interop` | HWND interop |
| `System.Windows.Interop` | `Microsoft.UI.Xaml.Hosting` | XAML Islands |
## Core Type Mapping
@@ -45,7 +47,7 @@ Complete reference for mapping WPF types to WinUI 3 equivalents, based on the Im
These controls exist in both frameworks with the same name — change `System.Windows.Controls` to `Microsoft.UI.Xaml.Controls`:
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
### Controls With Different Names or Behavior
@@ -56,6 +58,7 @@ These controls exist in both frameworks with the same name — change `System.Wi
| `TabControl` | `TabView` | Different API |
| `Menu` | `MenuBar` | Different API |
| `StatusBar` | Custom `StackPanel` layout | No built-in equivalent |
| `ListBox` | `ListView` (or `ItemsView`) | **Deprecated in WinUI 3 — do not use.** Prefer `ListView`, or `ItemsView` (WinUI 1.5+) for modern collection scenarios |
| `AccessText` | Not available | Use `AccessKey` property on target control |
### WPF-UI (Lepo) to Native WinUI 3
@@ -109,6 +112,43 @@ Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All
| `System.Windows.Interop.WindowInteropHelper` | `WinRT.Interop.WindowNative.GetWindowHandle()` | |
| `System.Windows.SystemColors` | Resource keys via `ThemeResource` | No direct static class |
| `System.Windows.SystemParameters` | Win32 API or `DisplayInformation` | No direct equivalent |
| `System.Windows.Clipboard` | `Windows.ApplicationModel.DataTransfer.Clipboard` | Different API surface |
| `System.Windows.Input.RoutedUICommand` | `Microsoft.UI.Xaml.Input.StandardUICommand` / `XamlUICommand` | Or use `ICommand` / `[RelayCommand]` |
| `System.Windows.Input.CommandBinding` | **Remove** | Bind `ICommand` directly in XAML |
## Controls That Need Translation (No 1:1 Mapping)
These controls exist in WPF but require a different control, third-party library, or Community Toolkit package in WinUI 3:
| WPF Control | WinUI 3 Replacement | Package / Notes |
|-------------|---------------------|-----------------|
| `DataGrid` | [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) | Community library; the Toolkit `DataGrid` is no longer maintained. PowerToys legacy modules may still pin v7 `CommunityToolkit.WinUI.UI.Controls.DataGrid` 7.1.2 — prefer `WinUI.TableView` for new work. |
| `Ribbon` | `CommandBar` / `NavigationView`, or [Toolkit Labs Ribbon](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/Ribbon) | No first-party Ribbon in WinUI; the Labs component is experimental and partial |
| `Menu` / `MenuItem` | `MenuBar` / `MenuBarItem` / `MenuFlyoutItem` | `MenuBar` for classic menu, `MenuFlyout` for context |
| `ContextMenu` | `MenuFlyout` | Assign to `ContextFlyout` property |
| `ToolBar` / `ToolBarTray` | `CommandBar` + `AppBarButton` | |
| `StatusBar` | Custom `Grid`/`StackPanel` or `InfoBar` | No StatusBar control |
| `TabControl` | `TabView` or `NavigationView` (top mode) | `TabView` for closeable tabs |
| `DocumentViewer` | `WebView2` | `Microsoft.Web.WebView2` — render PDFs/XPS |
| `FlowDocument` | `RichTextBlock` | Partial replacement only |
| `RichTextBox` | `RichEditBox` | Rich text editing |
| `GroupBox` | `Expander` (built-in) or `HeaderedContentControl` (Toolkit) | See [Layout & Header Controls from CommunityToolkit.WinUI](#layout--header-controls-from-communitytoolkitwinui) below |
| `Label` | `TextBlock` | WPF `Label` is a `ContentControl`; use `TextBlock` + `AccessKey` |
| `TreeView` | `TreeView` (native) | Available natively, but data binding model differs significantly |
| `MediaElement` | `MediaPlayerElement` | Different API |
## Layout & Header Controls from CommunityToolkit.WinUI
These WPF layout/header controls have no built-in WinUI 3 equivalent — install the corresponding CommunityToolkit package. **The NuGet package id and the XAML namespace differ intentionally**: package names end in `.Primitives` / `.HeaderedControls`, but both packages register their controls in the shorter `CommunityToolkit.WinUI.Controls` XAML namespace (confirmed in the [official Microsoft Q&A](https://learn.microsoft.com/en-us/answers/questions/5746230/why-does-communitytoolkit-uwp-controls-primitive-u): *"all controls live under `CommunityToolkit.WinUI.Controls` … This is intentional"*).
| WPF Control | WinUI 3 Replacement | NuGet Package | XAML Namespace |
|-------------|---------------------|---------------|----------------|
| `WrapPanel` | `WrapPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
| `UniformGrid` | `UniformGrid` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
| `DockPanel` | `DockPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
| `GroupBox` (alt.) | `HeaderedContentControl` | `CommunityToolkit.WinUI.Controls.HeaderedControls` | `using:CommunityToolkit.WinUI.Controls` |
Other primitives in `Primitives`: `ConstrainedBox`, `SwitchPresenter`, `WrapLayout`, `StaggeredPanel`. Other Headered controls in `HeaderedControls`: `HeaderedItemsControl`, `HeaderedTreeView`.
## NuGet Package Migration
@@ -124,6 +164,11 @@ Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All
| (none) | `WinUIEx` | Optional, window helpers |
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
| (none) | `CommunityToolkit.WinUI.Extensions` | Optional |
| (none) | `CommunityToolkit.WinUI.Controls.Primitives` | Optional — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox`, `SwitchPresenter` |
| (none) | `CommunityToolkit.WinUI.Controls.HeaderedControls` | Optional — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView` |
| (none) | `CommunityToolkit.WinUI.Controls.SettingsControls` | Optional — `SettingsCard`, `SettingsExpander` |
| (none) | `CommunityToolkit.WinUI.Controls.Sizers` | Optional — `GridSplitter`, `PropertySizer` |
| (none) | `CommunityToolkit.WinUI.UI.Controls.DataGrid` | Legacy v7 — only if migrating existing `DataGrid` code; prefer [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) for new work |
| (none) | `Microsoft.Web.WebView2` | If using WebView |
## Project File Changes
@@ -167,8 +212,9 @@ Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All
Key changes:
- `UseWPF``UseWinUI`
- TFM: `net8.0-windows``net8.0-windows10.0.19041.0`
- Add `WindowsPackageType=None` for unpackaged desktop apps
- Add `SelfContained=true` + `WindowsAppSDKSelfContained=true`
- **CRITICAL: Add `WindowsPackageType=None`** — marks the app as unpackaged (no MSIX). Without this, the build produces an MSIX-style package that won't run as a standalone PowerToys module.
- **CRITICAL: Add `WindowsAppSDKSelfContained=true`** — bundles the Windows App SDK runtime DLLs (e.g. `Microsoft.UI.Xaml.dll`) into the output directory. Without this, the app throws `COMException: ClassFactory cannot supply requested class` at startup because the WinUI 3 COM classes cannot be found.
- Add `SelfContained=true` (usually via `Common.SelfContained.props`)
- Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs` entry point
- Set `ProjectPriFileName` to match your module's assembly name
- Move icon from `Resources/` to `Assets/<Module>/`

View File

@@ -127,6 +127,74 @@ If the module uses the `WPF-UI` library, replace all Lepo controls with native W
</Window>
```
#### Recommended: Use WindowEx from WinUIEx
> **Tip:** Prefer `WinUIEx.WindowEx` over bare `Window`. It restores many WPF-like window properties directly in XAML, avoiding boilerplate code-behind for common windowing tasks.
```xml
<!-- WinUI 3 with WindowEx (preferred in PowerToys) -->
<winuiex:WindowEx
xmlns:winuiex="using:WinUIEx"
x:Class="MyApp.MainWindow"
MinWidth="480"
MinHeight="320"
IsShownInSwitchers="True"
IsTitleBarVisible="True">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
...
</Grid>
</winuiex:WindowEx>
```
Properties available on `WindowEx` that mirror WPF `Window`:
| WPF Window Property | WindowEx Property | Notes |
|---------------------|-------------------|-------|
| `MinWidth` / `MinHeight` | `MinWidth` / `MinHeight` | Set directly in XAML |
| `Width` / `Height` | `Width` / `Height` | Initial window size |
| `WindowState` | `WindowState` | Minimized, Maximized, Normal |
| `Title` | `Title` or `x:Uid` | Window title |
| `Icon` | Use `TitleBar.IconSource` | Via WinUI TitleBar control |
| `ShowInTaskbar` | `IsShownInSwitchers` | Alt-Tab visibility |
| `TopMost` | `IsAlwaysOnTop` | Always-on-top window |
NuGet: `WinUIEx` — already referenced by most PowerToys modules.
#### Recommended: Page-in-Window Architecture
> **Tip:** WinUI 3 `Window` is NOT a `FrameworkElement` — it does not support `Resources`, `DataContext`, `x:Bind`, or `VisualStateManager` directly. Place a `Page` as the Window's root content to regain these WPF-like capabilities.
```xml
<!-- WinUI 3 — Window contains a Page for full FrameworkElement support -->
<winuiex:WindowEx x:Class="MyApp.MainWindow"
xmlns:winuiex="using:WinUIEx"
xmlns:views="using:MyApp.Views">
<views:MainPage x:Name="mainPage" />
</winuiex:WindowEx>
```
```xml
<!-- MainPage.xaml — has full FrameworkElement capabilities -->
<Page x:Class="MyApp.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Page.Resources>
<!-- Resources work here (unlike on Window) -->
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
</Page.Resources>
<Grid>
<VisualStateManager.VisualStateGroups>
<!-- VisualStateManager works here (unlike on Window) -->
</VisualStateManager.VisualStateGroups>
...
</Grid>
</Page>
```
This is the standard pattern in PowerToys (e.g., FileLocksmith, EnvironmentVariables).
### App.xaml Resources
```xml
@@ -150,6 +218,20 @@ If the module uses the `WPF-UI` library, replace all Lepo controls with native W
</Application.Resources>
```
### CommunityToolkit.WinUI — WPF Replacement Controls
> **Tip:** The `CommunityToolkit.WinUI` package provides many controls and helpers familiar to WPF developers that are missing from WinUI 3 out of the box. Before writing custom replacements, check whether CommunityToolkit already provides what you need.
Key packages (XAML namespace is `using:CommunityToolkit.WinUI.Controls` for the `Controls.*` family):
- **`CommunityToolkit.WinUI.Controls.Primitives`** — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox`, `SwitchPresenter`
- **`CommunityToolkit.WinUI.Controls.HeaderedControls`** — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView`
- **`CommunityToolkit.WinUI.Controls.SettingsControls`** — `SettingsCard`, `SettingsExpander`
- **`CommunityToolkit.WinUI.Controls.Sizers`** — `GridSplitter`, `PropertySizer`, `ContentSizer`
- **`CommunityToolkit.WinUI.UI.Controls.DataGrid`** — legacy v7 `DataGrid` (no longer maintained); prefer [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) for new work
- **`CommunityToolkit.WinUI.Converters`** — Common value converters (`BoolToVisibilityConverter`, `StringFormatConverter`, etc.)
- **`CommunityToolkit.WinUI.Behaviors`** — XAML behaviors for animations and interactions
- **`CommunityToolkit.WinUI.Extensions`** — Extension methods for WinUI types
### Common Control Replacements
```xml
@@ -187,11 +269,135 @@ If the module uses the `WPF-UI` library, replace all Lepo controls with native W
<!-- Handle Enter/Escape keys in code-behind if needed -->
```
## No-Equivalent Patterns (Requires Architectural Rework)
These WPF features demand design changes, not find-and-replace. Read this section BEFORE attempting to migrate any file that uses these patterns.
### MultiBinding → x:Bind Function Binding
WinUI does not support `MultiBinding`. Replace with `x:Bind` function binding (most direct replacement), a computed ViewModel property, or multiple simple bindings.
**WPF:**
```xml
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
```
**WinUI 3:**
```xml
<TextBlock Text="{x:Bind local:Converters.FormatFullName(ViewModel.FirstName, ViewModel.LastName), Mode=OneWay}" />
```
```csharp
public static class Converters
{
public static string FormatFullName(string first, string last) => $"{first} {last}";
}
```
### Adorners → Context-Dependent Replacements
WPF's `AdornerLayer` has no WinUI equivalent. Choose replacement by use case:
| Adorner Use Case | WinUI 3 Replacement |
|------------------|---------------------|
| Validation indicators | `TeachingTip`, `InfoBar`, or InputValidation templates |
| Resize handles | `Popup` positioned relative to target |
| Drag preview | `DragItemsStarting` event with custom DragUI |
| Overlay decorations | Canvas overlay or Popup layer |
| Watermark / Placeholder | `TextBox.PlaceholderText` (built-in) |
### RoutedUICommand → ICommand / RelayCommand
WinUI does not support routed commands or `CommandBinding`. Replace with standard `ICommand` pattern:
```csharp
// CommunityToolkit.Mvvm
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save() { /* save logic */ }
private bool CanSave() => IsDirty;
```
WinUI 3 also provides `StandardUICommand` and `XamlUICommand` for pre-defined platform commands (Cut, Copy, Paste, Delete) with built-in icons and keyboard accelerators.
### Tunneling / Preview Events
WinUI has no tunneling event model. `PreviewMouseDown`, `PreviewKeyDown`, etc. do not exist.
- Replace with the bubbling equivalent (`PointerPressed`, `KeyDown`)
- If you relied on tunneling to intercept events before children, restructure using the `Handled` property
- For must-handle scenarios, use `AddHandler` with `handledEventsToo: true`:
```csharp
myElement.AddHandler(UIElement.PointerPressedEvent,
new PointerEventHandler(OnPointerPressed), handledEventsToo: true);
```
## Style and Template Changes
### Implicit Styles — Always Use BasedOn
> **Warning:** In WinUI 3, always use `BasedOn` when overriding default control styles. Without it, your style **replaces the entire default style** rather than extending it.
```xml
<!-- WRONG — replaces entire default style, control may lose all visual appearance -->
<Style TargetType="Button">
<Setter Property="Background" Value="Red" />
</Style>
<!-- CORRECT — extends the default style -->
<Style TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}">
<Setter Property="Background" Value="Red" />
</Style>
```
### Triggers → VisualStateManager
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported. Two replacement approaches:
#### Approach 1: StateTrigger (direct DataTrigger replacement — simpler)
Use this for data-driven state changes. This is the closest equivalent to WPF `DataTrigger`:
**WPF:**
```xml
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter Property="Background" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
```
**WinUI 3:**
```xml
<Border x:Name="MyBorder">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Active">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind ViewModel.IsActive, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="MyBorder.Background" Value="Green" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Border>
```
Note: `VisualStateManager` must be placed on a control inside the Window, NOT on the Window itself.
#### Approach 2: ControlTemplate (for property triggers like IsMouseOver)
Use this when replacing `<Trigger Property="IsMouseOver">` or similar control-state triggers:
**WPF:**
```xml
@@ -200,9 +406,6 @@ WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue"/>
</Trigger>
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
</Style.Triggers>
</Style>
```
@@ -249,11 +452,42 @@ WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
| `Disabled` | `Disabled` |
| `Pressed` | `Pressed` |
## Visibility.Hidden — No Equivalent
WinUI only has `Visible` and `Collapsed`. There is no `Hidden`.
| WPF | WinUI 3 | Behavior |
|-----|---------|----------|
| `Visibility.Visible` | `Visibility.Visible` | Rendered and occupies layout space |
| `Visibility.Hidden` | **Not available** | Use `Opacity="0"` with `Visibility="Visible"` to hide but keep layout |
| `Visibility.Collapsed` | `Visibility.Collapsed` | Not rendered, no layout space |
## Resource Dictionary Changes
### Window.Resources → Grid.Resources
### XamlControlsResources Must Be First
WinUI 3 `Window` is NOT a `DependencyObject` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
> **Warning:** `XamlControlsResources` must be the **first** merged dictionary in `App.xaml`. It provides the default Fluent styles. Omitting it gives you controls with no visual appearance. Resource paths use `ms-appx:///` instead of relative paths.
```xml
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- MUST be first -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Then your custom dictionaries -->
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
```
### Window.Resources → Grid.Resources (or use Page)
WinUI 3 `Window` is NOT a `FrameworkElement` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
**Preferred approach:** Use the [Page-in-Window architecture](#recommended-page-in-window-architecture) described above. A `Page` inside the `Window` gives you full `FrameworkElement` capabilities (Resources, DataContext, x:Bind, VisualStateManager).
**Fallback** (for simple windows without a Page):
```xml
<!-- WPF -->
@@ -317,19 +551,25 @@ Both are available. Prefer `{x:Bind}` for compile-time safety and performance.
| Performance | Reflection-based | Compiled |
| Function binding | No | Yes |
### WPF-Specific Binding Features to Remove
### Binding Differences from WPF
These WPF binding patterns behave differently in WinUI 3 — review on a case-by-case basis rather than mechanically removing.
```xml
<!-- These WPF-only features must be removed or rewritten -->
<!-- UpdateSourceTrigger: limited support in WinUI 3 -->
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
<!-- WinUI 3: UpdateSourceTrigger not needed; TextBox uses PropertyChanged by default -->
<!-- WinUI 3: UpdateSourceTrigger=LostFocus does NOT exist; PropertyChanged is the TextBox default.
Prefer x:Bind, which binds with PropertyChanged semantics by default for TwoWay. -->
<TextBox Text="{x:Bind ViewModel.Value, Mode=TwoWay}" />
{Binding RelativeSource={RelativeSource Self}, ...}
<!-- WinUI 3: Use x:Bind which binds to the page itself, or use ElementName -->
<!-- RelativeSource: Self and TemplatedParent ARE supported in WinUI 3 -->
<TextBlock Text="{Binding Tag, RelativeSource={RelativeSource Self}}" />
<!-- Works in WinUI 3. FindAncestor mode is NOT supported — use ElementName, x:Bind,
or the CommunityToolkit FrameworkElementExtensions.Ancestor attached property to reach ancestors. -->
<!-- {Binding} empty path: works in WinUI 3 (binds to the current DataContext) -->
<ItemsControl ItemsSource="{Binding}" />
<!-- WinUI 3: Must specify explicit path -->
<!-- This is valid. Note: x:Bind requires an explicit path — there is no empty-path x:Bind. -->
<ItemsControl ItemsSource="{x:Bind ViewModel.Items}" />
```
@@ -355,6 +595,64 @@ ExtendsContentIntoTitleBar="True" <!-- Set in code-behind -->
| `IsHitTestVisible` | `IsHitTestVisible` | Same |
| `TextBox.VerticalScrollBarVisibility` | `ScrollViewer.VerticalScrollBarVisibility` (attached) | Attached property |
## Complete Find-and-Replace Reference
Use this table for mechanical batch translation. Apply these rules consistently to every file.
### XAML Attribute Replacements
| Find | Replace With | Context |
|------|-------------|---------|
| `ContextMenu=` | `ContextFlyout=` | On any UIElement |
| `{DynamicResource ` | `{ThemeResource ` | Theme-responsive references |
| `{x:Static prefix:Resources.Key}` | `x:Uid="Key"` (with `.resw`) | Resource string — most common WPF case; mechanical `{x:Bind}` will NOT compile here |
| `{x:Static prefix:Type.Member}` | `{x:Bind prefix:Type.Member}` | Static field/property reference (function binding) |
| `Visibility="Hidden"` | `Visibility="Collapsed"` | Or use `Opacity="0"` for layout |
| `MouseLeftButtonDown` | `PointerPressed` | Event handlers |
| `MouseLeftButtonUp` | `PointerReleased` | Event handlers |
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` + check `IsRightButtonPressed` |
| `MouseRightButtonUp` | `PointerReleased` + check `IsRightButtonPressed` | No direct WinUI event; use `RightTapped` only for context-menu-open semantics |
| `MouseEnter` | `PointerEntered` | Event handlers |
| `MouseLeave` | `PointerExited` | Event handlers |
| `MouseMove` | `PointerMoved` | Event handlers |
| `MouseWheel` | `PointerWheelChanged` | Event handlers |
| `MouseDoubleClick` | `DoubleTapped` | Event handlers |
| `PreviewMouseDown` | `PointerPressed` | No tunneling in WinUI — remove Preview |
| `PreviewMouseUp` | `PointerReleased` | No tunneling in WinUI — remove Preview |
| `PreviewKeyDown` | `KeyDown` | No tunneling in WinUI — remove Preview |
| `PreviewKeyUp` | `KeyUp` | No tunneling in WinUI — remove Preview |
| `PreviewMouseWheel` | `PointerWheelChanged` | No tunneling in WinUI — remove Preview |
| `Focusable="True"` | `IsTabStop="True"` | Focus behavior |
| `Focusable="False"` | `IsTabStop="False"` | Focus behavior |
| `TextWrapping="WrapWithOverflow"` | `TextWrapping="Wrap"` | TextBlock, TextBox |
| `MediaElement` | `MediaPlayerElement` | Media playback |
| `clr-namespace:` | `using:` | xmlns declarations |
| `;assembly=` | (remove) | Assembly qualification not needed |
### Code-Behind Replacements
| Find | Replace With |
|------|-------------|
| `using System.Windows;` | `using Microsoft.UI.Xaml;` |
| `using System.Windows.Controls;` | `using Microsoft.UI.Xaml.Controls;` |
| `using System.Windows.Media;` | `using Microsoft.UI.Xaml.Media;` |
| `using System.Windows.Data;` | `using Microsoft.UI.Xaml.Data;` |
| `using System.Windows.Input;` | `using Microsoft.UI.Xaml.Input;` |
| `using System.Windows.Threading;` | `using Microsoft.UI.Dispatching;` |
| `using System.Windows.Shapes;` | `using Microsoft.UI.Xaml.Shapes;` |
| `using System.Windows.Markup;` | `using Microsoft.UI.Xaml.Markup;` |
| `using System.Windows.Automation;` | `using Microsoft.UI.Xaml.Automation;` |
| `using System.Windows.Media.Animation;` | `using Microsoft.UI.Xaml.Media.Animation;` |
| `using System.Windows.Documents;` | `using Microsoft.UI.Xaml.Documents;` |
| `using System.Windows.Navigation;` | `using Microsoft.UI.Xaml.Navigation;` |
| `Dispatcher.Invoke(` | `DispatcherQueue.TryEnqueue(` |
| `Dispatcher.BeginInvoke(` | `DispatcherQueue.TryEnqueue(` |
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` |
| `MouseEventArgs` | `PointerRoutedEventArgs` |
| `KeyEventArgs` | `KeyRoutedEventArgs` |
| `RoutedUICommand` | `RelayCommand` (CommunityToolkit.Mvvm) |
| `CommandBinding` | Remove; bind ICommand directly |
## XAML Formatting (XamlStyler)
After migration, run XamlStyler to normalize formatting:

View File

@@ -1,8 +1,10 @@
name: Automatic Triaging on Issue Creation
name: Automatic Triaging on Issue/PR Creation
on:
issues:
types: [opened, reopened]
pull_request_target:
types: [opened, reopened, synchronize]
# Manual trigger: go to Actions → "Automatic Triaging on Issue Creation" → Run workflow.
# Enter one or more comma-separated issue numbers (e.g. "1234" or "1234,1235,1236")
# to apply AI-generated area labels to existing untriaged issues.
@@ -15,12 +17,14 @@ on:
permissions:
models: read
issues: write
pull-requests: write
concurrency:
# Each workflow run gets its own concurrency group.
# For issue events, group by issue number so a rapid close+reopen only runs once.
# For pull request events, group by PR number so rapid updates coalesce.
# For manual dispatch (which may cover multiple issues), use the unique run ID.
group: ${{ github.event_name == 'issues' && format('{0}-issue-{1}', github.workflow, github.event.issue.number) || github.run_id }}
group: ${{ github.event_name == 'issues' && format('{0}-issues-{1}', github.workflow, github.event.issue.number) || github.event_name == 'pull_request_target' && format('{0}-pr-{1}', github.workflow, github.event.pull_request.number) || github.run_id }}
cancel-in-progress: true
jobs:
@@ -28,7 +32,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Apply area labels with AI
uses: actions/github-script@v7
uses: actions/github-script@v9
env:
# actions/github-script does not propagate `github-token` to
# process.env. Expose it explicitly so the inline script can
@@ -38,7 +42,7 @@ jobs:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// When triggered manually, process each supplied issue number in turn.
// When triggered by an issue event, use the event's issue number.
// When triggered by an issue or PR event, use the event's number.
let issueNumbers;
if (context.eventName === 'workflow_dispatch') {
issueNumbers = String(context.payload.inputs.issue_numbers)
@@ -55,23 +59,32 @@ jobs:
}
for (const issueNumber of issueNumbers) {
console.log(`\n--- Processing issue #${issueNumber} ---`);
console.log(`\n--- Processing item #${issueNumber} ---`);
await labelIssue(issueNumber);
}
async function labelIssue(issueNumber) {
// Fetch the issue so both the automatic and manual paths have the same data.
// Fetch as an issue resource; PRs are represented by issues with a pull_request field.
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
});
const itemType = issue.pull_request ? 'Pull request' : 'Issue';
// Skip pull requests that already have labels applied.
if (issue.pull_request && issue.labels && issue.labels.length > 0) {
const existingLabels = issue.labels.map(l => l.name).join(', ');
console.log(`${itemType} #${issueNumber} already has labels (${existingLabels}); skipping.`);
return;
}
const title = issue.title ?? '';
const body = issue.body ?? '';
if (!title && !body) {
console.log(`Issue #${issueNumber} has no title or body; skipping.`);
console.log(`${itemType} #${issueNumber} has no title or body; skipping.`);
return;
}
@@ -124,8 +137,8 @@ jobs:
'Area-Localization',
];
const systemPrompt = `You are a GitHub issue triage assistant for the microsoft/PowerToys repository.
Your job is to classify issues by assigning the correct area label(s).
const systemPrompt = `You are a GitHub triage assistant for the microsoft/PowerToys repository.
Your job is to classify issues and pull requests by assigning the correct area label(s).
Rules:
- Only return labels from the following list, exactly as written:
@@ -136,9 +149,9 @@ jobs:
- Respond with ONLY a JSON array of label strings, no explanation.
Example: ["Product-FancyZones","Product-Settings"]`;
const userPrompt = `Issue title: ${title}
const userPrompt = `${itemType} title: ${title}
Issue body:
${itemType} body:
${body.slice(0, MAX_BODY_LENGTH)}`;
// Validate that the token is available before making the API call.
@@ -203,11 +216,25 @@ jobs:
return;
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: toApply,
});
console.log(`Issue #${issueNumber}: added labels: ${toApply.join(', ')}`);
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: toApply,
});
console.log(`${itemType} #${issueNumber}: added labels: ${toApply.join(', ')}`);
} catch (error) {
// Some contexts (for example, restricted integrations) can deny
// label writes even when workflow permissions request write scope.
// Skip without failing the entire triage workflow.
const status = error?.status;
const message = error?.message ?? String(error);
if (status === 403 && message.includes('Resource not accessible by integration')) {
console.log(`${itemType} #${issueNumber}: skipping label write due to restricted token context (403).`);
return;
}
throw error;
}
}

View File

@@ -21,6 +21,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4

View File

@@ -27,7 +27,7 @@ jobs:
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Run GenAI Issue Deduplicator
uses: pelikhan/action-genai-issue-dedup@v0

View File

@@ -0,0 +1,67 @@
# Builds the Dev Docs website from doc/devdocs with docmd and publishes it to GitHub Pages.
#
# The generated site is uploaded as a Pages artifact and deployed directly. It is never
# committed to the repo, so doc/devdocs-website/site stays untracked (see .gitignore).
#
# Requires GitHub Pages to be enabled with "Source: GitHub Actions" under the repository
# Settings -> Pages.
name: Publish Dev Docs Website
on:
push:
branches:
- main
paths:
- 'doc/devdocs/**'
- 'doc/devdocs-website/**'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
# Allow only one Pages deployment at a time and let an in-progress deploy finish.
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
# Full history so docmd's git plugin can resolve per-page "last updated" dates.
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: latest
- name: Build static site with docmd
working-directory: doc/devdocs-website
# docmd is pinned in package.json; dependencies are installed fresh each run.
run: |
npm install
npm run build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: doc/devdocs-website/site
# v4+ excludes dotfiles by default; keep docmd's generated .nojekyll.
include-hidden-files: true
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5

View File

@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Detect telemetry event changes and comment PR
env:

View File

@@ -4,6 +4,29 @@
<Import Project="$(MSBuildCachePackageRoot)\build\$(MSBuildCachePackageName).targets" Condition="'$(MSBuildCacheEnabled)' == 'true'" />
<Import Project="$(MSBuildCacheSharedCompilationPackageRoot)\build\Microsoft.MSBuildCache.SharedCompilation.targets" Condition="'$(MSBuildCacheEnabled)' == 'true'" />
<!--
Onboarding guard: PowerToys has deeply nested source paths that exceed the legacy
260-character MAX_PATH limit. Without Windows long path support enabled, the build
fails with cryptic "path too long" / "could not find file" errors that are hard for
new contributors to diagnose. Detect the missing registry setting up front and emit a
clear, actionable error before the confusing failures occur.
- Covers both Visual Studio (Ctrl+Shift+B) and the command-line build scripts.
- Runs only during real builds (skips design-time/IntelliSense passes).
- Bypass with /p:SkipLongPathsCheck=true if you know what you're doing.
See tools\build\setup-dev-environment.ps1 to enable everything automatically.
-->
<Target Name="EnsureLongPathsEnabled"
BeforeTargets="PrepareForBuild"
Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipLongPathsCheck)' != 'true' and '$(OS)' == 'Windows_NT'">
<PropertyGroup>
<_LongPathsEnabled>$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem', 'LongPathsEnabled', null, RegistryView.Registry64))</_LongPathsEnabled>
</PropertyGroup>
<Error Condition="'$(_LongPathsEnabled)' != '1'"
Code="PTLONGPATH"
Text="Windows long path support is not enabled. PowerToys source paths exceed the 260-character MAX_PATH limit, so the build will fail with cryptic 'path too long' errors. Fix it by running (from an elevated PowerShell): .\tools\build\setup-dev-environment.ps1 -- or set HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1 (DWORD) and restart Windows. To bypass this check, build with /p:SkipLongPathsCheck=true." />
</Target>
<!-- Override ManifestTool to the x64 host tool under WindowsSdkDir for all projects once the SDK path is known. -->
<PropertyGroup Label="ManifestToolOverride">
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>

View File

@@ -26,7 +26,7 @@
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
@@ -38,7 +38,7 @@
<PackageVersion Include="Mages" Version="3.0.0" />
<PackageVersion Include="Markdig.Signed" Version="0.34.0" />
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
<PackageVersion Include="MessagePack" Version="3.1.3" />
<PackageVersion Include="MessagePack" Version="3.1.7" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.102" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
@@ -64,7 +64,7 @@
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.71.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.71.0-alpha" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.4022.49" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="10.0.8" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
@@ -76,7 +76,7 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.1.0" />
@@ -151,4 +151,4 @@
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
</ItemGroup>
</Project>
</Project>

View File

@@ -12,6 +12,7 @@ This software incorporates material from third parties.
- Peek
- PowerDisplay
- Registry Preview
- ZoomIt
## Utility: Color Picker
@@ -1549,6 +1550,69 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
## Utility: ZoomIt
### libwebp
ZoomIt uses libwebp to encode screenshots in the WebP image format.
**Source**: <https://github.com/webmproject/libwebp>
BSD-3-Clause License
Copyright (c) 2010, Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Google nor the names of its contributors may
be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Additional IP Rights Grant (Patents)
"These implementations" means the copyrightable works that implement the WebM
codecs distributed by Google as part of the WebM Project.
Google hereby grants to you a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable (except as stated in this section) patent license to
make, have made, use, offer to sell, sell, import, transfer, and otherwise
run, modify and propagate the contents of these implementations of WebM, where
such license applies only to those patent claims, both currently owned by
Google and acquired in the future, licensable by Google that are necessarily
infringed by these implementations of WebM. This grant does not include claims
that would be infringed only as a consequence of further modification of these
implementations. If you or your agent or exclusive licensee institute or order
or agree to the institution of patent litigation or any other patent
enforcement activity against any entity (including a cross-claim or
counterclaim in a lawsuit) alleging that any of these implementations of WebM
or any code incorporated within any of these implementations of WebM
constitute direct or contributory patent infringement, or inducement of
patent infringement, then any patent rights granted to you under this License
for these implementations of WebM shall terminate as of the date such
litigation is filed.
## NuGet Packages used by PowerToys
- AdaptiveCards.ObjectModel.WinUI3

View File

@@ -656,7 +656,10 @@
</Project>
</Folder>
<Folder Name="/modules/launcher/Tests/">
<File Path="src/modules/launcher/Plugins/Microsoft.Plugin.Folder.UnitTests/Microsoft.Plugin.Folder.UnitTests.csproj" />
<Project Path="src/modules/launcher/Plugins/Microsoft.Plugin.Folder.UnitTests/Microsoft.Plugin.Folder.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -1004,6 +1007,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuide.UnitTests/ShortcutGuide.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj" Id="e487304a-b1fb-4e6b-8e70-014051af5b99" />
</Folder>
<Folder Name="/modules/Workspaces/">

2
doc/devdocs-website/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# docmd build output — published to GitHub Pages by CI, not committed.
site/

View File

@@ -0,0 +1,3 @@
# No lockfile: docmd is pinned in package.json and installed fresh on every build
# (locally and in CI), so a tracked package-lock.json is unnecessary here.
package-lock=false

View File

@@ -0,0 +1,44 @@
# Dev Docs Website
This folder hosts the [docmd](https://docmd.io/) project that turns the PowerToys developer
documentation in [`doc/devdocs`](../devdocs) into a static website.
## Generated site
The `site/` folder is the docmd build output. It is **not committed** to the repository &mdash; it is
git-ignored and rebuilt on demand. You only need it locally when previewing your changes (see below).
Publishing is handled by the
[Publish Dev Docs Website](../../.github/workflows/regenerate-devdocs-website.yml) GitHub Action, which
runs whenever files under `doc/devdocs` (or this folder) change on the `main` branch &mdash; it can also
be triggered manually from the **Actions** tab. The action builds the site and deploys it straight to
GitHub Pages as an artifact, so nothing is written back to the repository.
> [!NOTE]
> The action requires GitHub Pages to be enabled with **Source: GitHub Actions** under the repository
> **Settings → Pages**.
## Editing the docs
To change the documentation, edit the Markdown files under [`doc/devdocs`](../devdocs). The remaining
files in this folder are maintained by hand and are safe to edit:
- `docmd.config.json` &mdash; docmd configuration (title, source, output, plugins)
- `package.json` &mdash; pins the docmd version used to build the site
- `docmd-plugins/` &mdash; local build-time docmd plugins
> [!TIP]
> Link to repository files with repo-root-relative paths such as `/src/modules/.../Foo.cpp`.
> VS Code resolves these against the workspace root (so they open the local file), and the
> bundled `github-source-links` plugin rewrites them to
> `https://github.com/microsoft/PowerToys/blob/main/...` on the published site.
## Building locally
Requires [Node.js](https://nodejs.org/).
```powershell
npm install # install dependencies (first time only)
npm run dev # start a local preview server at http://localhost:3000
npm run build # generate the static site into ./site
```

View File

@@ -0,0 +1,52 @@
// docmd plugin: github-source-links
//
// The dev docs link to source files with repo-root-relative paths such as
// "/src/modules/.../Foo.cpp". VS Code resolves those against the workspace root,
// so they stay clickable while editing locally. On the published static site,
// however, a "/src/..." link resolves against the site origin and 404s.
//
// This plugin rewrites those links to absolute GitHub blob URLs so they work on
// the published site, while the Markdown sources stay untouched (keeping local
// VS Code navigation intact).
//
// It hooks markdownSetup at the Markdown token level, so it only rewrites links
// written in the docs' content. docmd's own generated links (sidebar, breadcrumbs,
// canonical tags) are never seen here, which matters because an internal doc route
// like "/tools/build-tools" is otherwise indistinguishable from a repo path like
// "/tools/BugReportTool" once rendered to HTML.
//
// docmd appends a trailing slash to the rewritten links (".../Foo.cpp/"); GitHub
// resolves that to the file anyway, so it is left as-is for simplicity.
const REPO_BLOB_BASE = 'https://github.com/microsoft/PowerToys/blob/main';
export default {
plugin: {
name: 'github-source-links',
version: '1.0.0',
capabilities: ['markdown'],
},
markdownSetup(md) {
const defaultRender =
md.renderer.rules.link_open ||
((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const token = tokens[idx];
const hrefIndex = token.attrIndex('href');
if (hrefIndex >= 0) {
const href = token.attrs[hrefIndex][1];
// Only repo-root-relative links ("/src/..."). Leave protocol-relative
// ("//host"), absolute ("https://..."), relative and anchor links alone.
if (href.length > 1 && href[0] === '/' && href[1] !== '/') {
token.attrs[hrefIndex][1] = REPO_BLOB_BASE + href;
}
}
return defaultRender(tokens, idx, options, env, self);
};
},
};

View File

@@ -0,0 +1,8 @@
{
"name": "docmd-plugin-github-source-links",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "index.js",
"description": "docmd plugin that rewrites repo-root-relative doc links to GitHub blob URLs at build time."
}

View File

@@ -0,0 +1,9 @@
{
"title": "PowerToys Dev Docs",
"src": "../devdocs",
"out": "site",
"base": "/",
"plugins": {
"docmd-plugin-github-source-links": {}
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "powertoys-devdocs-website",
"version": "1.0.0",
"private": true,
"description": "Static Dev Docs website generated from doc/devdocs with docmd.",
"scripts": {
"dev": "docmd dev",
"build": "docmd build"
},
"dependencies": {
"docmd-plugin-github-source-links": "file:./docmd-plugins/github-source-links"
},
"devDependencies": {
"@docmd/core": "0.8.6"
}
}

View File

@@ -42,4 +42,4 @@ In the PR that adds a new plugin, reference a new issue to track the work for fu
- [ ] Add the resource folder to https://github.com/microsoft/PowerToys/blob/21247c0bb09a1bee3d14d6efa53d0c247f7236af/installer/PowerToysSetup/Product.wxs#L825
- [ ] Add the resource files under the section https://github.com/microsoft/PowerToys/blob/21247c0bb09a1bee3d14d6efa53d0c247f7236af/installer/PowerToysSetup/Product.wxs#L882
- [ ] Your plugin's executable file (DLL) has to have correct version informations after building it. (This version information will be shown on the settings page.)
- [ ] Your plugin's executable file (DLL) has to have correct version information after building it. (This version information will be shown on the settings page.)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -1,19 +0,0 @@
# Backlog
This file captures the prioritized list of issues the FancyZones team will tackle
## On deck
## Backlog
Add tests to the new editor [197](https://github.com/microsoft/PowerToys/issues/197)
Cycle through windows in a Zone [175](https://github.com/microsoft/PowerToys/issues/175)
Minimize/restore windows in a zone as a group [174](https://github.com/microsoft/PowerToys/issues/174)
FancyZones should support custom layouts for different "environments" [177](https://github.com/microsoft/PowerToys/issues/177)
Win+arrow is directional based on zone rect [162](https://github.com/microsoft/PowerToys/issues/162)
Dragging a zoned window should restore size to a checkpointed size instead of current rect [166](https://github.com/microsoft/PowerToys/issues/166)
FancyZones should merge with MTND and include zone moves in the pop-up [178](https://github.com/microsoft/PowerToys/issues/178)
Drag to edge of screen automatically switches virtual desktops [168](https://github.com/microsoft/PowerToys/issues/168)
Visual updates for Win+Arrow [171](https://github.com/microsoft/PowerToys/issues/171)
Add "magnetic dragging and resizing" mode to FancyZones [181](https://github.com/microsoft/PowerToys/issues/181)
Create layout from current windows [159](https://github.com/microsoft/PowerToys/issues/159)
Zone sets that have a dynamic number of zones [160](https://github.com/microsoft/PowerToys/issues/160)

View File

@@ -1,24 +0,0 @@
# PowerToys Backlog
The list below is the set of utilities we're considering and the rough priority order of the utilities. If you have feedback on the order of the utilities, please use the issues for each one to provide that feedback. Note that new features for existing utilities (dock / undock zone layouts for FancyZones) are tracked in the backlog for each utility.
## On deck
* Maximize to new desktop widget - The MTND widget shows a pop-up button when a user hovers over the maximize / restore button on any window. Clicking it creates a new desktop, sends the app to that desktop and maximizes the app on the new desktop.
* [Process terminate tool](https://github.com/indierawk2k2/PowerToys-1/blob/master/specs/Terminate%20Spec.md)
* [Animated gif screen recorder](https://github.com/indierawk2k2/PowerToys-1/blob/master/specs/GIF%20Maker%20Spec.md)
## Backlog
Please use issues and votes to guide the project to suggest new ideas and help us prioritize the list below.
1. [Keyboard shortcut manager](https://github.com/microsoft/PowerToys/issues/6)
2. [Win+R replacement](https://github.com/microsoft/PowerToys/issues/44)
3. Resource use tool (maps between a resource like a file handle to an app and vice-versa)
4. Performance analysis over time to track which processes have been slowing down your machine
5. Better Alt+Tab including browser tab integration and search for running apps
6. [Battery tracker](https://github.com/microsoft/PowerToys/issues/7)
7. [Quick resolution swaps in taskbar](https://github.com/microsoft/PowerToys/issues/27)
8. Mouse events without focus
9. Cmd (or PS or Bash) from here
10. Contents menu file browsing

View File

@@ -1,13 +0,0 @@
# Backlog
This file captures the prioritized list of issues for the Windows key shortcut guide
## On deck
Windows key shortcut guide animation performance is choppy [198](https://github.com/microsoft/PowerToys/issues/198)
Shortcut guide strings should be localized [199](https://github.com/microsoft/PowerToys/issues/199)
## Backlog
Add Win+Shift+S to the WKSG (screenshot tool) [179](https://github.com/microsoft/PowerToys/issues/179)
Replace SVG with software-generated content. [156](https://github.com/microsoft/PowerToys/issues/156)
Shortcut sorting [154](https://github.com/microsoft/PowerToys/issues/154)
Make shortcut descriptors clickable [152](https://github.com/microsoft/PowerToys/issues/152)

View File

@@ -1,114 +0,0 @@
---
last-update: 1-18-2026
---
# PowerToys Awake Changelog
## Builds
The build ID can be found in `Core\Constants.cs` in the `BuildId` variable - it is a unique identifier for the current builds that allows better diagnostics (we can look up the build ID from the logs) and offers a way to triage Awake-specific issues faster independent of the PowerToys version. The build ID does not carry any significance beyond that within the PowerToys code base.
The build ID moniker is made up of two components - a reference to a [Halo](https://en.wikipedia.org/wiki/Halo_(franchise)) character, and the date when the work on the specific build started in the format of `MMDDYYYY`.
| Build ID | Build Date |
|:-------------------------------------------------------------------|:------------------|
| [`DIDACT_01182026`](#DIDACT_01182026-january-18-2026) | January 18, 2026 |
| [`TILLSON_11272024`](#TILLSON_11272024-november-27-2024) | November 27, 2024 |
| [`PROMETHEAN_09082024`](#PROMETHEAN_09082024-september-8-2024) | September 8, 2024 |
| [`VISEGRADRELAY_08152024`](#VISEGRADRELAY_08152024-august-15-2024) | August 15, 2024 |
| [`DAISY023_04102024`](#DAISY023_04102024-april-10-2024) | April 10, 2024 |
| [`ATRIOX_04132023`](#ATRIOX_04132023-april-13-2023) | April 13, 2023 |
| [`LIBRARIAN_03202022`](#librarian_03202022-march-20-2022) | March 20, 2022 |
| `ARBITER_01312022` | January 31, 2022 |
### `DIDACT_01182026` (January 18, 2026)
>[!NOTE]
>See pull request: [Awake - `DIDACT_01182026`](https://github.com/microsoft/PowerToys/pull/44795)
- [#32544](https://github.com/microsoft/PowerToys/issues/32544) Fixed an issue where Awake settings became non-functional after the PC wakes from sleep. Added `WM_POWERBROADCAST` handling to detect system resume events (`PBT_APMRESUMEAUTOMATIC`, `PBT_APMRESUMESUSPEND`) and re-apply `SetThreadExecutionState` to restore the awake state.
- [#36150](https://github.com/microsoft/PowerToys/issues/36150) Fixed an issue where Awake would not prevent sleep when AC power is connected. Added `PBT_APMPOWERSTATUSCHANGE` handling to re-apply `SetThreadExecutionState` when the power source changes (AC/battery transitions).
- Fixed an issue where toggling "Keep screen on" during an active timed session would disrupt the countdown timer. The display setting now updates directly without restarting the timer, preserving the exact remaining time.
- [#41918](https://github.com/microsoft/PowerToys/issues/41918) Fixed `WM_COMMAND` message processing flaw in `TrayHelper.WndProc` that incorrectly compared enum values against enum count. Added proper bounds checking for custom tray time entries.
- Investigated [#44134](https://github.com/microsoft/PowerToys/issues/44134) - documented that `ES_DISPLAY_REQUIRED` (used when "Keep display on" is enabled) blocks Task Scheduler idle detection, preventing scheduled maintenance tasks like SSD TRIM. Workaround: disable "Keep display on" or manually run `Optimize-Volume -DriveLetter C -ReTrim`. Additional investigation needed for potential "idle window" feature.
- [#41738](https://github.com/microsoft/PowerToys/issues/41738) Fixed `--display-on` CLI flag default from `true` to `false` to align with documentation and PowerToys settings behavior. This is a breaking change for scripts relying on the undocumented default.
- [#41674](https://github.com/microsoft/PowerToys/issues/41674) Fixed silent failure when `SetThreadExecutionState` fails. The monitor thread now handles the return value, logs an error, and reverts to passive mode with updated tray icon.
- [#38770](https://github.com/microsoft/PowerToys/issues/38770) Fixed tray icon failing to appear after Windows updates. Increased retry attempts and delays for icon Add operations (10 attempts, up to ~15.5 seconds total) while keeping existing fast retry behavior for Update/Delete operations.
- [#40501](https://github.com/microsoft/PowerToys/issues/40501) Fixed tray icon not disappearing when Awake is disabled. The `SetShellIcon` function was incorrectly requiring an icon for Delete operations, causing the `NIM_DELETE` message to never be sent.
- [#40659](https://github.com/microsoft/PowerToys/issues/40659) Fixed potential stack overflow crash in EXPIRABLE mode. Added early return after SaveSettings when correcting past expiration times, matching the pattern used by other mode handlers to prevent reentrant execution.
### `TILLSON_11272024` (November 27, 2024)
>[!NOTE]
>See pull request: [Awake - `TILLSON_11272024`](https://github.com/microsoft/PowerToys/pull/36049)
- [#35250](https://github.com/microsoft/PowerToys/issues/35250) Updates the icon retry policy, making sure that the icon consistently and correctly renders in the tray.
- [#35848](https://github.com/microsoft/PowerToys/issues/35848) Fixed a bug where custom tray time shortcuts for longer than 24 hours would be parsed as zero hours/zero minutes.
- [#34716](https://github.com/microsoft/PowerToys/issues/34716) Properly recover the state icon in the tray after an `explorer.exe` crash.
- Added configuration safeguards to make sure that invalid values for timed keep-awake times do not result in exceptions.
- Updated the tray initialization logic, making sure we wait for it to be properly created before setting icons.
- Expanded logging capabilities to track invoking functions.
- Added command validation logic to make sure that incorrect command line arguments display an error.
- Display state now shown in the tray tooltip.
- When timed mode is used, changing the display setting will no longer reset the timer.
### `PROMETHEAN_09082024` (September 8, 2024)
>[!NOTE]
>See pull request: [Awake - `PROMETHEAN_09082024`](https://github.com/microsoft/PowerToys/pull/34717)
- Updating the initialization logic to make sure that settings are respected for proper group policy and single-instance detection.
- [#34148](https://github.com/microsoft/PowerToys/issues/34148) Fixed a bug from the previous release that incorrectly synchronized threads for shell icon creation and initialized parent PID when it was not parented.
### `VISEGRADRELAY_08152024` (August 15, 2024)
>[!NOTE]
>See pull request: [Awake - `VISEGRADRELAY_08152024`](https://github.com/microsoft/PowerToys/pull/34316)
- [#34148](https://github.com/microsoft/PowerToys/issues/34148) Fixes the issue where the Awake icon is not displayed.
- [#17969](https://github.com/microsoft/PowerToys/issues/17969) Add the ability to bind the process target to the parent of the Awake launcher.
- PID binding now correctly ignores irrelevant parameters (e.g., expiration, interval) and only works for indefinite periods.
- Amending the native API surface to make sure that the Win32 error is set correctly.
### `DAISY023_04102024` (April 10, 2024)
>[!NOTE]
>See pull request: [Awake Update - `DAISY023_04102024`](https://github.com/microsoft/PowerToys/pull/32378)
- [#33630](https://github.com/microsoft/PowerToys/issues/33630) When in the UI and you select `0` as hours and `0` as minutes in `TIMED` awake mode, the UI becomes non-responsive whenever you try to get back to timed after it rolls back to `PASSIVE`.
- [#12714](https://github.com/microsoft/PowerToys/issues/12714) Adds the option to keep track of Awake state through tray tooltip.
- [#11996](https://github.com/microsoft/PowerToys/issues/11996) Adds custom icons support for mode changes in Awake.
- Removes the dependency on `System.Windows.Forms` and instead uses native Windows APIs to create the tray icon.
- Removes redundant/unused code that impacted application performance.
- Updates dependent packages to their latest versions (`Microsoft.Windows.CsWinRT` and `System.Reactive`).
### `ATRIOX_04132023` (April 13, 2023)
- Moves from using `Task.Run` to spin up threads to actually using a blocking queue that properly sets thread parameters on the same thread.
- Moves back to using native Windows APIs through P/Invoke instead of using a package.
- Move away from custom logging and to built-in logging that is consistent with the rest of PowerToys.
- Updates `System.CommandLine` and `System.Reactive` to the latest preview versions of the package.
### `LIBRARIAN_03202022` (March 20, 2022)
- Changed the tray context menu to be following OS conventions instead of the style offered by Windows Forms. This introduces better support for DPI scaling and theming in the future.
- Custom times in the tray can now be configured in the `settings.json` file for awake, through the `tray_times` property. The property values are representative of a `Dictionary<string, int>` and can be in the form of `"YOUR_NAME": LENGTH_IN_SECONDS`:
```json
{
"properties": {
"awake_keep_display_on": true,
"awake_mode": 2,
"awake_hours": 0,
"awake_minutes": 3,
"tray_times": {
"Custom length": 1800,
"Another custom length": 3600
}
},
"name": "Awake",
"version": "1.0"
}
```
- Proper Awake background window closure was implemented to ensure that the process collects the correct handle instead of the empty one that was previously done through `System.Diagnostics.Process.GetCurrentProcess().CloseMainWindow()`. This likely can help with the Awake process that is left hanging after PowerToys itself closes.

View File

@@ -195,10 +195,18 @@ Special sections start with an identifier enclosed between `<` and `>`. This dec
A string array of all the keys that need to be pressed. If a number is supplied, it should be read as a [KeyCode](https://learn.microsoft.com/windows/win32/inputdev/virtual-key-codes) and displayed accordingly (based on the Keyboard Layout of the user).
**Literal digit keys**:
Because a bare number is interpreted as a virtual-key code, a literal digit key must be authored using the `<N>` notation (the digit enclosed between `<` and `>`), where `N` is `0``9`. For example, `<9>` represents the literal `9` key (as in the "switch to the last tab" shortcut), not the virtual-key code `9` (which is `Tab`). The interpreter strips the brackets and displays just the digit.
This applies only to a single literal digit. A range such as `1 - 8` is a free-form label, not a key, and is supplied verbatim (the brackets would only be trimmed from the ends, so `<1> - <8>` would not render as intended).
**Special keys**:
Special keys are enclosed between `<` and `>` and correspond to a key that should be displayed in a certain way. If the interpreter of the manifest file can't understand the content, the brackets should be left out.
By convention these tokens are written as double-quoted strings in the YAML (for example `"<Enter>"` and `"<9>"`), matching the quoting used for punctuation key values. YAML treats the quoted and unquoted forms identically, so quoting is for consistency rather than a strict requirement for bracketed tokens.
|Name|Description|
|----|-----------|
|`<Office>`| Corresponds to the Office key on some Windows keyboards |

View File

@@ -1,37 +0,0 @@
# Unofficial community driven install methods
These are community driven alternative install methods to Windows Package Manager (WinGet) and GitHub. The PowerToys teams does not update or manage these install methods.
These will be listed in alphabetical order.
## Chocolatey
Download and upgrade PowerToys from [Chocolatey](https://chocolatey.org). If you have any issues when installing/upgrading the package please go to the [package page](https://chocolatey.org/packages/powertoys) and follow the [Chocolatey triage process](https://chocolatey.org/docs/package-triage-process)
To install PowerToys, run the following command from the command line / PowerShell:
```powershell
choco install powertoys
```
To upgrade PowerToys, run the following command from the command line / PowerShell:
```powershell
choco upgrade powertoys
```
## Scoop
Download and update PowerToys from [Scoop](https://scoop.sh).
To install PowerToys, run the following command from the command line / PowerShell:
```powershell
scoop install powertoys
```
To update PowerToys, run the following command from the command line / PowerShell:
```powershell
scoop update powertoys
```

View File

@@ -95,6 +95,9 @@ std::optional<fs::path> CopySelfToTempDir()
return dst_path;
}
// The installer filename read from UpdateState.json is validated by
// updating::IsSafeDownloadedInstallerFilename (common/updating/updateLifecycle.h)
// so it can be unit-tested. See ObtainInstaller below for how it's used.
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
{
using namespace updating;
@@ -107,7 +110,25 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
// so we don't need a GitHub API call (which may fail if offline).
if (state.state == UpdateState::readyToInstall)
{
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
if (!IsSafeDownloadedInstallerFilename(state.downloadedInstallerFilename))
{
Logger::error(L"Ignoring unexpected downloadedInstallerFilename from update state: {}", state.downloadedInstallerFilename);
return std::nullopt;
}
const fs::path updatesDir = get_pending_updates_path();
fs::path installer{ updatesDir / state.downloadedInstallerFilename };
// Make sure the resolved path actually stays within the Updates directory.
std::error_code ec;
const fs::path normalizedInstaller = fs::weakly_canonical(installer, ec);
const fs::path normalizedUpdatesDir = fs::weakly_canonical(updatesDir, ec);
if (ec || normalizedInstaller.parent_path() != normalizedUpdatesDir)
{
Logger::error(L"Resolved installer path is outside the updates directory: {}", installer.native());
return std::nullopt;
}
if (fs::is_regular_file(installer))
{
return std::move(installer);

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
</packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
</packages>

View File

@@ -73,8 +73,7 @@
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<WarningLevel>Level4</WarningLevel>
<AdditionalOptions>%(AdditionalOptions) /bigobj</AdditionalOptions>
<!-- TODO: _SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS: suppress VS 2026 STL hard error for <experimental/coroutine> until the code is ported to <coroutine> -->
<PreprocessorDefinitions>_WINRT_DLL;WINRT_LEAN_AND_MEAN;_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>_WINRT_DLL;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>../../..;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalUsingDirectories>$(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories)</AdditionalUsingDirectories>
</ClCompile>
@@ -162,7 +161,7 @@
<ClCompile>
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">stdcpp17</LanguageStandard>
<LanguageStandard>stdcpp20</LanguageStandard>
</ClCompile>
<Link>
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT

View File

@@ -0,0 +1,139 @@
// 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 Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
namespace Microsoft.PowerToys.Common.UI.Controls.Backdrops;
/// <summary>
/// A <see cref="SystemBackdrop"/> that renders desktop acrylic and stays in
/// the active visual state even when the hosting window is not activated.
/// </summary>
/// <remarks>
/// The built-in <see cref="DesktopAcrylicBackdrop"/> tracks the host window's
/// <c>IsInputActive</c> state and falls back to a solid color whenever the
/// window is not the foreground window. That makes it unusable for transient,
/// non-activating surfaces such as toasts or popups created with
/// <c>SW_SHOWNA</c> / <c>WS_EX_TRANSPARENT</c>, where the window is never
/// activated by design.
///
/// This backdrop drives a <see cref="DesktopAcrylicController"/> with a
/// <see cref="SystemBackdropConfiguration"/> whose <c>IsInputActive</c> is
/// permanently <see langword="true"/>, so the native acrylic effect is always
/// rendered.
/// </remarks>
public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
{
/// <summary>
/// Identifies the <see cref="Kind"/> dependency property.
/// </summary>
public static readonly DependencyProperty KindProperty = DependencyProperty.Register(
nameof(Kind),
typeof(DesktopAcrylicKind),
typeof(AlwaysActiveDesktopAcrylicBackdrop),
new PropertyMetadata(DesktopAcrylicKind.Default, OnKindChanged));
private readonly Dictionary<ICompositionSupportsSystemBackdrop, BackdropTarget> _targets = new();
/// <summary>
/// Gets or sets the desktop acrylic material variant to render. Defaults to
/// <see cref="DesktopAcrylicKind.Default"/> (the standard, more opaque
/// acrylic); <see cref="DesktopAcrylicKind.Thin"/> renders a lighter, more
/// translucent material and <see cref="DesktopAcrylicKind.Base"/> the base
/// material. Changing this updates any live backdrop targets immediately.
/// </summary>
public DesktopAcrylicKind Kind
{
get => (DesktopAcrylicKind)GetValue(KindProperty);
set => SetValue(KindProperty, value);
}
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
{
base.OnTargetConnected(connectedTarget, xamlRoot);
var configuration = new SystemBackdropConfiguration
{
IsInputActive = true,
Theme = ResolveTheme(xamlRoot),
};
var controller = new DesktopAcrylicController
{
Kind = Kind,
};
controller.SetSystemBackdropConfiguration(configuration);
controller.AddSystemBackdropTarget(connectedTarget);
var target = new BackdropTarget(controller, configuration, xamlRoot);
_targets[connectedTarget] = target;
if (xamlRoot.Content is FrameworkElement rootElement)
{
rootElement.ActualThemeChanged += target.OnActualThemeChanged;
}
}
protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget)
{
base.OnTargetDisconnected(disconnectedTarget);
if (_targets.Remove(disconnectedTarget, out var target))
{
if (target.XamlRoot.Content is FrameworkElement rootElement)
{
rootElement.ActualThemeChanged -= target.OnActualThemeChanged;
}
target.Controller.RemoveSystemBackdropTarget(disconnectedTarget);
target.Controller.Dispose();
}
}
private static void OnKindChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = (AlwaysActiveDesktopAcrylicBackdrop)d;
var kind = (DesktopAcrylicKind)e.NewValue;
foreach (var target in self._targets.Values)
{
target.Controller.Kind = kind;
}
}
private static SystemBackdropTheme ResolveTheme(XamlRoot xamlRoot) =>
xamlRoot.Content is FrameworkElement rootElement
? rootElement.ActualTheme switch
{
ElementTheme.Dark => SystemBackdropTheme.Dark,
ElementTheme.Light => SystemBackdropTheme.Light,
_ => SystemBackdropTheme.Default,
}
: SystemBackdropTheme.Default;
private sealed class BackdropTarget
{
public BackdropTarget(DesktopAcrylicController controller, SystemBackdropConfiguration configuration, XamlRoot xamlRoot)
{
Controller = controller;
Configuration = configuration;
XamlRoot = xamlRoot;
}
public DesktopAcrylicController Controller { get; }
public SystemBackdropConfiguration Configuration { get; }
public XamlRoot XamlRoot { get; }
public void OnActualThemeChanged(FrameworkElement sender, object args)
{
Configuration.Theme = ResolveTheme(XamlRoot);
}
}
}

View File

@@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:backdrops="using:Microsoft.PowerToys.Common.UI.Controls.Backdrops"
xmlns:local="using:Microsoft.PowerToys.Common.UI.Controls">
<Style BasedOn="{StaticResource DefaultTransientSurfaceStyle}" TargetType="local:TransientSurface" />
<Style x:Key="DefaultTransientSurfaceStyle" TargetType="local:TransientSurface">
<Setter Property="BorderBrush" Value="{ThemeResource SurfaceStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TransientSurface">
<Grid
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Translation="0,0,24">
<Grid.Shadow>
<ThemeShadow />
</Grid.Shadow>
<SystemBackdropElement CornerRadius="{TemplateBinding CornerRadius}">
<SystemBackdropElement.SystemBackdrop>
<backdrops:AlwaysActiveDesktopAcrylicBackdrop Kind="{TemplateBinding AcrylicKind}" />
</SystemBackdropElement.SystemBackdrop>
</SystemBackdropElement>
<ContentPresenter
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,467 @@
// 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 CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Animations;
using Microsoft.PowerToys.Common.UI.Controls.Window;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
namespace Microsoft.PowerToys.Common.UI.Controls;
/// <summary>
/// A floating, self-animating "pseudo window" surface for transient PowerToys
/// overlays (toasts, banners, indicators). It looks like a control but behaves
/// like a lightweight window: it provides the PowerToys-standard chrome — 1 px
/// border in <c>SurfaceStrokeColorDefaultBrush</c>, 8 px corner radius, a
/// <c>ThemeShadow</c>, and an always-active desktop acrylic backdrop — and owns
/// its own show/hide animations.
/// </summary>
/// <remarks>
/// <para>Designed to be declared as the root content of a
/// <see cref="TransparentWindow"/>, which stays animation-agnostic. Call
/// <see cref="SubscribeTo"/> once (e.g. from the hosting window's constructor)
/// to wire this surface to the window's <see cref="TransparentWindow.Showing"/> /
/// <see cref="TransparentWindow.Hiding"/> events. From then on the surface
/// animates itself in/out whenever the window is shown or hidden, and uses the
/// <see cref="TransparentWindow.Hiding"/> deferral to keep the window visible
/// until its out-animation finishes.</para>
/// <para>The show transition comes from the window's
/// <see cref="TransparentWindow.Show(Transition)"/> call (or from
/// <see cref="ShowTransition"/> when shown without one); the hide transition
/// always comes from <see cref="HideTransition"/>. Animations target the
/// surface itself, so the entire surface (border, acrylic, shadow, inner
/// content) animates as one. Apps that want a different look supply their own
/// <c>Style TargetType="TransientSurface"</c> in resources — the standard WinUI
/// restyle path.</para>
/// </remarks>
public sealed partial class TransientSurface : ContentControl
{
private const float ShadowDepth = 32f;
private const double SlideInOffset = 24;
private const double SlideOutOffset = 12;
// "Pop" transition: scale between 96% and 100% (a subtle 4% grow). Following
// Fluent motion guidance the scale uses a decelerate (EaseOut) curve; the
// fade is kept fast so the surface reads as an instant, light pop.
//
// The fade must run at least as long as the scale: if the scale outlasted the
// fade, the surface would reach full opacity while still visibly growing,
// which reads as a "resize" rather than a pop. Keeping the fade >= the scale
// hides the growth under the opacity ramp, so by the time it is fully opaque
// it is already at 100% size.
private const float PopScaleFrom = 0.96f;
private const double PopFadeShowMs = 180;
private const double PopScaleShowMs = 150;
private const double PopFadeHideMs = 120;
private const double PopScaleHideMs = 120;
public static readonly DependencyProperty ShowTransitionProperty = DependencyProperty.Register(
nameof(ShowTransition),
typeof(Transition),
typeof(TransientSurface),
new PropertyMetadata(Transition.None, OnTransitionChanged));
public static readonly DependencyProperty HideTransitionProperty = DependencyProperty.Register(
nameof(HideTransition),
typeof(Transition),
typeof(TransientSurface),
new PropertyMetadata(Transition.None, OnTransitionChanged));
public static readonly DependencyProperty AcrylicKindProperty = DependencyProperty.Register(
nameof(AcrylicKind),
typeof(DesktopAcrylicKind),
typeof(TransientSurface),
new PropertyMetadata(DesktopAcrylicKind.Thin));
private readonly DispatcherQueueTimer _hideCompletedTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private readonly ImplicitAnimationSet _noAnimations = new();
private ImplicitAnimationSet _showAnimations = new();
private ImplicitAnimationSet _hideAnimations = new();
private bool _hasCustomShowAnimations;
private bool _hasCustomHideAnimations;
private Action? _abandonPendingHide;
public TransientSurface()
{
DefaultStyleKey = typeof(TransientSurface);
RebuildDefaultAnimations();
// Pin the scale center to the surface's center so the "Pop" transition
// grows/shrinks from the middle, not the top-left corner. An expression
// animation bound to the visual's own size keeps the center correct from
// the very first frame (a SizeChanged handler would race the show
// animation and let the first pop scale from 0,0).
PinScaleCenter();
// Start hidden so the first Show() animates in from the configured pose.
Visibility = Visibility.Collapsed;
}
/// <summary>
/// Raised after <see cref="Hide"/> once the longest animation in
/// <see cref="HideAnimations"/> (delay + duration) has completed.
/// </summary>
public event EventHandler? HideCompleted;
/// <summary>
/// Gets or sets the transition played when the surface is shown without an
/// explicit one (see <see cref="Show()"/>). Defaults to
/// <see cref="Transition.None"/>, which plays no animation at all (the
/// surface appears instantly); a directional value adds a fade plus a slide
/// in from that edge, and <see cref="Transition.Pop"/> a fade plus a subtle
/// scale-up. Changing this regenerates the default <see cref="ShowAnimations"/>
/// unless it has been set explicitly.
/// </summary>
public Transition ShowTransition
{
get => (Transition)GetValue(ShowTransitionProperty);
set => SetValue(ShowTransitionProperty, value);
}
/// <summary>
/// Gets or sets the transition played when the surface is hidden (see
/// <see cref="Hide"/>). Defaults to <see cref="Transition.None"/>, which
/// plays no animation at all (the surface disappears instantly); a
/// directional value adds a fade plus a slide out toward that edge, and
/// <see cref="Transition.Pop"/> a fade plus a subtle scale-down. Changing
/// this regenerates the default <see cref="HideAnimations"/> unless it has
/// been set explicitly.
/// </summary>
public Transition HideTransition
{
get => (Transition)GetValue(HideTransitionProperty);
set => SetValue(HideTransitionProperty, value);
}
/// <summary>
/// Gets or sets the desktop acrylic material variant painted behind the
/// surface. Defaults to <see cref="DesktopAcrylicKind.Thin"/> (a lighter,
/// more translucent material); set <see cref="DesktopAcrylicKind.Default"/>
/// for the standard, more opaque acrylic or <see cref="DesktopAcrylicKind.Base"/>
/// for the base material. Has no effect when a custom template without the
/// default acrylic backdrop is applied.
/// </summary>
public DesktopAcrylicKind AcrylicKind
{
get => (DesktopAcrylicKind)GetValue(AcrylicKindProperty);
set => SetValue(AcrylicKindProperty, value);
}
/// <summary>
/// Gets or sets the animations played when <see cref="Show()"/> flips the
/// surface to <see cref="Visibility.Visible"/>. Defaults to the animation
/// derived from <see cref="ShowTransition"/>. Assigning a value marks the set
/// as custom so <see cref="ShowTransition"/> no longer overwrites it.
/// </summary>
public ImplicitAnimationSet ShowAnimations
{
get => _showAnimations;
set
{
_showAnimations = value ?? new ImplicitAnimationSet();
_hasCustomShowAnimations = true;
}
}
/// <summary>
/// Gets or sets the animations played when <see cref="Hide"/> flips the
/// surface to <see cref="Visibility.Collapsed"/>. Defaults to the animation
/// derived from <see cref="HideTransition"/>. Assigning a value marks the set
/// as custom so <see cref="HideTransition"/> no longer overwrites it.
/// </summary>
public ImplicitAnimationSet HideAnimations
{
get => _hideAnimations;
set
{
_hideAnimations = value ?? new ImplicitAnimationSet();
_hasCustomHideAnimations = true;
}
}
/// <summary>
/// Wires this surface to a hosting <see cref="TransparentWindow"/> so it
/// animates itself in and out in response to the window's
/// <see cref="TransparentWindow.Showing"/> / <see cref="TransparentWindow.Hiding"/>
/// events. Call this once after the surface has been set as (or placed within)
/// the window's content.
/// </summary>
/// <param name="host">The window whose show/hide transitions drive this surface.</param>
public void SubscribeTo(TransparentWindow host)
{
ArgumentNullException.ThrowIfNull(host);
host.Showing += OnHostShowing;
host.Hiding += OnHostHiding;
}
/// <summary>
/// Resets the surface to its hidden pose and flips it to
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays,
/// using <paramref name="transition"/> as the show transition.
/// </summary>
/// <param name="transition">The transition to play when showing.</param>
public void Show(Transition transition)
{
ShowTransition = transition;
Show();
}
/// <summary>
/// Resets the surface to its hidden pose and flips it to
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays.
/// Repeated calls re-trigger the show animation cleanly and cancel any
/// pending <see cref="HideCompleted"/> notification.
/// </summary>
public void Show()
{
_hideCompletedTimer.Stop();
// If a hide from a previous cycle is still in flight, abandon it: drop its
// pending HideCompleted handler so the outstanding deferral is never
// completed. We are showing again, so the host must keep the window
// visible instead of later hiding it for this interrupted cycle.
_abandonPendingHide?.Invoke();
_abandonPendingHide = null;
// Attach the show animation and detach any hide animation: when Show() is
// called while the surface is still visible, the Collapsed -> Visible
// restart below would otherwise play the hide animation (a fade/scale out)
// immediately before the show, producing a visible flash. The real hide
// animation is re-attached just-in-time in Hide().
Implicit.SetShowAnimations(this, _showAnimations);
Implicit.SetHideAnimations(this, _noAnimations);
// Reset to the hidden pose so the show animation always animates from the
// configured starting frame.
Visibility = Visibility.Collapsed;
Visibility = Visibility.Visible;
}
/// <summary>
/// Flips the surface to <see cref="Visibility.Collapsed"/> so
/// <see cref="HideAnimations"/> plays, then raises <see cref="HideCompleted"/>
/// once the longest animation in <see cref="HideAnimations"/> (delay +
/// duration) has completed.
/// </summary>
public void Hide()
{
// Attach the hide animation just before collapsing (Show() detaches it to
// avoid a flash when re-showing an already-visible surface).
Implicit.SetHideAnimations(this, _hideAnimations);
Visibility = Visibility.Collapsed;
_hideCompletedTimer.Debounce(
() => HideCompleted?.Invoke(this, EventArgs.Empty),
interval: GetAnimationSetTotalDuration(_hideAnimations),
immediate: false);
}
private static void OnTransitionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((TransientSurface)d).RebuildDefaultAnimations();
}
private static TimeSpan GetAnimationSetTotalDuration(ImplicitAnimationSet set)
{
TimeSpan longest = TimeSpan.Zero;
foreach (var animation in set)
{
if (animation is Animation anim)
{
var total = (anim.Delay ?? TimeSpan.Zero) + (anim.Duration ?? TimeSpan.Zero);
if (total > longest)
{
longest = total;
}
}
}
return longest;
}
private static (string? ShowFrom, string? HideTo) GetSlideOffsets(Transition transition) => transition switch
{
Transition.Bottom => ($"0,{SlideInOffset},{ShadowDepth}", $"0,{SlideOutOffset},{ShadowDepth}"),
Transition.Top => ($"0,{-SlideInOffset},{ShadowDepth}", $"0,{-SlideOutOffset},{ShadowDepth}"),
Transition.Left => ($"{-SlideInOffset},0,{ShadowDepth}", $"{-SlideOutOffset},0,{ShadowDepth}"),
Transition.Right => ($"{SlideInOffset},0,{ShadowDepth}", $"{SlideOutOffset},0,{ShadowDepth}"),
_ => (null, null),
};
private void OnHostShowing(TransparentWindow sender, ShowingEventArgs e)
{
if (e.Transition is Transition transition)
{
Show(transition);
}
else
{
Show();
}
}
private void OnHostHiding(TransparentWindow sender, HidingEventArgs e)
{
// Take a deferral so the host keeps its window visible until our
// out-animation has finished, then complete it from HideCompleted.
var deferral = e.GetDeferral();
void OnHideCompleted(object? s, EventArgs args)
{
HideCompleted -= OnHideCompleted;
_abandonPendingHide = null;
deferral.Complete();
}
// Let a subsequent Show() cancel this hide cleanly: unsubscribe the
// handler so the deferral is never completed (the window stays visible)
// rather than firing AppWindow.Hide for an interrupted cycle.
_abandonPendingHide = () => HideCompleted -= OnHideCompleted;
HideCompleted += OnHideCompleted;
Hide();
}
private void RebuildDefaultAnimations()
{
if (!_hasCustomShowAnimations)
{
_showAnimations = BuildShowAnimations(ShowTransition);
}
if (!_hasCustomHideAnimations)
{
_hideAnimations = BuildHideAnimations(HideTransition);
}
}
private void PinScaleCenter()
{
var visual = ElementCompositionPreview.GetElementVisual(this);
var center = visual.Compositor.CreateExpressionAnimation("Vector3(this.Target.Size.X * 0.5, this.Target.Size.Y * 0.5, 0)");
visual.StartAnimation("CenterPoint", center);
}
private static ImplicitAnimationSet BuildShowAnimations(Transition transition)
{
var animations = new ImplicitAnimationSet();
if (transition == Transition.None)
{
// No animation at all.
return animations;
}
if (transition == Transition.Pop)
{
animations.Add(new OpacityAnimation
{
From = 0,
To = 1.0,
Duration = TimeSpan.FromMilliseconds(PopFadeShowMs),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
EasingType = EasingType.Cubic,
});
animations.Add(new ScaleAnimation
{
From = $"{PopScaleFrom},{PopScaleFrom},1",
To = "1,1,1",
Duration = TimeSpan.FromMilliseconds(PopScaleShowMs),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
EasingType = EasingType.Cubic,
});
return animations;
}
var (slideFrom, _) = GetSlideOffsets(transition);
animations.Add(new OpacityAnimation
{
From = 0,
To = 1.0,
Duration = TimeSpan.FromMilliseconds(200),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
EasingType = EasingType.Cubic,
});
animations.Add(new TranslationAnimation
{
From = slideFrom,
To = $"0,0,{ShadowDepth}",
Duration = TimeSpan.FromMilliseconds(250),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
EasingType = EasingType.Cubic,
});
return animations;
}
private static ImplicitAnimationSet BuildHideAnimations(Transition transition)
{
var animations = new ImplicitAnimationSet();
if (transition == Transition.None)
{
// No animation at all.
return animations;
}
if (transition == Transition.Pop)
{
animations.Add(new OpacityAnimation
{
From = 1.0,
To = 0,
Duration = TimeSpan.FromMilliseconds(PopFadeHideMs),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
EasingType = EasingType.Cubic,
});
animations.Add(new ScaleAnimation
{
From = "1,1,1",
To = $"{PopScaleFrom},{PopScaleFrom},1",
Duration = TimeSpan.FromMilliseconds(PopScaleHideMs),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
EasingType = EasingType.Cubic,
});
return animations;
}
var (_, slideTo) = GetSlideOffsets(transition);
animations.Add(new OpacityAnimation
{
From = 1.0,
To = 0,
Duration = TimeSpan.FromMilliseconds(180),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
EasingType = EasingType.Cubic,
});
animations.Add(new TranslationAnimation
{
From = $"0,0,{ShadowDepth}",
To = slideTo,
Duration = TimeSpan.FromMilliseconds(180),
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
EasingType = EasingType.Cubic,
});
return animations;
}
}

View File

@@ -0,0 +1,35 @@
// 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.PowerToys.Common.UI.Controls;
/// <summary>
/// A show or hide transition a surface (e.g. <see cref="TransientSurface"/>)
/// plays when it is shown or hidden. The directional values describe an edge —
/// interpreted as <em>in from</em> that edge on show and <em>out toward</em> it
/// on hide — while <see cref="None"/> and <see cref="Pop"/> are non-directional.
/// </summary>
public enum Transition
{
/// <summary>No animation; the surface appears and disappears instantly.</summary>
None,
/// <summary>Slide from the left edge (in from on show, out toward on hide).</summary>
Left,
/// <summary>Slide from the top edge (in from on show, out toward on hide).</summary>
Top,
/// <summary>Slide from the right edge (in from on show, out toward on hide).</summary>
Right,
/// <summary>Slide from the bottom edge (in from on show, out toward on hide).</summary>
Bottom,
/// <summary>
/// A subtle "pop": a quick fade combined with a small scale between 96% and
/// 100% from the surface's center. Stays in place — no slide.
/// </summary>
Pop,
}

View File

@@ -4,5 +4,6 @@
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/TransientSurface/TransientSurface.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -9,7 +9,7 @@ using Microsoft.UI.Windowing;
using Windows.Graphics;
using WinUIEx;
namespace Microsoft.PowerToys.Common.UI.Controls.Flyout;
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
/// <summary>
/// Shared helper for positioning and sizing flyout-style WinUI 3 windows
@@ -187,16 +187,13 @@ public static partial class FlyoutWindowHelper
}
/// <summary>
/// Two-step move that avoids WM_DPICHANGED double-scaling. First teleports a 1×1
/// window into the target display (which may trigger an auto-rescale, but on a 1×1
/// rect the effect is invisible). Then sets the real position+size while the window
/// is already on the target monitor — no DPI boundary crossing, so WinUI's auto
/// handler doesn't fire and overwrite our computed rect.
///
/// Skips the teleport when the window is already on the target display, since there
/// is no boundary to cross.
/// Move and resize <paramref name="window"/> to <paramref name="finalRect"/> (absolute
/// screen physical-pixel coordinates) on <paramref name="targetDisplay"/>. Performs a
/// two-step move that avoids WM_DPICHANGED double-scaling: first a 1×1 teleport into the
/// target display (invisible at that size), then the real position+size while the window
/// is already on that monitor. Skips the teleport when already on the target display.
/// </summary>
private static void MoveAndResizeOnDisplay(WindowEx window, DisplayArea targetDisplay, RectInt32 finalRect)
public static void MoveAndResizeOnDisplay(WindowEx window, DisplayArea targetDisplay, RectInt32 finalRect)
{
var currentDisplay = DisplayArea.GetFromWindowId(window.AppWindow.Id, DisplayAreaFallback.Nearest);
bool needsTeleport = currentDisplay is null || currentDisplay.DisplayId.Value != targetDisplay.DisplayId.Value;

View File

@@ -0,0 +1,61 @@
// 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.Threading;
using Deferral = global::Windows.Foundation.Deferral;
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
/// <summary>
/// Data for <see cref="TransparentWindow.Hiding"/>. Supports deferrals so an
/// animated surface can keep the window visible until its out-animation has
/// finished. If no handler takes a deferral, the window hides immediately.
/// </summary>
public sealed class HidingEventArgs : EventArgs
{
private int _outstanding;
private bool _raised;
private Action? _continuation;
/// <summary>
/// Requests that the window stay visible until the returned deferral is
/// completed. Call <see cref="Deferral.Complete"/> once the out-animation
/// has finished.
/// </summary>
/// <returns>A deferral that must be completed to allow the window to hide.</returns>
public Deferral GetDeferral()
{
Interlocked.Increment(ref _outstanding);
return new Deferral(OnDeferralCompleted);
}
/// <summary>
/// Called by the window after raising the event to register what should run
/// once every outstanding deferral has completed (or immediately if none
/// were taken).
/// </summary>
internal void RunWhenComplete(Action continuation)
{
_continuation = continuation;
_raised = true;
TryComplete();
}
private void OnDeferralCompleted()
{
Interlocked.Decrement(ref _outstanding);
TryComplete();
}
private void TryComplete()
{
if (_raised && Volatile.Read(ref _outstanding) == 0)
{
var continuation = _continuation;
_continuation = null;
continuation?.Invoke();
}
}
}

View File

@@ -0,0 +1,26 @@
// 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;
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
/// <summary>
/// Data for <see cref="TransparentWindow.Showing"/>. Carries the transition the
/// content should play, or <see langword="null"/> to let the content use its own
/// configured show transition.
/// </summary>
public sealed class ShowingEventArgs : EventArgs
{
public ShowingEventArgs(Transition? transition)
{
Transition = transition;
}
/// <summary>
/// Gets the transition the content should play, or <see langword="null"/> to
/// use the content's own configured show transition.
/// </summary>
public Transition? Transition { get; }
}

View File

@@ -0,0 +1,164 @@
// 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.Runtime.InteropServices;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Windowing;
using Windows.Foundation;
using WinUIEx;
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
/// <summary>
/// Reusable transparent host window for transient overlays
/// (toasts, banners, indicators) that should not steal foreground.
/// </summary>
/// <remarks>
/// <para>The constructor applies all of the boilerplate that PowerToys overlays
/// currently hand-roll:</para>
/// <list type="bullet">
/// <item>Strip the native frame and caption (<c>WS_THICKFRAME</c> etc.).</item>
/// <item>Disable the Win11 1-pixel DWM border and corner rounding.</item>
/// <item>Mark the window as a tool window so it stays out of the taskbar and Alt-Tab.</item>
/// <item>Extend content into the title bar and collapse the title bar.</item>
/// <item>Apply a <see cref="TransparentTintBackdrop"/> so the HWND is fully
/// see-through and the visible chrome can be drawn by the content.</item>
/// </list>
/// <para>This window is intentionally animation-agnostic: it does not own any
/// chrome or motion. Consumers supply their own content (typically a
/// <see cref="TransientSurface"/>) which draws the acrylic, border, corners and
/// shadow, and animates itself. <see cref="Show()"/> and <see cref="Hide"/>
/// coordinate <c>SW_SHOWNA</c> (no-activate) with the
/// <see cref="Showing"/> / <see cref="Hiding"/> events: a content surface
/// subscribes to those (e.g. via <see cref="TransientSurface.SubscribeTo"/>)
/// and plays its in/out animation. The <see cref="Hiding"/> event supports
/// deferrals, so the underlying
/// <see cref="Microsoft.UI.Windowing.AppWindow.Hide"/> is delayed until the
/// content has finished animating out. With no listener the window simply shows
/// or hides immediately.</para>
/// </remarks>
public partial class TransparentWindow : WinUIEx.WindowEx
{
private const uint DwmwaColorNone = 0xFFFFFFFE;
private const int DwmwaWindowCornerPreference = 33;
private const int DwmwaBorderColor = 34;
private const int DwmwcpDoNotRound = 1;
private const int GwlExStyle = -20;
private const int WsExToolWindow = 0x00000080;
private const int SwShowNa = 8;
private readonly nint _hwnd;
public TransparentWindow()
{
AppWindow.Hide();
ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
unsafe
{
uint borderColor = DwmwaColorNone;
_ = DwmSetWindowAttribute(_hwnd, DwmwaBorderColor, &borderColor, sizeof(uint));
int cornerPref = DwmwcpDoNotRound;
_ = DwmSetWindowAttribute(_hwnd, DwmwaWindowCornerPreference, &cornerPref, sizeof(int));
}
ApplyExStyleBit(WsExToolWindow, true);
SystemBackdrop = new TransparentTintBackdrop();
}
/// <summary>
/// Raised (without activation) when <see cref="Show()"/> makes the window
/// visible. A content surface subscribes to this to play its in-animation,
/// using <see cref="ShowingEventArgs.Transition"/>.
/// </summary>
public event TypedEventHandler<TransparentWindow, ShowingEventArgs>? Showing;
/// <summary>
/// Raised when <see cref="Hide"/> begins dismissing the window. A content
/// surface subscribes to this to play its out-animation, taking a deferral
/// (<see cref="HidingEventArgs.GetDeferral"/>) so the underlying window stays
/// visible until the animation completes.
/// </summary>
public event TypedEventHandler<TransparentWindow, HidingEventArgs>? Hiding;
/// <summary>
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
/// <see cref="Showing"/> without a transition, so subscribed content animates
/// in using its own configured show transition.
/// </summary>
public void Show() => RaiseShow(null);
/// <summary>
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
/// <see cref="Showing"/> so subscribed content animates in using
/// <paramref name="transition"/>, overriding its configured show transition.
/// </summary>
/// <param name="transition">The transition the content should play.</param>
public void Show(Transition transition) => RaiseShow(transition);
private void RaiseShow(Transition? transition)
{
DispatcherQueue.TryEnqueue(
DispatcherQueuePriority.Low,
() =>
{
_ = ShowWindow(_hwnd, SwShowNa);
Showing?.Invoke(this, new ShowingEventArgs(transition));
});
}
/// <summary>
/// Raises <see cref="Hiding"/> so subscribed content animates out, then hides
/// the underlying <see cref="Microsoft.UI.Windowing.AppWindow"/> once every
/// deferral taken by a handler has completed (immediately if none were taken).
/// </summary>
public void Hide()
{
DispatcherQueue.TryEnqueue(
DispatcherQueuePriority.Low,
() =>
{
var args = new HidingEventArgs();
Hiding?.Invoke(this, args);
args.RunWhenComplete(AppWindow.Hide);
});
}
private void ApplyExStyleBit(int bit, bool set)
{
if (_hwnd == 0)
{
return;
}
nint exStyle = GetWindowLongPtr(_hwnd, GwlExStyle);
nint updated = set ? exStyle | bit : exStyle & ~(nint)bit;
if (updated != exStyle)
{
_ = SetWindowLongPtr(_hwnd, GwlExStyle, updated);
}
}
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
[LibraryImport("dwmapi.dll")]
private static unsafe partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, void* pvAttribute, int cbAttribute);
}

View File

@@ -58,7 +58,7 @@ namespace Microsoft.Interop.Tests
ClientPipe.Start();
// Test can be flaky as the pipes are still being set up and we end up receiving no message. Wait for a bit to avoid that.
Thread.Sleep(100);
Thread.Sleep(500);
ClientPipe.Send(testString);

View File

@@ -676,4 +676,61 @@ namespace UpdatingUnitTests
LocalFree(argv);
}
};
// Tests for IsSafeDownloadedInstallerFilename: the updater reads
// downloadedInstallerFilename from the persisted UpdateState.json and must only
// accept a plain filename so it never looks outside the Updates folder when the
// cached state is stale, corrupted, or otherwise unexpected.
TEST_CLASS(IsSafeDownloadedInstallerFilenameTests)
{
public:
// Normal installer asset names are bare filenames and must be accepted,
// so the regular update flow does not regress.
TEST_METHOD(NormalInstallerFilenamesAreAccepted)
{
Assert::IsTrue(updating::IsSafeDownloadedInstallerFilename(L"PowerToysSetup-0.95.0-x64.exe"));
Assert::IsTrue(updating::IsSafeDownloadedInstallerFilename(L"PowerToysUserSetup-0.95.0-arm64.exe"));
Assert::IsTrue(updating::IsSafeDownloadedInstallerFilename(L"PowerToysSetup-1.0.0-x64.msi"));
}
// Empty values must be rejected (no installer to run).
TEST_METHOD(EmptyFilenameIsRejected)
{
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L""));
}
// Relative parent-directory components must be rejected.
TEST_METHOD(ParentDirectoryComponentsAreRejected)
{
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"..\\..\\setup.msi"));
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"../../setup.exe"));
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L".."));
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"."));
}
// Any directory component (even without "..") must be rejected — the value
// must be a single bare filename.
TEST_METHOD(NestedPathComponentsAreRejected)
{
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"sub\\setup.exe"));
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"sub/setup.exe"));
}
// Absolute paths, drive-relative paths and UNC paths must be rejected,
// because fs::path's operator/ would let them replace the Updates directory.
TEST_METHOD(AbsoluteAndUncPathsAreRejected)
{
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"C:\\setup.msi"));
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"C:setup.msi"));
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"\\setup.msi"));
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"\\\\server\\share\\setup.exe"));
}
// A bare filename that merely contains a ".." substring is rejected as a
// conservative measure (real asset names never contain "..").
TEST_METHOD(EmbeddedDotDotSubstringIsRejected)
{
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"setup..name.exe"));
}
};
}

View File

@@ -44,4 +44,43 @@ namespace updating
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
return argCount >= 4;
}
// Returns true when the value read from UpdateState.json is a plain installer
// filename that can be combined with the pending-updates directory. UpdateState.json
// is persisted state that may be stale, corrupted, or otherwise unexpected, so the
// cached filename could contain path separators or an absolute/drive-relative path.
// Only a single bare filename (the form produced by the download step) is accepted;
// anything else is rejected so the updater never looks outside the Updates folder.
inline bool IsSafeDownloadedInstallerFilename(const std::wstring& filename)
{
if (filename.empty())
{
return false;
}
// Reject any path separators or parent-directory tokens outright. Installer
// asset filenames never contain these.
if (filename.find(L'/') != std::wstring::npos ||
filename.find(L'\\') != std::wstring::npos ||
filename.find(L"..") != std::wstring::npos)
{
return false;
}
const fs::path candidate{ filename };
// Must be a single path component: no drive/root and no directory portion.
if (candidate.has_root_name() || candidate.has_root_directory() || candidate.has_parent_path())
{
return false;
}
const auto name = candidate.filename().wstring();
if (name != filename || name == L"." || name == L"..")
{
return false;
}
return true;
}
}

View File

@@ -261,7 +261,7 @@ namespace powertoys_gpo
}
}
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
// No list exists for machine and user, or no value was found in the list, or an error occurred while reading the value.
return std::nullopt;
}

View File

@@ -47,6 +47,8 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
public bool ShowCustomPreview => false;
public bool ShowAIPaste => true;
public bool CloseAfterLosingFocus => false;
public bool EnableClipboardPreview => true;

View File

@@ -251,7 +251,8 @@
Margin="20,0,20,0"
x:FieldModifier="public"
IsEnabled="{x:Bind ViewModel.IsCustomAIServiceEnabled, Mode=OneWay}"
TabIndex="0">
TabIndex="0"
Visibility="{x:Bind ViewModel.ShowAIPasteSection, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
<controls:PromptBox.Footer>
<StackPanel Orientation="Horizontal">
<TextBlock

View File

@@ -17,6 +17,8 @@ namespace AdvancedPaste.Settings
public bool ShowCustomPreview { get; }
public bool ShowAIPaste { get; }
public bool CloseAfterLosingFocus { get; }
public bool EnableClipboardPreview { get; }

View File

@@ -38,6 +38,8 @@ namespace AdvancedPaste.Settings
public bool ShowCustomPreview { get; private set; }
public bool ShowAIPaste { get; private set; }
public bool CloseAfterLosingFocus { get; private set; }
public bool EnableClipboardPreview { get; private set; }
@@ -54,6 +56,7 @@ namespace AdvancedPaste.Settings
IsAIEnabled = false;
ShowCustomPreview = true;
ShowAIPaste = true;
CloseAfterLosingFocus = false;
EnableClipboardPreview = true;
PasteAIConfiguration = new PasteAIConfiguration();
@@ -109,6 +112,7 @@ namespace AdvancedPaste.Settings
IsAIEnabled = properties.IsAIEnabled;
ShowCustomPreview = properties.ShowCustomPreview;
ShowAIPaste = properties.ShowAIPaste;
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
EnableClipboardPreview = properties.EnableClipboardPreview;
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();

View File

@@ -234,6 +234,8 @@ namespace AdvancedPaste.ViewModels
public bool ShowClipboardHistoryButton => ClipboardHistoryEnabled;
public bool ShowAIPasteSection => _userSettings.ShowAIPaste && IsAllowedByGPO;
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
private PasteFormats CustomAIFormat =>
@@ -320,6 +322,7 @@ namespace AdvancedPaste.ViewModels
OnPropertyChanged(nameof(AIProviders));
OnPropertyChanged(nameof(AllowedAIProviders));
OnPropertyChanged(nameof(ShowClipboardPreview));
OnPropertyChanged(nameof(ShowAIPasteSection));
NotifyActiveProviderChanged();

View File

@@ -1 +1 @@
{"properties":{"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}},"paste-ai-configuration":{"active-provider-id":"","providers":[],"use-shared-credentials":true}},"name":"AdvancedPaste","version":"1"}
{"properties":{"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"ShowAIPaste":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}},"paste-ai-configuration":{"active-provider-id":"","providers":[],"use-shared-credentials":true}},"name":"AdvancedPaste","version":"1"}

View File

@@ -29,7 +29,7 @@
* Paste as custom format using AI
- [] Open Settings, navigate to Enable Paste with AI and set OpenAI key.
- [] Copy some text to clipboard. Any text.
- [] Open Advanced Paste window using hotkey, and confirm that Custom intput text box is now enabled. Write "Insert smiley after every word" and press Enter. Observe that result preview shows coppied text with smileys between words. Press Enter to paste the result and observe that it is pasted.
- [] Open Advanced Paste window using hotkey, and confirm that Custom input text box is now enabled. Write "Insert smiley after every word" and press Enter. Observe that result preview shows copied text with smileys between words. Press Enter to paste the result and observe that it is pasted.
- [] Open Advanced Paste window using hotkey. Input some query (any, feel free to play around) and press Enter. When result is shown, click regenerate button, to see if new result is generated. Select one of the results and paste. Observe that correct result is pasted.
- [] Create few custom actions. Set up hotkey for custom actions and confirm they work. Enable/disable custom actions and confirm that the change is reflected in Advanced Paste UI - custom action is not listed. Try different ctrl + <num> in-app shortcuts for custom actions. Try moving custom actions up/down and confirm that the change is reflected in Advanced Paste UI.
- [] Open Settings and disable Custom format preview. Open Advanced Paste window with hotkey, enter some query and press enter. Observe that result is now pasted right away, without showing the preview first.

View File

@@ -144,17 +144,19 @@ namespace EnvironmentVariablesUILib.Helpers
set.Variables = new System.Collections.ObjectModel.ObservableCollection<Variable>(sortedList.Values);
}
internal static bool SetVariableWithoutNotify(Variable variable)
// Profiles override User variables only (see doc/devdocs/modules/environmentvariables.md), so
// every registry write driven by a profile targets the current-user environment regardless of a
// variable's ParentType. These helpers centralize that behavior for the apply/unapply/edit paths.
internal static bool SetProfileVariableWithoutNotify(Variable variable)
{
bool fromMachine = variable.ParentType switch
{
VariablesSetType.Profile => false,
VariablesSetType.User => false,
VariablesSetType.System => true,
_ => throw new NotImplementedException(),
};
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine: false);
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine);
return true;
}
internal static bool UnsetProfileVariableWithoutNotify(Variable variable)
{
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine: false);
return true;
}
@@ -175,21 +177,6 @@ namespace EnvironmentVariablesUILib.Helpers
return true;
}
internal static bool UnsetVariableWithoutNotify(Variable variable)
{
bool fromMachine = variable.ParentType switch
{
VariablesSetType.Profile => false,
VariablesSetType.User => false,
VariablesSetType.System => true,
_ => throw new NotImplementedException(),
};
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine);
return true;
}
internal static bool UnsetVariable(Variable variable)
{
bool fromMachine = variable.ParentType switch

View File

@@ -35,8 +35,6 @@ namespace EnvironmentVariablesUILib.Models
{
foreach (var variable in Variables)
{
var applyToSystem = variable.ApplyToSystem;
// Get existing variable with the same name if it exist
var variableToOverride = EnvironmentVariablesHelper.GetExisting(variable.Name);
@@ -46,13 +44,13 @@ namespace EnvironmentVariablesUILib.Models
variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, this.Name);
// Backup the variable
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride))
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variableToOverride))
{
LoggerInstance.Logger.LogError("Failed to set backup variable.");
}
}
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variable))
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variable))
{
LoggerInstance.Logger.LogError("Failed to set profile variable.");
}
@@ -78,7 +76,7 @@ namespace EnvironmentVariablesUILib.Models
public void UnapplyVariable(Variable variable)
{
// Unset the variable
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(variable))
if (!EnvironmentVariablesHelper.UnsetProfileVariableWithoutNotify(variable))
{
LoggerInstance.Logger.LogError("Failed to unset variable.");
}
@@ -93,12 +91,12 @@ namespace EnvironmentVariablesUILib.Models
{
var variableToRestore = new Variable(originalName, backupVariable.Values, backupVariable.ParentType);
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable))
if (!EnvironmentVariablesHelper.UnsetProfileVariableWithoutNotify(backupVariable))
{
LoggerInstance.Logger.LogError("Failed to unset backup variable.");
}
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore))
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variableToRestore))
{
LoggerInstance.Logger.LogError("Failed to restore backup variable.");
}

View File

@@ -143,12 +143,12 @@ namespace EnvironmentVariablesUILib.Models
{
var variableToRestore = new Variable(clone.Name, backupVariable.Values, backupVariable.ParentType);
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable))
if (!EnvironmentVariablesHelper.UnsetProfileVariableWithoutNotify(backupVariable))
{
LoggerInstance.Logger.LogError("Failed to unset backup variable.");
}
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore))
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variableToRestore))
{
LoggerInstance.Logger.LogError("Failed to restore backup variable.");
}
@@ -169,7 +169,7 @@ namespace EnvironmentVariablesUILib.Models
if (EnvironmentVariablesHelper.GetExisting(variableToOverride.Name) == null)
{
// Backup the variable
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride))
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variableToOverride))
{
LoggerInstance.Logger.LogError("Failed to set backup variable.");
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 B

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -6,6 +6,7 @@ using System;
using System.Drawing;
using System.IO;
using ManagedCommon;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;
@@ -20,7 +21,20 @@ namespace PowerToys.FileLocksmithUI.Converters
if (!string.IsNullOrEmpty(y))
{
icon = Icon.ExtractAssociatedIcon(y);
try
{
icon = Icon.ExtractAssociatedIcon(y);
}
catch (Exception ex)
{
// The process image path can be non-empty but no longer exist on disk
// (e.g. self-updating software that deletes its old versioned directory while
// the old process is still running). ExtractAssociatedIcon then throws and,
// because this converter runs per-row during ListView virtualization, the
// exception would otherwise reach App_UnhandledException and fast-fail the app.
// Fall through to the placeholder icon instead of crashing.
Logger.LogWarning($"Couldn't extract the icon for '{y}'. {ex}");
}
}
if (icon != null)

View File

@@ -88,7 +88,7 @@
</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalDependencies>WindowsApp.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>WindowsApp.lib;Gdiplus.lib;Dwmapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">

View File

@@ -10,6 +10,8 @@
#include "resource.h"
#include <dwmapi.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
@@ -21,6 +23,7 @@ TRACELOGGING_DEFINE_PROVIDER(
// Globals
// ---------------------------------------------------------------------------
static HINSTANCE g_hInstance = nullptr;
static ULONG_PTR g_gdiplusToken = 0; // GDI+ token for overlay border rendering
static HHOOK g_hhkKeyboard = nullptr;
static HHOOK g_hhkMouse = nullptr;
static HWND g_hMsgWnd = nullptr;
@@ -47,6 +50,29 @@ static HWND g_hOverlay = nullptr; // semi-transparent overlay during drag
static int g_overlayInfoX = 0, g_overlayInfoY = 0;
static int g_overlayInfoW = 0, g_overlayInfoH = 0;
// Visible frame overlay metrics. Computed once per drag/resize (cold path) and
// reused while rendering - never recomputed in the mouse-move hot path.
// Margins are the difference between GetWindowRect and the DWM extended frame
// bounds (the invisible resize border), so the fill and border hug the visible
// window. The border is drawn just inside the visible edge; Always On Top draws
// its own border just outside that edge, so the two stack into a clean double
// layer without Grab and Move having to widen its stroke.
static int g_overlayMarginL = 0, g_overlayMarginT = 0, g_overlayMarginR = 0, g_overlayMarginB = 0;
static int g_overlayCornerRadius = 0; // physical px; 0 = square corners
static int g_overlayBorderThickness = 4; // physical px
// Fluent "warning" gold - copy of WinUI SystemFillColorCaution
// (used as a ThemeResource for warnings across the Settings UI). A Win32 layered
// window can't resolve a ThemeResource, so the literal is required here.
static constexpr COLORREF OVERLAY_BORDER_COLOR = RGB(255, 185, 0); // #FFB900
// Border thickness in DIPs (scaled by the target window DPI).
static constexpr int OVERLAY_BORDER_DIP = 4;
// Translucent white wash painted over the visible window during a drag/resize,
// matching the prior overlay. ~40% opacity (premultiplied white = 0x66666666).
static constexpr BYTE OVERLAY_FILL_ALPHA = 0x66;
static bool g_shouldAbsorbAlt =
true; // true if we want to absorb Alt on the next keydown (set when Alt is pressed without dragging, cleared on next non-Alt key or Alt keyup)
static bool g_altAbsorbed = false; // true if we absorbed an Alt keydown
@@ -343,9 +369,128 @@ static void SettingsWatcherThread(DWORD mainThreadId)
static int g_overlayRenderedW = 0;
static int g_overlayRenderedH = 0;
// Maps the DWM window corner preference to a base radius in DIPs, matching
// Always On Top (WindowCornerUtils::CornersRadius).
static int CornerRadiusForWindow(HWND hwnd)
{
// Remote sessions draw square windows even on Win11, yet still report DWMWCP_DEFAULT. Match the
// window: a remote session gets square (radius 0) so the overlay border doesn't round off the corner.
if (GetSystemMetrics(SM_REMOTESESSION))
{
return 0;
}
int pref = 0; // DWMWCP_DEFAULT
if (DwmGetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)) != S_OK)
{
return 0; // pre-Win11 / unsupported -> square corners
}
switch (pref)
{
case DWMWCP_ROUND:
return 8;
case DWMWCP_ROUNDSMALL:
return 4;
case DWMWCP_DEFAULT:
return 8;
default:
return 0; // DWMWCP_DONOTROUND
}
}
// Computes the overlay metrics (margins to the visible frame, corner radius, border
// thickness) for the target window. Cold path only: called at the start of a
// drag/resize and after un-maximize, never from the mouse-move hot path.
static void PrepareOverlayMetrics(HWND target)
{
g_overlayMarginL = g_overlayMarginT = g_overlayMarginR = g_overlayMarginB = 0;
g_overlayCornerRadius = 0;
g_overlayBorderThickness = OVERLAY_BORDER_DIP;
if (!target)
{
return;
}
const UINT dpi = GetDpiForWindow(target);
const float scale = (dpi != 0) ? dpi / 96.0f : 1.0f;
RECT windowRect{};
RECT frameRect{};
if (GetWindowRect(target, &windowRect) &&
SUCCEEDED(DwmGetWindowAttribute(target, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRect, sizeof(frameRect))))
{
g_overlayMarginL = max(0, static_cast<int>(frameRect.left - windowRect.left));
g_overlayMarginT = max(0, static_cast<int>(frameRect.top - windowRect.top));
g_overlayMarginR = max(0, static_cast<int>(windowRect.right - frameRect.right));
g_overlayMarginB = max(0, static_cast<int>(windowRect.bottom - frameRect.bottom));
}
g_overlayCornerRadius = static_cast<int>(CornerRadiusForWindow(target) * scale);
g_overlayBorderThickness = static_cast<int>(OVERLAY_BORDER_DIP * scale);
}
// Draws an antialiased (optionally rounded) border stroke fully inside `rect` using
// GDI+. The stroke hugs the inner edge of `rect` (the visible window frame).
static void DrawOverlayBorder(Gdiplus::Graphics& graphics, const RECT& rect, int thickness, int radius)
{
const int w = rect.right - rect.left;
const int h = rect.bottom - rect.top;
if (w <= 0 || h <= 0 || thickness <= 0)
{
return;
}
// Keep the whole stroke inside the visible frame on every side.
thickness = min(thickness, min(w, h) / 2);
if (thickness <= 0)
{
return;
}
const float half = thickness / 2.0f;
const Gdiplus::RectF path(
rect.left + half,
rect.top + half,
static_cast<Gdiplus::REAL>(w) - thickness,
static_cast<Gdiplus::REAL>(h) - thickness);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
Gdiplus::Pen pen(
Gdiplus::Color(255, GetRValue(OVERLAY_BORDER_COLOR), GetGValue(OVERLAY_BORDER_COLOR), GetBValue(OVERLAY_BORDER_COLOR)),
static_cast<Gdiplus::REAL>(thickness));
if (radius <= 0)
{
graphics.DrawRectangle(&pen, path);
return;
}
// The stroke is centred, so the path corner radius is the window radius minus
// half the thickness; that keeps the outer edge aligned with the window corner.
const float pathRadius = max(0.0f, radius - half);
const float diameter = min(pathRadius * 2.0f, min(path.Width, path.Height));
if (diameter <= 0.0f)
{
graphics.DrawRectangle(&pen, path);
return;
}
Gdiplus::GraphicsPath border;
border.AddArc(path.X, path.Y, diameter, diameter, 180.0f, 90.0f);
border.AddArc(path.GetRight() - diameter, path.Y, diameter, diameter, 270.0f, 90.0f);
border.AddArc(path.GetRight() - diameter, path.GetBottom() - diameter, diameter, diameter, 0.0f, 90.0f);
border.AddArc(path.X, path.GetBottom() - diameter, diameter, diameter, 90.0f, 90.0f);
border.CloseFigure();
graphics.DrawPath(&pen, &border);
}
// Renders the overlay surface using per-pixel alpha via UpdateLayeredWindow.
// The white background is painted at ~40% opacity; the geometry label box is
// painted fully opaque so it remains legible regardless of what is beneath.
// A translucent white wash covers the visible window (matching the prior overlay)
// with a tight warning-gold border on top, both hugging the visible window frame;
// the optional geometry label box is painted fully opaque so it remains legible
// regardless of what is beneath.
static void RenderOverlayContent(HWND hwnd, int cw, int ch)
{
if (!hwnd || cw <= 0 || ch <= 0)
@@ -372,8 +517,52 @@ static void RenderOverlayContent(HWND hwnd, int cw, int ch)
HDC memDC = CreateCompatibleDC(screenDC);
HBITMAP hOldBmp = static_cast<HBITMAP>(SelectObject(memDC, hDib));
// Premultiplied white at ~40% opacity: A=0x66, R=G=B=0x66 → 0x66666666
memset(pBits, 0x66, static_cast<size_t>(cw) * ch * sizeof(DWORD));
// Start fully transparent.
memset(pBits, 0, static_cast<size_t>(cw) * ch * sizeof(DWORD));
// We apply a translucent white rect with a gold border.
// The overlay window spans GetWindowRect, so inset by
// the invisible-border margins so both hug the visible edge; Always On Top draws
// its own border just outside that edge, giving a clean double layer.
{
const RECT visible = {
g_overlayMarginL,
g_overlayMarginT,
cw - g_overlayMarginR,
ch - g_overlayMarginB
};
const int vw = visible.right - visible.left;
const int vh = visible.bottom - visible.top;
Gdiplus::Bitmap bitmap(cw, ch, cw * 4, PixelFormat32bppPARGB, reinterpret_cast<BYTE*>(pBits));
Gdiplus::Graphics graphics(&bitmap);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
if (vw > 0 && vh > 0)
{
Gdiplus::SolidBrush fillBrush(Gdiplus::Color(OVERLAY_FILL_ALPHA, 255, 255, 255));
if (g_overlayCornerRadius > 0)
{
// Round the wash to match the window corners (and the border).
const float d = min(static_cast<float>(g_overlayCornerRadius) * 2.0f,
static_cast<float>(min(vw, vh)));
Gdiplus::GraphicsPath fillPath;
fillPath.AddArc(static_cast<float>(visible.left), static_cast<float>(visible.top), d, d, 180.0f, 90.0f);
fillPath.AddArc(static_cast<float>(visible.right) - d, static_cast<float>(visible.top), d, d, 270.0f, 90.0f);
fillPath.AddArc(static_cast<float>(visible.right) - d, static_cast<float>(visible.bottom) - d, d, d, 0.0f, 90.0f);
fillPath.AddArc(static_cast<float>(visible.left), static_cast<float>(visible.bottom) - d, d, d, 90.0f, 90.0f);
fillPath.CloseFigure();
graphics.FillPath(&fillBrush, &fillPath);
}
else
{
graphics.FillRectangle(&fillBrush, visible.left, visible.top, vw, vh);
}
}
DrawOverlayBorder(graphics, visible, g_overlayBorderThickness, g_overlayCornerRadius);
graphics.Flush();
}
if (g_showGeometry)
{
@@ -1045,6 +1234,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
g_dragTarget = hwnd;
g_dragStart = pt;
GetWindowRect(hwnd, &g_dragWndRect);
PrepareOverlayMetrics(hwnd);
// Show the semi-transparent overlay on top of the target (persistent window fix #9)
ShowOverlay(g_dragWndRect, g_curSizeAll);
@@ -1112,6 +1302,7 @@ static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
g_resizeTarget = hwnd;
g_resizeLast = pt;
GetWindowRect(hwnd, &g_resizeWndRect);
PrepareOverlayMetrics(hwnd);
g_currentHandle = GetClosestHandle(pt, g_resizeWndRect);
ShowOverlay(g_resizeWndRect, CursorForHandle(g_currentHandle));
@@ -1183,6 +1374,9 @@ static void HandleDragMove(POINT pt)
g_dragStart = pt;
g_dragWndRect = {newX, newY, newX + restoredW, newY + restoredH};
// Corner radius / invisible-border margins differ once restored.
PrepareOverlayMetrics(g_dragTarget);
}
}
@@ -1230,6 +1424,9 @@ static void HandleDragResize(POINT pt)
SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
g_resizeWndRect = {newLeft, newTop, newLeft + newW, newTop + newH};
// Corner radius / invisible-border margins differ once restored.
PrepareOverlayMetrics(g_resizeTarget);
g_resizeLast = pt;
g_currentHandle = GetClosestHandle(pt, g_resizeWndRect);
}
@@ -1375,6 +1572,10 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
INITCOMMONCONTROLSEX commonControls = { sizeof(commonControls), ICC_STANDARD_CLASSES };
InitCommonControlsEx(&commonControls);
// Initialise GDI+ for antialiased overlay border rendering
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, nullptr);
// Register a message-only window class
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
@@ -1387,13 +1588,13 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
return 1;
}
// Register the overlay window class (white background, ARROW cursor)
// Register the overlay window class (layered per-pixel-alpha surface, ARROW cursor)
WNDCLASSEXW overlayWindowClass = {};
overlayWindowClass.cbSize = sizeof(overlayWindowClass);
overlayWindowClass.lpfnWndProc = DefWindowProcW;
overlayWindowClass.hInstance = hInstance;
overlayWindowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
overlayWindowClass.hbrBackground = static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH));
overlayWindowClass.hbrBackground = nullptr; // per-pixel alpha via UpdateLayeredWindow
overlayWindowClass.lpszClassName = OVERLAY_CLASS_NAME;
if (!RegisterClassExW(&overlayWindowClass))
{
@@ -1482,6 +1683,11 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
g_hOverlay = nullptr;
}
RemoveTrayIcon();
if (g_gdiplusToken)
{
Gdiplus::GdiplusShutdown(g_gdiplusToken);
g_gdiplusToken = 0;
}
TraceLoggingUnregister(g_hProvider);
return static_cast<int>(msg.wParam);

View File

@@ -3,6 +3,8 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <objidl.h>
#include <gdiplus.h>
#include <shellapi.h>
#include <commctrl.h>
#include <TraceLoggingProvider.h>

View File

@@ -197,7 +197,7 @@ namespace Hosts.UITests
// Open-filter-panel
this.Find<Button>("Filters").Click();
Assert.IsTrue(this.FindAll<Button>("Clear filters").Count == 1, "Filter panel should be opened afer click Filter Button");
Assert.IsTrue(this.FindAll<Button>("Clear filters").Count == 1, "Filter panel should be opened after click Filter Button");
var addressFilterCases = new KeyValuePair<string, int>[]
{

View File

@@ -6,6 +6,8 @@
#include "trace.h"
#include <cmath>
#include <algorithm>
#include <memory>
#include <vector>
#ifdef COMPOSITION
namespace winrt
@@ -48,6 +50,18 @@ private:
void ClearDrawingPoint();
void ClearDrawing();
void BringToFront();
// Ripple mode: spawn the press/hold ring + glow at the click point and
// continue the animation into a fade-out on release. The held ring may
// optionally follow the cursor while held (gated by m_rippleShowDragTrail).
void SpawnRippleHoldDot(MouseButton button);
void FadeRippleHoldDot(MouseButton button);
// Ripple mode: emit a single self-contained ripple (grow + fade) for a quick
// click, independent of any held indicator.
void EmitSingleRipple(MouseButton button);
// Spotlight mode: pressed-state animation that shrinks the mask while
// a mouse button is held and restores it on release.
void SpotlightAnimatePress();
void SpotlightAnimateRelease();
HHOOK m_mouseHook = NULL;
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept;
// Helpers for spotlight overlay
@@ -71,6 +85,16 @@ private:
winrt::CompositionSpriteShape m_leftPointer{ nullptr };
winrt::CompositionSpriteShape m_rightPointer{ nullptr };
winrt::CompositionSpriteShape m_alwaysPointer{ nullptr };
// Ellipse geometries kept alongside the pointer shapes so press-down /
// release animations can target the radius directly.
winrt::CompositionEllipseGeometry m_leftGeometry{ nullptr };
winrt::CompositionEllipseGeometry m_rightGeometry{ nullptr };
// Ripple-mode held glow (the soft halo behind the ring) — paired with
// m_left/rightPointer (which holds the ring shape) while a button is held.
winrt::CompositionSpriteShape m_leftRippleGlow{ nullptr };
winrt::CompositionSpriteShape m_rightRippleGlow{ nullptr };
winrt::CompositionEllipseGeometry m_leftGlowGeometry{ nullptr };
winrt::CompositionEllipseGeometry m_rightGlowGeometry{ nullptr };
// Spotlight overlay (mask with soft feathered edge)
winrt::SpriteVisual m_overlay{ nullptr };
winrt::CompositionMaskBrush m_spotlightMask{ nullptr };
@@ -84,9 +108,22 @@ private:
bool m_rightPointerEnabled = true;
bool m_alwaysPointerEnabled = true;
bool m_spotlightMode = false;
bool m_spotlightPressed = false;
bool m_rippleMode = true;
bool m_rippleShowDragTrail = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_DRAG_TRAIL;
bool m_rippleShowReleasePulse = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SHOW_RELEASE_PULSE;
float m_rippleSize = static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE);
float m_rippleIntensity = static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY);
int m_rippleDurationMs = MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
bool m_leftButtonPressed = false;
bool m_rightButtonPressed = false;
// Pending hold-detection timers. A ripple "held indicator" is only spawned
// once the button has been held past a short threshold; a quick click that
// releases before then emits a single self-contained ripple instead. This
// prevents a single click from rendering two ripples (press + release).
UINT_PTR m_leftHoldTimer = 0;
UINT_PTR m_rightHoldTimer = 0;
UINT_PTR m_timer_id = 0;
bool m_visible = false;
@@ -102,6 +139,11 @@ private:
winrt::Windows::UI::Color m_alwaysColor = MOUSE_HIGHLIGHTER_DEFAULT_ALWAYS_COLOR;
};
static const uint32_t BRING_TO_FRONT_TIMER_ID = 123;
static const uint32_t HOLD_RIPPLE_TIMER_LEFT = 124;
static const uint32_t HOLD_RIPPLE_TIMER_RIGHT = 125;
// How long a ripple button must be held before the persistent "held indicator"
// is shown. Releasing before this is treated as a quick click (single ripple).
static const uint32_t HOLD_RIPPLE_THRESHOLD_MS = 180;
Highlighter* Highlighter::instance = nullptr;
bool Highlighter::CreateHighlighter()
@@ -194,11 +236,34 @@ void Highlighter::AddDrawingPoint(MouseButton button)
{
circleShape.FillBrush(m_compositor.CreateColorBrush(m_leftClickColor));
m_leftPointer = circleShape;
m_leftGeometry = circleGeometry;
// Niels-style press-down shrink: holding the button squeezes the
// circle to 70% over 180ms after a 150ms delay so quick clicks skip
// it. StartDrawingPointFading stops this animation on release.
const float pressedRadius = m_radius * 0.70f;
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
anim.Duration(std::chrono::milliseconds(180));
anim.DelayTime(std::chrono::milliseconds(150));
circleGeometry.StartAnimation(L"Radius", anim);
}
else if (button == MouseButton::Right)
{
circleShape.FillBrush(m_compositor.CreateColorBrush(m_rightClickColor));
m_rightPointer = circleShape;
m_rightGeometry = circleGeometry;
const float pressedRadius = m_radius * 0.70f;
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
anim.Duration(std::chrono::milliseconds(180));
anim.DelayTime(std::chrono::milliseconds(150));
circleGeometry.StartAnimation(L"Radius", anim);
}
else
{
@@ -238,17 +303,36 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
if (button == MouseButton::Left)
{
m_leftPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
if (m_leftRippleGlow)
{
m_leftRippleGlow.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
}
}
else if (button == MouseButton::Right)
{
m_rightPointer.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
if (m_rightRippleGlow)
{
m_rightRippleGlow.Offset({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
}
}
else
{
// always / spotlight idle
if (m_spotlightMode)
{
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
if (m_spotlightPressed)
{
// Only update position while pressed — radius is being animated
if (m_spotlightMaskGradient)
{
m_spotlightMaskGradient.EllipseCenter({ static_cast<float>(pt.x), static_cast<float>(pt.y) });
}
}
else
{
UpdateSpotlightMask(static_cast<float>(pt.x), static_cast<float>(pt.y), m_radius, true);
}
}
else if (m_alwaysPointer)
{
@@ -259,14 +343,24 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button)
void Highlighter::StartDrawingPointFading(MouseButton button)
{
winrt::Windows::UI::Composition::CompositionSpriteShape circleShape{ nullptr };
winrt::Windows::UI::Composition::CompositionEllipseGeometry geom{ nullptr };
if (button == MouseButton::Left)
{
circleShape = m_leftPointer;
geom = m_leftGeometry;
}
else
{
// right
circleShape = m_rightPointer;
geom = m_rightGeometry;
}
// Stop any in-flight press-down shrink so the geometry doesn't keep
// animating while the fill is being faded out.
if (geom && m_compositor)
{
geom.StopAnimation(L"Radius");
}
auto brushColor = circleShape.FillBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>().Color();
@@ -329,6 +423,30 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
switch (wParam)
{
case WM_LBUTTONDOWN:
if (instance->m_spotlightMode)
{
instance->SpotlightAnimatePress();
break;
}
if (instance->m_rippleMode)
{
if (instance->m_leftPointerEnabled)
{
// Defer the held indicator: only spawn it if the button is
// still down after the hold threshold. A quick click handled
// on button-up emits a single ripple instead.
instance->m_leftButtonPressed = true;
if (instance->m_leftHoldTimer == 0)
{
instance->m_leftHoldTimer = SetTimer(instance->m_hwnd, HOLD_RIPPLE_TIMER_LEFT, HOLD_RIPPLE_THRESHOLD_MS, nullptr);
}
if (instance->m_timer_id == 0)
{
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);
}
}
break;
}
if (instance->m_leftPointerEnabled)
{
if (instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
@@ -354,6 +472,28 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
}
break;
case WM_RBUTTONDOWN:
if (instance->m_spotlightMode)
{
instance->SpotlightAnimatePress();
break;
}
if (instance->m_rippleMode)
{
if (instance->m_rightPointerEnabled)
{
// Defer the held indicator (see WM_LBUTTONDOWN).
instance->m_rightButtonPressed = true;
if (instance->m_rightHoldTimer == 0)
{
instance->m_rightHoldTimer = SetTimer(instance->m_hwnd, HOLD_RIPPLE_TIMER_RIGHT, HOLD_RIPPLE_THRESHOLD_MS, nullptr);
}
if (instance->m_timer_id == 0)
{
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);
}
}
break;
}
if (instance->m_rightPointerEnabled)
{
if (instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
@@ -376,6 +516,24 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
}
break;
case WM_MOUSEMOVE:
if (instance->m_rippleMode)
{
// Held ripple ring follows the cursor while a button is down,
// gated by the "follow cursor while held" setting. When the
// setting is off, the ring stays anchored at the click point.
if (instance->m_rippleShowDragTrail)
{
if (instance->m_leftButtonPressed && instance->m_leftPointer)
{
instance->UpdateDrawingPointPosition(MouseButton::Left);
}
if (instance->m_rightButtonPressed && instance->m_rightPointer)
{
instance->UpdateDrawingPointPosition(MouseButton::Right);
}
}
break;
}
if (instance->m_leftButtonPressed)
{
instance->UpdateDrawingPointPosition(MouseButton::Left);
@@ -390,11 +548,33 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
}
break;
case WM_LBUTTONUP:
if (instance->m_spotlightPressed)
{
instance->SpotlightAnimateRelease();
}
if (instance->m_leftButtonPressed)
{
instance->StartDrawingPointFading(MouseButton::Left);
if (instance->m_rippleMode)
{
if (instance->m_leftHoldTimer != 0)
{
// Released before the hold threshold => quick click.
KillTimer(instance->m_hwnd, instance->m_leftHoldTimer);
instance->m_leftHoldTimer = 0;
instance->EmitSingleRipple(MouseButton::Left);
}
else
{
// Held indicator was already shown; expand + fade it.
instance->FadeRippleHoldDot(MouseButton::Left);
}
}
else
{
instance->StartDrawingPointFading(MouseButton::Left);
}
instance->m_leftButtonPressed = false;
if (instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
if (!instance->m_rippleMode && instance->m_alwaysPointerEnabled && !instance->m_rightButtonPressed)
{
// Add AlwaysPointer only when it's enabled and RightPointer is not active
instance->AddDrawingPoint(MouseButton::None);
@@ -402,11 +582,32 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
}
break;
case WM_RBUTTONUP:
if (instance->m_spotlightPressed)
{
instance->SpotlightAnimateRelease();
}
if (instance->m_rightButtonPressed)
{
instance->StartDrawingPointFading(MouseButton::Right);
if (instance->m_rippleMode)
{
if (instance->m_rightHoldTimer != 0)
{
// Released before the hold threshold => quick click.
KillTimer(instance->m_hwnd, instance->m_rightHoldTimer);
instance->m_rightHoldTimer = 0;
instance->EmitSingleRipple(MouseButton::Right);
}
else
{
instance->FadeRippleHoldDot(MouseButton::Right);
}
}
else
{
instance->StartDrawingPointFading(MouseButton::Right);
}
instance->m_rightButtonPressed = false;
if (instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
if (!instance->m_rippleMode && instance->m_alwaysPointerEnabled && !instance->m_leftButtonPressed)
{
// Add AlwaysPointer only when it's enabled and LeftPointer is not active
instance->AddDrawingPoint(MouseButton::None);
@@ -448,9 +649,16 @@ void Highlighter::StopDrawing()
m_visible = false;
m_leftButtonPressed = false;
m_rightButtonPressed = false;
m_spotlightPressed = false;
m_leftPointer = nullptr;
m_rightPointer = nullptr;
m_alwaysPointer = nullptr;
m_leftGeometry = nullptr;
m_rightGeometry = nullptr;
m_leftRippleGlow = nullptr;
m_rightRippleGlow = nullptr;
m_leftGlowGeometry = nullptr;
m_rightGlowGeometry = nullptr;
if (m_overlay)
{
m_overlay.IsVisible(false);
@@ -478,6 +686,16 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings)
m_rightPointerEnabled = settings.rightButtonColor.A != 0;
m_alwaysPointerEnabled = settings.alwaysColor.A != 0;
m_spotlightMode = settings.spotlightMode && settings.alwaysColor.A != 0;
m_rippleMode = settings.rippleMode && !m_spotlightMode;
m_rippleSize = (settings.rippleSize > 0) ? static_cast<float>(settings.rippleSize) : static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_SIZE);
m_rippleIntensity = (settings.rippleIntensity > 0.0) ? static_cast<float>(settings.rippleIntensity) : static_cast<float>(MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_INTENSITY);
m_rippleDurationMs = (settings.rippleDurationMs > 0) ? settings.rippleDurationMs : MOUSE_HIGHLIGHTER_DEFAULT_RIPPLE_DURATION_MS;
m_rippleShowDragTrail = settings.rippleShowDragTrail;
m_rippleShowReleasePulse = settings.rippleShowReleasePulse;
// Reset transient pressed-state flag so a settings change while a button
// happens to be down doesn't leave the spotlight stuck at a shrunken size.
m_spotlightPressed = false;
if (m_spotlightMode)
{
@@ -548,6 +766,7 @@ LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LP
// If we would use a timer with a 50 ms period, there would be a flickering on the UI, as in most of the cases
// the pinned window hides our window in a few milliseconds.
case BRING_TO_FRONT_TIMER_ID:
{
static int fireCount = 0;
if (fireCount++ >= 4)
{
@@ -558,6 +777,24 @@ LRESULT CALLBACK Highlighter::WndProc(HWND hWnd, UINT message, WPARAM wParam, LP
instance->BringToFront();
break;
}
case HOLD_RIPPLE_TIMER_LEFT:
// Button held past the threshold: show the persistent held indicator.
KillTimer(instance->m_hwnd, instance->m_leftHoldTimer);
instance->m_leftHoldTimer = 0;
if (instance->m_leftButtonPressed)
{
instance->SpawnRippleHoldDot(MouseButton::Left);
}
break;
case HOLD_RIPPLE_TIMER_RIGHT:
KillTimer(instance->m_hwnd, instance->m_rightHoldTimer);
instance->m_rightHoldTimer = 0;
if (instance->m_rightButtonPressed)
{
instance->SpawnRippleHoldDot(MouseButton::Right);
}
break;
}
break;
}
default:
@@ -643,6 +880,548 @@ void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool sho
}
}
// Spotlight press-down: shrink the mask radius briefly while a button is held.
void Highlighter::SpotlightAnimatePress()
{
if (!m_spotlightMode || !m_spotlightMaskGradient)
{
return;
}
m_spotlightPressed = true;
const float pressedRadius = m_radius * 0.85f;
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.2f, 0.0f }, { 0.4f, 1.0f });
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
anim.InsertKeyFrame(0.0f, { m_radius, m_radius });
anim.InsertKeyFrame(1.0f, { pressedRadius, pressedRadius }, ease);
anim.Duration(std::chrono::milliseconds(120));
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", anim);
}
// Spotlight release: animate the mask back to the configured radius.
void Highlighter::SpotlightAnimateRelease()
{
m_spotlightPressed = false;
if (!m_spotlightMode || !m_spotlightMaskGradient)
{
return;
}
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
auto current = m_spotlightMaskGradient.EllipseRadius();
auto anim = m_compositor.CreateVector2KeyFrameAnimation();
anim.InsertKeyFrame(0.0f, current);
anim.InsertKeyFrame(1.0f, { m_radius, m_radius }, ease);
anim.Duration(std::chrono::milliseconds(200));
m_spotlightMaskGradient.StartAnimation(L"EllipseRadius", anim);
}
// Spawn the press/hold ring + glow at the click point. The shapes persist
// until FadeRippleHoldDot is called (button-up). While held they can be
// re-positioned to follow the cursor (UpdateDrawingPointPosition).
void Highlighter::SpawnRippleHoldDot(MouseButton button)
{
if (!m_compositor || !m_shape)
{
return;
}
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
if (color.A == 0)
{
return;
}
POINT pt{};
if (!GetCursorPos(&pt))
{
return;
}
ScreenToClient(m_hwnd, &pt);
const float fx = static_cast<float>(pt.x);
const float fy = static_cast<float>(pt.y);
// Resolve sizing/intensity from the ripple-specific settings so they're
// independent of the legacy "always-on dot" controls.
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
float intensity = m_rippleIntensity;
if (intensity < 0.15f) intensity = 0.15f;
if (intensity > 1.35f) intensity = 1.35f;
const float ringHeld = baseSize * 0.55f;
const float glowHeld = baseSize * 0.65f;
const float lineWidth = (std::max)(2.25f, baseSize * (0.035f + intensity * 0.045f));
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
// Held indicator: appears once the button has been held past the hold
// threshold and sits at the held radius until release. It must NOT expand
// outward on appearance — it only FADES IN at the held size. The single
// outward "ripple" expansion happens exclusively on release
// (FadeRippleHoldDot). If this grew outward, a slow single click (release
// shortly after the threshold) would show grow-to-held + release as two
// expansions — the double-ripple bug.
auto dur = std::chrono::milliseconds(120);
auto clampByte = [](float v) -> uint8_t {
if (v < 0.0f) v = 0.0f;
if (v > 255.0f) v = 255.0f;
return static_cast<uint8_t>(v);
};
// Glow color is the click color, lower alpha (×0.30), scaled by intensity.
const float glowAlpha = static_cast<float>(color.A) * 0.30f * intensity;
auto glowColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(glowAlpha), color.R, color.G, color.B);
auto glowTransparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
// Ring color uses full base alpha (alphaMul like the press recipe).
const float alphaMul = 0.18f + intensity * 0.78f;
auto ringColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * alphaMul), color.R, color.G, color.B);
auto ringTransparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
// Clean up any stray "still held" shapes for this button — guards against
// stray button-down without matching button-up (e.g. focus loss).
winrt::CompositionSpriteShape& heldRing = (button == MouseButton::Left) ? m_leftPointer : m_rightPointer;
winrt::CompositionSpriteShape& heldGlow = (button == MouseButton::Left) ? m_leftRippleGlow : m_rightRippleGlow;
winrt::CompositionEllipseGeometry& heldGeom = (button == MouseButton::Left) ? m_leftGeometry : m_rightGeometry;
winrt::CompositionEllipseGeometry& heldGlowGeom = (button == MouseButton::Left) ? m_leftGlowGeometry : m_rightGlowGeometry;
if (m_shape && m_shape.Shapes())
{
auto shapes = m_shape.Shapes();
uint32_t idx = 0;
if (heldRing && shapes.IndexOf(heldRing, idx))
{
shapes.RemoveAt(idx);
}
if (heldGlow && shapes.IndexOf(heldGlow, idx))
{
shapes.RemoveAt(idx);
}
}
// Glow (filled) — added first so the ring renders on top. Sits at the held
// radius and fades its alpha in (no outward size growth).
auto glowGeom = m_compositor.CreateEllipseGeometry();
glowGeom.Radius({ glowHeld, glowHeld });
auto glowBrush = m_compositor.CreateColorBrush(glowTransparent);
auto glowShape = m_compositor.CreateSpriteShape(glowGeom);
glowShape.Offset({ fx, fy });
glowShape.FillBrush(glowBrush);
m_shape.Shapes().Append(glowShape);
auto glowFadeIn = m_compositor.CreateColorKeyFrameAnimation();
glowFadeIn.InsertKeyFrame(0.0f, glowTransparent);
glowFadeIn.InsertKeyFrame(1.0f, glowColor, ease);
glowFadeIn.Duration(dur);
glowBrush.StartAnimation(L"Color", glowFadeIn);
// Ring (stroked) — same: fixed at held radius, alpha fade-in only.
auto ringGeom = m_compositor.CreateEllipseGeometry();
ringGeom.Radius({ ringHeld, ringHeld });
auto ringBrush = m_compositor.CreateColorBrush(ringTransparent);
auto ringShape = m_compositor.CreateSpriteShape(ringGeom);
ringShape.Offset({ fx, fy });
ringShape.StrokeBrush(ringBrush);
ringShape.StrokeThickness(lineWidth);
ringShape.IsStrokeNonScaling(true);
m_shape.Shapes().Append(ringShape);
auto ringFadeIn = m_compositor.CreateColorKeyFrameAnimation();
ringFadeIn.InsertKeyFrame(0.0f, ringTransparent);
ringFadeIn.InsertKeyFrame(1.0f, ringColor, ease);
ringFadeIn.Duration(dur);
ringBrush.StartAnimation(L"Color", ringFadeIn);
heldRing = ringShape;
heldGlow = glowShape;
heldGeom = ringGeom;
heldGlowGeom = glowGeom;
}
// Continue the held-ring/glow animation outward and fade both to transparent.
// For right-click, optionally spawn the expanding crosshair lines.
void Highlighter::FadeRippleHoldDot(MouseButton button)
{
if (!m_compositor || !m_shape)
{
return;
}
winrt::CompositionSpriteShape& heldRing = (button == MouseButton::Left) ? m_leftPointer : m_rightPointer;
winrt::CompositionSpriteShape& heldGlow = (button == MouseButton::Left) ? m_leftRippleGlow : m_rightRippleGlow;
winrt::CompositionEllipseGeometry& heldGeom = (button == MouseButton::Left) ? m_leftGeometry : m_rightGeometry;
winrt::CompositionEllipseGeometry& heldGlowGeom = (button == MouseButton::Left) ? m_leftGlowGeometry : m_rightGlowGeometry;
if (!heldRing && !heldGlow)
{
return;
}
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
float intensity = m_rippleIntensity;
if (intensity < 0.15f) intensity = 0.15f;
if (intensity > 1.35f) intensity = 1.35f;
int durationMs = m_rippleDurationMs;
if (durationMs < 60) durationMs = 60;
if (durationMs > 2000) durationMs = 2000;
auto dur = std::chrono::milliseconds(durationMs);
const float ringHeld = baseSize * 0.55f;
const float ringEnd = baseSize * 1.05f;
const float glowHeld = baseSize * 0.65f;
const float glowEnd = baseSize * 1.40f;
auto clampByte = [](float v) -> uint8_t {
if (v < 0.0f) v = 0.0f;
if (v > 255.0f) v = 255.0f;
return static_cast<uint8_t>(v);
};
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
auto transparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
// Track everything spawned by this fade (and the held shapes themselves)
// so the completion callback can remove them in one pass.
auto spawned = std::make_shared<std::vector<winrt::CompositionSpriteShape>>();
auto batch = m_compositor.CreateScopedBatch(winrt::CompositionBatchTypes::Animation);
if (heldGlow && heldGlowGeom)
{
// The held indicator has settled at the held radius; expand it outward
// from there and fade it to transparent.
heldGlowGeom.StopAnimation(L"Radius");
auto glowAnim = m_compositor.CreateVector2KeyFrameAnimation();
glowAnim.InsertKeyFrame(0.0f, { glowHeld, glowHeld });
glowAnim.InsertKeyFrame(1.0f, { glowEnd, glowEnd }, ease);
glowAnim.Duration(dur);
heldGlowGeom.StartAnimation(L"Radius", glowAnim);
auto brush = heldGlow.FillBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>();
auto startColor = brush.Color();
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
colorAnim.InsertKeyFrame(0.0f, startColor);
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
colorAnim.Duration(dur);
brush.StartAnimation(L"Color", colorAnim);
spawned->push_back(heldGlow);
}
if (heldRing && heldGeom)
{
heldGeom.StopAnimation(L"Radius");
auto ringAnim = m_compositor.CreateVector2KeyFrameAnimation();
ringAnim.InsertKeyFrame(0.0f, { ringHeld, ringHeld });
ringAnim.InsertKeyFrame(1.0f, { ringEnd, ringEnd }, ease);
ringAnim.Duration(dur);
heldGeom.StartAnimation(L"Radius", ringAnim);
auto brush = heldRing.StrokeBrush().as<winrt::Windows::UI::Composition::CompositionColorBrush>();
auto startColor = brush.Color();
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
colorAnim.InsertKeyFrame(0.0f, startColor);
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
colorAnim.Duration(dur);
brush.StartAnimation(L"Color", colorAnim);
spawned->push_back(heldRing);
}
// Right-click only: spawn expanding crosshair lines centered on the ring.
// Gated by the "show crosshairs on right-click release" toggle.
if (button == MouseButton::Right && m_rippleShowReleasePulse && heldRing)
{
const float xhairAlphaMul = 0.18f + intensity * 0.78f;
auto xhairColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * xhairAlphaMul), color.R, color.G, color.B);
const float xhairThickness = (std::max)(1.25f, baseSize * (0.025f + intensity * 0.03f));
auto center = heldRing.Offset();
const float startSpan = ringHeld * 0.85f;
const float endSpan = ringEnd * 0.85f;
auto makeLine = [&](float ax1, float ay1, float ax2, float ay2,
float bx1, float by1, float bx2, float by2) {
auto lineGeom = m_compositor.CreateLineGeometry();
lineGeom.Start({ ax1, ay1 });
lineGeom.End({ ax2, ay2 });
auto lineBrush = m_compositor.CreateColorBrush(xhairColor);
auto lineShape = m_compositor.CreateSpriteShape(lineGeom);
lineShape.StrokeBrush(lineBrush);
lineShape.StrokeThickness(xhairThickness);
lineShape.IsStrokeNonScaling(true);
m_shape.Shapes().Append(lineShape);
spawned->push_back(lineShape);
auto startAnim = m_compositor.CreateVector2KeyFrameAnimation();
startAnim.InsertKeyFrame(0.0f, { ax1, ay1 });
startAnim.InsertKeyFrame(1.0f, { bx1, by1 }, ease);
startAnim.Duration(dur);
lineGeom.StartAnimation(L"Start", startAnim);
auto endAnim = m_compositor.CreateVector2KeyFrameAnimation();
endAnim.InsertKeyFrame(0.0f, { ax2, ay2 });
endAnim.InsertKeyFrame(1.0f, { bx2, by2 }, ease);
endAnim.Duration(dur);
lineGeom.StartAnimation(L"End", endAnim);
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
colorAnim.InsertKeyFrame(0.0f, xhairColor);
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
colorAnim.Duration(dur);
lineBrush.StartAnimation(L"Color", colorAnim);
};
// Horizontal line (left half, right half).
makeLine(center.x - startSpan, center.y, center.x - startSpan * 0.30f, center.y,
center.x - endSpan, center.y, center.x - endSpan * 0.30f, center.y);
makeLine(center.x + startSpan * 0.30f, center.y, center.x + startSpan, center.y,
center.x + endSpan * 0.30f, center.y, center.x + endSpan, center.y);
// Vertical line (top half, bottom half).
makeLine(center.x, center.y - startSpan, center.x, center.y - startSpan * 0.30f,
center.x, center.y - endSpan, center.x, center.y - endSpan * 0.30f);
makeLine(center.x, center.y + startSpan * 0.30f, center.x, center.y + startSpan,
center.x, center.y + endSpan * 0.30f, center.x, center.y + endSpan);
}
// Detach our member handles BEFORE the batch completes so subsequent
// press events on this button create fresh shapes rather than racing.
heldRing = nullptr;
heldGlow = nullptr;
heldGeom = nullptr;
heldGlowGeom = nullptr;
batch.End();
if (spawned->empty())
{
return;
}
auto dispatcher = m_dispatcherQueueController.DispatcherQueue();
batch.Completed([dispatcher, spawned](auto&&, auto&&) {
dispatcher.TryEnqueue([spawned]() {
try
{
if (Highlighter::instance == nullptr || Highlighter::instance->m_shape == nullptr)
{
return;
}
auto shapes = Highlighter::instance->m_shape.Shapes();
for (auto const& s : *spawned)
{
uint32_t index = 0;
if (shapes.IndexOf(s, index))
{
shapes.RemoveAt(index);
}
}
}
catch (...)
{
// Highlighter may have torn down between batch completion and dispatch — ignore.
}
});
});
}
// Self-contained single ripple for a quick click (press + release before the
// hold threshold). Spawns a fresh ring + glow that grow from the click point
// outward and fade to transparent in one continuous animation — no held
// indicator, so a single click produces exactly one ripple. For right-click,
// optionally spawns the expanding crosshair lines too.
void Highlighter::EmitSingleRipple(MouseButton button)
{
if (!m_compositor || !m_shape)
{
return;
}
winrt::Windows::UI::Color color = (button == MouseButton::Left) ? m_leftClickColor : m_rightClickColor;
if (color.A == 0)
{
return;
}
POINT pt{};
if (!GetCursorPos(&pt))
{
return;
}
ScreenToClient(m_hwnd, &pt);
const float fx = static_cast<float>(pt.x);
const float fy = static_cast<float>(pt.y);
const float baseSize = (m_rippleSize > 1.0f) ? m_rippleSize : 1.0f;
float intensity = m_rippleIntensity;
if (intensity < 0.15f) intensity = 0.15f;
if (intensity > 1.35f) intensity = 1.35f;
int durationMs = m_rippleDurationMs;
if (durationMs < 60) durationMs = 60;
if (durationMs > 2000) durationMs = 2000;
auto dur = std::chrono::milliseconds(durationMs);
const float ringStart = baseSize * 0.20f;
const float ringEnd = baseSize * 1.05f;
const float glowStart = baseSize * 0.30f;
const float glowEnd = baseSize * 1.40f;
const float lineWidth = (std::max)(2.25f, baseSize * (0.035f + intensity * 0.045f));
auto clampByte = [](float v) -> uint8_t {
if (v < 0.0f) v = 0.0f;
if (v > 255.0f) v = 255.0f;
return static_cast<uint8_t>(v);
};
const float glowAlpha = static_cast<float>(color.A) * 0.30f * intensity;
auto glowColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(glowAlpha), color.R, color.G, color.B);
const float alphaMul = 0.18f + intensity * 0.78f;
auto ringColor = winrt::Windows::UI::ColorHelper::FromArgb(clampByte(static_cast<float>(color.A) * alphaMul), color.R, color.G, color.B);
auto ease = m_compositor.CreateCubicBezierEasingFunction({ 0.215f, 0.61f }, { 0.355f, 1.0f });
auto transparent = winrt::Windows::UI::ColorHelper::FromArgb(0, color.R, color.G, color.B);
auto spawned = std::make_shared<std::vector<winrt::CompositionSpriteShape>>();
auto batch = m_compositor.CreateScopedBatch(winrt::CompositionBatchTypes::Animation);
// Glow (filled) — added first so the ring renders on top.
auto glowGeom = m_compositor.CreateEllipseGeometry();
glowGeom.Radius({ glowStart, glowStart });
auto glowBrush = m_compositor.CreateColorBrush(glowColor);
auto glowShape = m_compositor.CreateSpriteShape(glowGeom);
glowShape.Offset({ fx, fy });
glowShape.FillBrush(glowBrush);
m_shape.Shapes().Append(glowShape);
spawned->push_back(glowShape);
auto glowAnim = m_compositor.CreateVector2KeyFrameAnimation();
glowAnim.InsertKeyFrame(0.0f, { glowStart, glowStart });
glowAnim.InsertKeyFrame(1.0f, { glowEnd, glowEnd }, ease);
glowAnim.Duration(dur);
glowGeom.StartAnimation(L"Radius", glowAnim);
auto glowColorAnim = m_compositor.CreateColorKeyFrameAnimation();
glowColorAnim.InsertKeyFrame(0.0f, glowColor);
glowColorAnim.InsertKeyFrame(1.0f, transparent, ease);
glowColorAnim.Duration(dur);
glowBrush.StartAnimation(L"Color", glowColorAnim);
// Ring (stroked).
auto ringGeom = m_compositor.CreateEllipseGeometry();
ringGeom.Radius({ ringStart, ringStart });
auto ringBrush = m_compositor.CreateColorBrush(ringColor);
auto ringShape = m_compositor.CreateSpriteShape(ringGeom);
ringShape.Offset({ fx, fy });
ringShape.StrokeBrush(ringBrush);
ringShape.StrokeThickness(lineWidth);
ringShape.IsStrokeNonScaling(true);
m_shape.Shapes().Append(ringShape);
spawned->push_back(ringShape);
auto ringAnim = m_compositor.CreateVector2KeyFrameAnimation();
ringAnim.InsertKeyFrame(0.0f, { ringStart, ringStart });
ringAnim.InsertKeyFrame(1.0f, { ringEnd, ringEnd }, ease);
ringAnim.Duration(dur);
ringGeom.StartAnimation(L"Radius", ringAnim);
auto ringColorAnim = m_compositor.CreateColorKeyFrameAnimation();
ringColorAnim.InsertKeyFrame(0.0f, ringColor);
ringColorAnim.InsertKeyFrame(1.0f, transparent, ease);
ringColorAnim.Duration(dur);
ringBrush.StartAnimation(L"Color", ringColorAnim);
// Right-click only: spawn expanding crosshair lines centered on the click
// point. Gated by the "show crosshairs on right-click release" toggle.
if (button == MouseButton::Right && m_rippleShowReleasePulse)
{
auto xhairColor = ringColor;
const float xhairThickness = (std::max)(1.25f, baseSize * (0.025f + intensity * 0.03f));
const float startSpan = (baseSize * 0.55f) * 0.85f;
const float endSpan = ringEnd * 0.85f;
auto makeLine = [&](float ax1, float ay1, float ax2, float ay2,
float bx1, float by1, float bx2, float by2) {
auto lineGeom = m_compositor.CreateLineGeometry();
lineGeom.Start({ ax1, ay1 });
lineGeom.End({ ax2, ay2 });
auto lineBrush = m_compositor.CreateColorBrush(xhairColor);
auto lineShape = m_compositor.CreateSpriteShape(lineGeom);
lineShape.StrokeBrush(lineBrush);
lineShape.StrokeThickness(xhairThickness);
lineShape.IsStrokeNonScaling(true);
m_shape.Shapes().Append(lineShape);
spawned->push_back(lineShape);
auto startAnim = m_compositor.CreateVector2KeyFrameAnimation();
startAnim.InsertKeyFrame(0.0f, { ax1, ay1 });
startAnim.InsertKeyFrame(1.0f, { bx1, by1 }, ease);
startAnim.Duration(dur);
lineGeom.StartAnimation(L"Start", startAnim);
auto endAnim = m_compositor.CreateVector2KeyFrameAnimation();
endAnim.InsertKeyFrame(0.0f, { ax2, ay2 });
endAnim.InsertKeyFrame(1.0f, { bx2, by2 }, ease);
endAnim.Duration(dur);
lineGeom.StartAnimation(L"End", endAnim);
auto colorAnim = m_compositor.CreateColorKeyFrameAnimation();
colorAnim.InsertKeyFrame(0.0f, xhairColor);
colorAnim.InsertKeyFrame(1.0f, transparent, ease);
colorAnim.Duration(dur);
lineBrush.StartAnimation(L"Color", colorAnim);
};
// Horizontal line (left half, right half).
makeLine(fx - startSpan, fy, fx - startSpan * 0.30f, fy,
fx - endSpan, fy, fx - endSpan * 0.30f, fy);
makeLine(fx + startSpan * 0.30f, fy, fx + startSpan, fy,
fx + endSpan * 0.30f, fy, fx + endSpan, fy);
// Vertical line (top half, bottom half).
makeLine(fx, fy - startSpan, fx, fy - startSpan * 0.30f,
fx, fy - endSpan, fx, fy - endSpan * 0.30f);
makeLine(fx, fy + startSpan * 0.30f, fx, fy + startSpan,
fx, fy + endSpan * 0.30f, fx, fy + endSpan);
}
batch.End();
auto dispatcher = m_dispatcherQueueController.DispatcherQueue();
batch.Completed([dispatcher, spawned](auto&&, auto&&) {
dispatcher.TryEnqueue([spawned]() {
try
{
if (Highlighter::instance == nullptr || Highlighter::instance->m_shape == nullptr)
{
return;
}
auto shapes = Highlighter::instance->m_shape.Shapes();
for (auto const& s : *spawned)
{
uint32_t index = 0;
if (shapes.IndexOf(s, index))
{
shapes.RemoveAt(index);
}
}
}
catch (...)
{
// Highlighter may have torn down between batch completion and dispatch — ignore.
}
});
});
}
#pragma region MouseHighlighter_API
void MouseHighlighterApplySettings(MouseHighlighterSettings settings)

Some files were not shown because too many files have changed in this diff Show More