Compare commits

..

39 Commits

Author SHA1 Message Date
Zach Teutsch
cf137ccbbc Merge branch 'main' into stable 2026-03-16 21:39:04 -04:00
Niels Laute
74c53c14e6 KBM Icon fix (#46157)
<!-- 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: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
2026-03-16 21:29:29 -04:00
Michael Jolley
77173cd075 CmdPal: Stop dock window resizes from saving for normal window opens (#46118)
Currently, if you resize a window opened from the dock (i.e. performance
monitor commands) then exit CmdPal, the resized "size" persists on
normal hotkey opens. This change tells CmdPal to revert the size when
opened and only save the size on normal window close.

Fixes #45591

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-03-16 19:47:54 +01:00
Niels Laute
149e7b1efe Update LightSwitchPage (#46160)
<!-- 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
2026-03-16 18:20:50 +00:00
Kai Tao
0c2d24c3f6 PowerToys Extension: Use project build because we don't need packagereference (#46080)
<!-- 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
At first version, tried to use the project reference instead of package
reference, while it did not work in my local environment, so used
package reference instead.

While planned to project reference for many reasons like first day
problem explosure in sdk, maintain the strict consistent winmd with the
extension host.

Hopefully to solve some of the extension not starting problem.

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

Built an installer and validated in my arm64 and x64 machine, works
perfectly
2026-03-16 10:58:21 +08:00
Zach Teutsch
2cc051ee33 Merge branch 'main' into stable 2026-03-12 14:29:12 -04:00
Michael Jolley
b81ea23c68 CmdPal: Adding a lock around perf monitor updates (#46061)
Based on reported bug in Teams.

Added lock around OnLoadBasePage._loadCount and modified
PerformanceWidgetPage to use Interlocked.Increment/Decrement on
_loadCount.

---------

Co-authored-by: leileizhang <leilzh@microsoft.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: moooyo <42196638+moooyo@users.noreply.github.com>
Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Kai Tao <69313318+vanzue@users.noreply.github.com>
Co-authored-by: Jiří Polášek <me@jiripolasek.com>
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Co-authored-by: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
Co-authored-by: Mike Hall <mikehall@microsoft.com>
Co-authored-by: vanzue <vanzue@outlook.com>
Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
Co-authored-by: Thanh Nguyen <74597207+ThanhNguyxn@users.noreply.github.com>
Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
2026-03-11 17:06:44 -04:00
Jaylyn Barbee
39bbf0593e [KBM] Fixing text replacement bug (#46069)
There is a bug now where text replacement is causing the app to crash.
Wrapping those blocks in try catch so the worker stays alive.
2026-03-11 14:02:46 -04:00
Dave Rayment
4620f6f381 [Docs] Update PowerToys download links to version 0.97.2 (#46058)
<!-- 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 0.97.1 download links to 0.97.2 in the main README.md.

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

- [x] Closes: #46057
<!-- - [ ] 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

Updated all download section descriptions and all links under the "items
that need to be updated release to release" comment, which updates the
download section links themselves.

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

Tested all download links to confirm they have been updated correctly:

<img width="418" height="392" alt="image"
src="https://github.com/user-attachments/assets/09628863-7681-4e71-9d31-bc9bb4bb91c1"
/>
2026-03-11 10:20:49 +00:00
Zach Teutsch
efc89b01ff Merge branch 'main' into stable 2026-03-10 22:42:57 -04:00
Zach Teutsch
9717eaac4c Merge branch 'main' into stable 2026-03-09 22:19:50 -04:00
Zach Teutsch
b5716d0499 add kbm ui dll to esrp signing json 2026-03-04 21:58:03 -05:00
Zach Teutsch
a4d23b7607 fix ESRP path for kbm winui 2026-03-04 20:24:20 -05:00
Zach Teutsch
5409b1b907 fix AppListItem.cs dupe function from merge 2026-03-04 18:12:01 -05:00
Zach Teutsch
fbe952715c Merge branch 'main' into stable 2026-03-04 17:05:50 -05:00
Shawn Yuan
034759f949 Fix WinuiEx crash issue (#45443)
<!-- 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 a crash related to `IsShownInSwitchers` when explorer.exe is not
running. The property has been removed from XAML and is now set in the
C# backend with added exception handling to improve stability. No
changes were made for projects where the property is set to true, as
they are not affected.

<!-- 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
- [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
- [ ] [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: vanzue <vanzue@outlook.com>
2026-02-06 17:31:07 +08:00
Thanh Nguyen
934c3bbce9 Fix CursorWrap "Automatically activate on utility startup" setting not persisting (#45210)
## Summary of the Pull Request

Fixes #45185 - CursorWrap "Automatically activate on utility startup"
setting cannot be disabled, and prevents spurious activation on startup.

## PR Checklist

- [x] Closes: #45185
- [x] **Communication:** Issue was reported by community; fix follows
established patterns from MousePointerCrosshairs
- [x] **Tests:** Manual validation performed by contributor (video
available)
- [x] **Localization:** No new user-facing strings added
- [ ] **Dev docs:** N/A - bug fix only
- [ ] **New binaries:** N/A - no new binaries
- [ ] **Documentation updated:** N/A - bug fix only

## Detailed Description of the Pull Request / Additional comments

### Problem

Users reported that disabling the "Automatically activate on utility
startup" setting for CursorWrap does not work - the mouse hook always
starts automatically regardless of the setting value.

### Root Causes

1. **`dllmain.cpp` `enable()` method**: `StartMouseHook()` was always
called unconditionally, ignoring `m_autoActivate`.
2. **`MouseUtilsViewModel.cs` `IsCursorWrapEnabled` setter**: enabling
CursorWrap forced `AutoActivate = true`, overriding the user's
preference.
3. **Startup edge case**: the trigger event could remain signaled from a
previous session, immediately toggling CursorWrap on startup even when
AutoActivate is off.

### Solution

1. **`src/modules/MouseUtils/CursorWrap/dllmain.cpp`**: only start the
mouse hook if `m_autoActivate` is true.
2. **`src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs`**:
remove the line that forced `AutoActivate = true` when enabling
CursorWrap.
3. **`src/modules/MouseUtils/CursorWrap/dllmain.cpp`**: reset the
trigger event on enable to avoid immediate activation on startup.

### Pattern Reference

This fix follows the same pattern used by **MousePointerCrosshairs**
module which has a similar `AutoActivate` setting that works correctly.

## Validation Steps Performed

### Build

- `tools\build\build.ps1 -Platform x64 -Configuration Debug`

### Manual validation (contributor)

#### Test Case 1: AutoActivate = false (should NOT auto-start mouse
hook)

1. Open PowerToys Settings → Mouse Utilities → Cursor Wrap
2. Enable Cursor Wrap
3. **Disable** "Automatically activate on utility startup"
4. Close PowerToys completely (right-click tray icon → Exit)
5. Restart PowerToys
6. **Expected Result**: CursorWrap module is loaded but mouse hook is
NOT active - cursor does NOT wrap at screen edges
7. Press activation hotkey (default: `Win+Alt+U`)
8. **Expected Result**: Mouse hook activates, cursor now wraps at screen
edges
9. **Actual Result**:  Works as expected

#### Test Case 2: AutoActivate = true (should auto-start mouse hook)

1. Open PowerToys Settings → Mouse Utilities → Cursor Wrap
2. Enable Cursor Wrap
3. **Enable** "Automatically activate on utility startup"
4. Close PowerToys completely
5. Restart PowerToys
6. **Expected Result**: Mouse hook is immediately active, cursor wraps
at screen edges without pressing hotkey
7. **Actual Result**:  Works as expected

#### Test Case 3: Setting persistence after restart

1. Set AutoActivate = false, restart PowerToys
2. Open Settings and verify AutoActivate is still false
3. Set AutoActivate = true, restart PowerToys
4. Open Settings and verify AutoActivate is still true
5. **Actual Result**:  Setting persists correctly

#### Test Case 4: Hotkey toggle works correctly

1. With AutoActivate = false, restart PowerToys
2. Press hotkey → cursor should start wrapping
3. Press hotkey again → cursor should stop wrapping
4. **Actual Result**:  Hotkey toggle works correctly

---

**Note**: Video demonstration available from contributor.
2026-02-05 20:34:15 +08:00
Mike Hall
ac548297c9 Add option to disable CursorWrap when on a single monitor. (#45303)
## Summary of the Pull Request
CursorWrap wraps on the outer edge of monitors, if a user is swapping
between a laptop and docked laptop with external monitors the user might
want to only enable wrapping when connected to external monitors, and
disable when only on the laptop.

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

- [ ] Closes: #45198
- [ ] Closes: #45154
- [ ] **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
Currently CursorWrap will wrap around the horizontal/vertical edges of
monitors, if the user has more than one monitor the outer edges are used
as wrap targets, if the user only has one monitor (perhaps a laptop)
wrapping might be temporarily disabled until additional external
monitors are added (such as being plugged into a dock or using a USB-C
monitor).

The new option will disable wrapping if only a single monitor is
detected, monitor detection is dynamic.

## Validation Steps Performed
Validated on a Surface Laptop 7 Pro (Intel) with a USB-C External
Monitor.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-02-05 20:33:49 +08:00
Shawn Yuan
17a215d321 Fix Advanced Paste settings page crash issue (#45207)
<!-- 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 pull request refactors the `AdvancedPasteAdditionalActions` class
to use private backing fields and custom property accessors for its
action properties. This change allows for better control over property
initialization and ensures that the properties always have valid,
non-null default values.

**Refactoring for property initialization and null safety:**

* Introduced private backing fields (`_imageToText`, `_pasteAsFile`,
`_transcode`) for the `ImageToText`, `PasteAsFile`, and `Transcode`
properties in `AdvancedPasteAdditionalActions`, replacing
auto-properties.
* Updated the property accessors for `ImageToText`, `PasteAsFile`, and
`Transcode` to use the new backing fields and ensure that a new default
instance is assigned if a null value is provided during initialization.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-05 10:37:31 +08:00
Jaylyn Barbee
1b6a8c54ff [Light Switch] Fix Light Switch start up logic (#45304)
<!-- 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
Title

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

- [x] Closes: https://github.com/microsoft/PowerToys/issues/45291
<!-- - [ ] 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

<!-- 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
Before, there was a function that initialized some variables about the
current system state that were later used to check against if that state
needed to change in a different function. That caused from some issues
because I was reusing the function for a double purpose. Now the
`SyncInitialThemeState()` function in the State Manager will sync those
initial variables and apply the correct theme if needed.

I also removed an unnecessary parameter from `onTick`

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manual testing
2026-02-05 10:29:42 +08:00
Kai Tao
67518dd754 Workspace: Fix an overlay issue for workspace snapshot draw (#45183)
<!-- 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
Root cause: Workspaces uses DPI-unaware coordinates (via
GetDpiUnawareScreens()
which runs in a temporary DPI-unaware thread) to store/match window
positions
across different DPI settings. However, WorkspacesEditor itself uses
PerMonitorV2
DPI awareness for UI clarity. When assigning these DPI-unaware
coordinates directly
to WPF window properties, WPF automatically scaled them again based on
current DPI,
causing incorrect overlay positioning.

Fix: Use SetWindowPositionDpiUnaware() to bypass WPF's automatic DPI
scaling
by temporarily switching to DPI-unaware context when calling Win32
SetWindowPos.

Fix #45174

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

- [ ] Closes: #45174
<!-- - [ ] 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
Verified in local build vs production build, and the problem fixed in
local build.
2026-02-05 10:29:36 +08:00
Kai Tao
18d1fd568c PowerToys extension: Bundle localization files into installer (#45194)
<!-- 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: #45171
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
<img width="925" height="612" alt="image"
src="https://github.com/user-attachments/assets/214ead95-504a-4e48-bc25-138323d973f9"
/>
2026-02-05 10:29:31 +08:00
leileizhang
3eae35f356 [ImageResizer] Fix Image Resizer not working after upgrade on Windows 10 (#45184)
<!-- 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 an issue where Image Resizer stops working after upgrading
PowerToys on Windows 10
- Root cause: the PackageIdentityMSIX (sparse app) was not being
properly cleaned up during upgrade

## Problem
Previous versions of PowerToys installed the sparse app on Windows 10.
The current version only installs it on Windows 11+ (build >= 22000).
During upgrade on Windows 10:
1. The `NOT UPGRADINGPRODUCTCODE` condition prevented the uninstall
action from running
2. The Windows 11 version check prevented the new sparse app from being
installed
3. Result: the old sparse app remained on the system, causing Image
Resizer to malfunction

## Fix
Changed the `UninstallPackageIdentityMSIX` condition from:
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
to:
Installed AND (REMOVE="ALL")

This ensures the old sparse app is properly cleaned up during upgrades,
which is also consistent with other similar cleanup

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

- [x] Closes: #45178 #45280
<!-- - [ ] 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
1. Install PowerToys version 0.96.1 on Windows 10.
2. Upgrade to version 0.97.1.
3. Run Get-AppxPackage -Name "*Sparse*" in PowerShell to check whether a
Sparse App package is present.
2026-02-05 10:29:25 +08:00
Niels Laute
e349779766 Fix contrast issue (#45367)
<!-- 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: #42261
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-05 10:29:19 +08:00
Mike Hall
5466ab6cf8 CursorWrap improvements (#44936)
## Summary of the Pull Request
- Updated engine for better multi-monitor support.
- Closing the laptop lid will now update the monitor topology
- New settings/dropdown to support wrapping on horizontal, vertical, or
both

<img width="1103" height="643" alt="image"
src="https://github.com/user-attachments/assets/ff4f0835-a8ca-4603-9441-123b71747d5c"
/>

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

- [x] Closes: #44820
- [x] Closes: #44864
- [x] Closes: #44952

- [ ] **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
Feedback for CursorWrap shows that users want the ability to constrain
wrapping for horizontal only, vertical only, or both (default behavior).
This PR adds a new dropdown to CursorWrap settings to enable a user to
select the appropriate wrapping model.

## Validation Steps Performed
Local build and running on Surface Laptop 7 Pro - will also validate on
a multi-monitor setup.

---------

Co-authored-by: vanzue <vanzue@outlook.com>
2026-01-27 13:28:31 +08:00
Heiko
bf19bdc1ee [Enterprise; Policy] Add policy for CursorWrap to ADMX (#45028)
<!-- 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

Added missing policy definition.

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

- [x] Closes: #44897
<!-- - [ ] 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)
- [x] **Documentation updated:** See PR for issue #44484 

<!-- 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-01-27 11:42:06 +08:00
Jiří Polášek
2441621b80 CmdPal: Remove deadlock bait from AppListItem (#45076)
## Summary of the Pull Request

This PR removes a Task.Wait() call from lazy-loading AppListItem details
that could be invoked on the UI thread and lead to a deadlock.

It now follows the same pattern previously used for loading icons in the
same class, which has proven to work well.

Prevents #44938 from stepping on this landmine.

Cherry-picked from #44973.

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

- [x] Closes: #45074 
<!-- - [ ] 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-01-27 10:36:42 +08:00
Shawn Yuan
1ca9d10ff5 [Settings] [Advanced Paste] Upgrade advanced paste settings safely to fix settings ui crash (#44862)
<!-- 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 pull request makes a minor fix in the `AdvancedPasteViewModel`
constructor to ensure the correct settings repository is used for null
checking. The change improves code correctness by verifying
`advancedPasteSettingsRepository` instead of the generic
`settingsRepository`.

- Fixed null check to use `advancedPasteSettingsRepository` instead of
`settingsRepository` in the `AdvancedPasteViewModel` constructor for
more accurate validation.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44835
<!-- - [ ] 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
- [ ] **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-01-26 16:06:19 +08:00
Gordon Lam
b438f15f6e [Peek] Fix Space key triggering during file rename (#44845) (#44995)
Don't show error window when CurrentItem is null - just return silently.
This restores the original behavior where CaretVisible() detection in
GetSelectedItems() would suppress Peek by returning null, and no window
would be shown.

PR #44703 added an error window for virtual folders (Home/Recent), but
this also triggered when user was typing (rename, search, address bar),
stealing focus and cancelling the operation.

Fixes #44845

<!-- 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
2026-01-26 16:06:12 +08:00
Shawn Yuan
48de981f50 Add telemetry for tray icon (#44985)
<!-- 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 pull request adds telemetry tracking for user interactions with the
application's tray icon. Specifically, it introduces new methods for
logging `left-click`, `right-click`, and `double-click` events, and
integrates these telemetry calls into the tray icon event handling
logic.

<!-- 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
- [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
- [ ] [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-01-26 16:06:06 +08:00
Shawn Yuan
9ab6559fac [Settings] Fix right click menu display issue (#44982)
<!-- 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 pull request updates the tray icon context menu logic to better
reflect the state of the "Quick Access" feature. The menu now
dynamically updates its items and labels based on whether Quick Access
is enabled or disabled, improving clarity for users.

**Menu behavior improvements:**

* The tray icon menu now reloads itself when the Quick Access setting
changes, ensuring the menu always matches the current state.
* The "Settings" menu item label changes to "Settings\tLeft-click" when
Quick Access is disabled, providing clearer instructions to users.
[[1]](diffhunk://#diff-e5efbda4c356e159a6ca82a425db84438ab4014d1d90377b98a2eb6d9632d32dR176-R179)
[[2]](diffhunk://#diff-7139ecb2cf76e472c574a155268c19e919e2cce05d9d345c50c1f1bffc939e1aR198-R248)
* The Quick Access menu item is removed from the context menu when the
feature is disabled, preventing confusion.

**Internal state tracking:**

* Added a new variable `last_quick_access_state` to track the previous
Quick Access state and trigger menu reloads only when necessary.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #44810
<!-- - [ ] 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
- When Quick Access is disabled

<img width="1537" height="312" alt="image"
src="https://github.com/user-attachments/assets/5d51f24e-ccb4-4973-afaa-8b64cc35db87"
/>

- When Quick Access is enabled
<img width="1601" height="201" alt="image"
src="https://github.com/user-attachments/assets/56366d10-bcec-4892-b2d2-f8213ad726aa"
/>

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-01-26 16:05:58 +08:00
moooyo
8cc32d3098 fix: Improve Unicode normalization and add regex metachar tests (#44944)
Enhanced SanitizeAndNormalize to handle Unicode normalization more
robustly, ensuring correct buffer sizing and error handling. Added unit
tests for regex metacharacters `$` and `^` to verify correct replacement
behavior at string boundaries. Improves Unicode support and test
coverage for regex edge cases.

<!-- 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: #44942 #44892
<!-- - [ ] 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 <yuleng@microsoft.com>
2026-01-26 16:05:54 +08:00
Jiří Polášek
3b7eedfb67 CmdPal: Improve loading of application icons - part 1 (#44938)
## Summary of the Pull Request

This PR improves loading of application icons:

- Fixes loading of icons from internet shortcuts

## Pictures? Pictures!

<img width="683" height="399" alt="image"
src="https://github.com/user-attachments/assets/5e566648-7b1a-4254-8afd-557a321b19d6"
/>


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

- [x] Closes: #44819
<!-- - [ ] 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-01-26 16:05:49 +08:00
Kai Tao
d7e1b18ba4 Runner TrayIcon: Monochrome icon should adapt to windows theme instead of the app theme (#44931)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
As title
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [X] Closes: #44891
<!-- - [ ] 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
============ System light + App Light
<img width="903" height="239" alt="image"
src="https://github.com/user-attachments/assets/581606fb-99b5-4df9-a520-545a0c04676c"
/>
============ System Light + App Dark
<img width="991" height="239" alt="image"
src="https://github.com/user-attachments/assets/822009e9-57cf-452b-b3aa-f1cbc25883f8"
/>
============ System Dark + App Light
<img width="932" height="236" alt="image"
src="https://github.com/user-attachments/assets/98a56d48-31f0-4f75-95a4-8c7dc83c3866"
/>
============ System Dark + App Dark
<img width="903" height="236" alt="image"
src="https://github.com/user-attachments/assets/2500a0d5-6b27-403e-89b4-69b7d3b91e79"
/>
============
2026-01-26 16:05:43 +08:00
Kai Tao
0206fdbec1 Cmdpal: use latest msix to install (#44886)
<!-- 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
We should install latest cmdpal msix
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #44860
<!-- - [ ] 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-01-26 16:05:36 +08:00
Leilei Zhang
bdaf644f02 test sign 2026-01-19 14:29:46 +08:00
moooyo
329c8c2616 refactor(imageresizer): disable AI feature and cache functionality (#44759)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

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

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

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

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2026-01-19 10:51:10 +08:00
Niels Laute
b081e413b1 Upgrade MarkdownTextBlock (#44793)
<!-- 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:

- upgrades `CommunityToolkit.WinUI.Labs.MarkdownTextBlock` to version
`0.1.260116-build.2514`. This update includes a bunch of improvements
for markdown rendering in its default config, and fixes a couple of bug
with regards to rendering large images getting clipped when resizing the
window.
- replaces an incorrect image in the `Command Palette Sample Page`
extension for the markdown + images sample.

Before vs after:

<img width="910" height="234" alt="image"
src="https://github.com/user-attachments/assets/b3dad76c-a89e-4b47-90f8-d3c64f00615f"
/>


<img width="1245" height="827" alt="image"
src="https://github.com/user-attachments/assets/00037fb5-453e-4d85-83c9-92c265b9f968"
/>



<!-- 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-01-19 09:56:30 +08:00
leileizhang
290fa01adf [ImageResizer] Temporarily disable AI Super Resolution feature (#44768)
<!-- 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
Temporarily disables the AI Super Resolution feature in Image Resizer
while keeping all code intact for re-enabling in a future release.

<!-- 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-01-17 11:28:47 +08:00
48 changed files with 271 additions and 2706 deletions

View File

@@ -16,6 +16,7 @@ adaptivecards
ADDSTRING
ADDUNDORECORD
ADifferent
ADMINS
adml
admx
advfirewall
@@ -169,7 +170,11 @@ cim
CImage
cla
CLASSDC
classguid
classmethod
CLASSNOTAVAILABLE
claude
CLEARTYPE
clickable
clickonce
clientside
@@ -201,6 +206,7 @@ colorformat
colorhistory
colorhistorylimit
COLORKEY
colorref
comctl
comdlg
comexp
@@ -218,6 +224,7 @@ CONTEXTHELP
CONTEXTMENUHANDLER
contractversion
CONTROLPARENT
Convs
cooldown
copiedcolorrepresentation
COPYPEN
@@ -229,6 +236,8 @@ cpcontrols
cph
cplusplus
CPower
cpptools
cppvsdbg
cppwinrt
createdump
CREATEPROCESS
@@ -251,6 +260,8 @@ CTLCOLORSTATIC
CURRENTDIR
CURSORINFO
cursorpos
CURSORSHOWING
cursorwrap
customaction
CUSTOMACTIONTEST
CVal
@@ -267,12 +278,14 @@ dacl
datareader
datatracker
Dayof
dbcc
DBID
DBLCLKS
DBLEPSILON
DBPROP
DBPROPIDSET
DBPROPSET
DBT
DCBA
DCOM
DComposition
@@ -288,6 +301,8 @@ DEFAULTFLAGS
DEFAULTICON
defaultlib
DEFAULTONLY
DEFAULTSIZE
defaulttonearest
DEFAULTTONULL
DEFAULTTOPRIMARY
DEFERERASE
@@ -307,11 +322,21 @@ DESKTOPABSOLUTEPARSING
desktopshorcutinstalled
devblogs
devdocs
devenv
DEVICEINTERFACE
devicetype
DEVINTERFACE
devmgmt
DEVMODE
DEVMODEW
DEVNODES
devpal
DEVTYP
dfx
DIALOGEX
diffs
digicert
DINORMAL
DISABLEASACTIONKEY
DISABLENOSCROLL
diskmgmt
@@ -425,6 +450,12 @@ eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FARPROC
fdw
fdx
FErase
fesf
FFFF
Figma
FILEEXPLORER
FILEFLAGS
FILEFLAGSMASK
@@ -441,6 +472,7 @@ FILESYSPATH
Filetime
FILEVERSION
FILTERMODE
FInc
findfast
FIXEDFILEINFO
FIXEDSYS
@@ -526,11 +558,13 @@ HCRYPTPROV
hcursor
hcwhite
hdc
HDEVNOTIFY
hdr
hdrop
hdwwiz
Helpline
helptext
hgdiobj
HGFE
hglobal
hhk
@@ -675,6 +709,7 @@ jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
jjw
jobject
JOBOBJECT
jpe
jpnime
Jsons
@@ -709,6 +744,7 @@ Ldone
Ldr
LEFTSCROLLBAR
LEFTTEXT
leftclick
LError
LEVELID
LExit
@@ -740,6 +776,8 @@ lowlevel
LOWORD
lparam
LPBITMAPINFOHEADER
LPCFHOOKPROC
lpch
LPCITEMIDLIST
LPCLSID
lpcmi
@@ -757,6 +795,7 @@ LPMONITORINFO
LPOSVERSIONINFOEXW
LPQUERY
lprc
LPrivate
LPSAFEARRAY
lpstr
lpsz
@@ -803,6 +842,8 @@ MAXSHORTCUTSIZE
maxversiontested
MBM
MBR
Mbuttondown
mcp
MDICHILD
MDL
mdtext
@@ -814,7 +855,10 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metacharacter
metadatamatters
Metadatas
Metacharacter
metafile
metapackage
mfc
@@ -841,6 +885,7 @@ mmsys
mobileredirect
mockapi
MODALFRAME
modelcontextprotocol
MODESPRUNED
MONITORENUMPROC
MONITORINFO
@@ -875,7 +920,9 @@ MSLLHOOKSTRUCT
Mso
msrc
msstore
mstsc
msvcp
MT
MTND
mtrue
MULTIPLEUSE
@@ -885,6 +932,8 @@ mvvm
MVVMTK
MWBEx
MYICON
myorg
myrepo
NAMECHANGE
namespaceanddescendants
nao
@@ -1009,6 +1058,7 @@ openas
opencode
OPENFILENAME
opensource
openurl
openxmlformats
OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE
@@ -1203,6 +1253,7 @@ RAWPATH
rbhid
rclsid
RCZOOMIT
rdp
RDW
READMODE
READOBJECTS
@@ -1230,6 +1281,7 @@ remappings
REMAPSUCCESSFUL
REMAPUNSUCCESSFUL
Remotable
remotedesktop
remoteip
Removelnk
renamable
@@ -1260,6 +1312,7 @@ RIGHTSCROLLBAR
riid
RKey
RNumber
rollups
rop
ROUNDSMALL
rpcrt
@@ -1471,6 +1524,9 @@ SVGIO
svgz
SVSI
SWFO
swp
SWPNOSIZE
SWPNOZORDER
SWRESTORE
symbolrequestprod
SYMCACHE
@@ -1487,6 +1543,8 @@ SYSKEY
syskeydown
SYSKEYUP
SYSLIB
sysmenu
systemai
SYSTEMAPPS
SYSTEMMODAL
SYSTEMTIME
@@ -1573,6 +1631,9 @@ UHash
UIA
UIEx
ULONGLONG
Ultrawide
UMax
UMin
ums
uncompilable
UNCPRIORITY

View File

@@ -26,7 +26,7 @@
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<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" />
@@ -40,7 +40,6 @@
<!-- 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="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />

View File

@@ -53,17 +53,17 @@ Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysUserSetup-0.97.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysUserSetup-0.97.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysSetup-0.97.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.1/PowerToysSetup-0.97.1-arm64.exe
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.2/PowerToysUserSetup-0.97.2-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.2/PowerToysUserSetup-0.97.2-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.2/PowerToysSetup-0.97.2-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.97.2/PowerToysSetup-0.97.2-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.97.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.97.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.97.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.97.1-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.97.2-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.97.2-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.97.2-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.97.2-arm64.exe][ptMachineArm64] |
</details>

View File

@@ -1,308 +0,0 @@
# Keyboard Manager CmdPal Integration
## Goal
Expose Keyboard Manager mappings in Command Palette (`cmdpal`) through `ext.powertoys` with two separate user experiences:
- quick actions for executable mappings
- an inspection list for all current mappings
This should be done without introducing a new settings schema or a CmdPal-specific Keyboard Manager settings file.
The first scope should cover:
- Expose `Run Program` remaps as invokable CmdPal actions
- Expose `Open URI` remaps as invokable CmdPal actions
- Add one Keyboard Manager `List all mappings` command item
- List all current mappings on a dedicated Keyboard Manager page
- Make the primary interaction for a mapping item be inspection of what that mapping does
## Current State
The repository already contains most of the plumbing needed for this integration:
1. Keyboard Manager publishes actions through the module action surface in `src/modules/keyboardmanager/dll/dllmain.cpp`.
2. Runner aggregates module actions through `src/runner/action_registry.cpp` and exposes them over the existing named pipe consumed by `RunnerActionClient`.
3. The PowerToys CmdPal extension enumerates module commands through `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/ModuleCommandCatalog.cs`.
4. Keyboard Manager already has a provider in `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs` that:
- shows the active-state toggle
- opens the new editor
- enumerates Runner actions with the `powertoys.keyboardManager.mapping.` prefix
- invokes them through `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/KeyboardManager/InvokeKeyboardManagerCustomActionCommand.cs`
At the Keyboard Manager layer, only remappings backed by executable actions are currently turned into invokable actions:
- `Shortcut::IsRunProgram()`
- `Shortcut::IsOpenURI()`
This is implemented in `is_keyboard_manager_custom_action`, `append_mapping_actions`, and `invoke_keyboard_manager_custom_action` in `src/modules/keyboardmanager/dll/dllmain.cpp`.
The repository also already has read-side Keyboard Manager mapping logic in `src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs`, but that code lives inside the editor UI project and is not an appropriate dependency for `ext.powertoys`.
## Lightweight Design
### Design Decision
Split the design into two surfaces:
1. Executable mapping actions
2. Mapping inspection
Executable mapping actions stay centered on the existing Runner action registry.
Mapping inspection should use a dedicated read-only Keyboard Manager query service shared with CmdPal, following the existing `*.ModuleServices` pattern used by other modules.
Do not add:
- a new CmdPal-only data file
- a direct JSON parse of Keyboard Manager settings in `ext.powertoys`
- a UI-project dependency from `ext.powertoys` to `KeyboardManagerEditorUI`
- CmdPal-specific logic in the Keyboard Manager editor
### Why This Is The Right Shape
This matches the way other PowerToys modules integrate with CmdPal:
- module owns its state and execution semantics
- executable actions are published through a small action surface
- richer read-only data can be exposed through a shared service layer when the module needs inspection or navigation
This keeps the startup path lean and avoids duplicating Keyboard Manager parsing logic in `ext.powertoys`.
## Proposed Functional Model
### Surface A: Executable Actions
Keyboard Manager remains the source of truth for which mappings are eligible for direct invocation in CmdPal.
When `get_actions()` is called:
1. Load the current `MappingConfiguration`
2. Enumerate OS-level shortcut remaps
3. Enumerate app-specific shortcut remaps
4. Keep only remaps whose target operation is:
- `Run Program`
- `Open URI`
5. Emit one Runner action descriptor per eligible remap
### Identity For Executable Actions
Each action id remains derived from the remap identity:
- source shortcut
- exact-match flag
- app scope
- target operation type
- target payload fields
The current implementation uses a hashed identity under the prefix `powertoys.keyboardManager.mapping.`. That is acceptable for a lightweight design because:
- CmdPal does not need stable ids across edits beyond the current session
- the action id is regenerated from source-of-truth settings
- action invocation already re-resolves the action against current config and fails safely if the mapping no longer exists
### Invocation Of Executable Actions
CmdPal invokes the selected item through `RunnerActionClient.InvokeAction(actionId)`.
Keyboard Manager stays responsible for:
- launching programs
- open-existing-instance behavior
- elevation mode
- start-in directory
- window visibility
- URI/path normalization and shell execution
This is important because CmdPal should not duplicate Keyboard Manager's execution semantics.
### Surface B: All Mappings Inspection
CmdPal also needs one dedicated Keyboard Manager entry for inspecting every current mapping, not just executable ones.
That entry should be a top-level module item such as:
- `List Keyboard Manager mappings`
Its command should open a dedicated `KeyboardManagerMappingsPage`.
### Data Source For All Mappings
The `KeyboardManagerMappingsPage` should not be backed by Runner actions because Runner actions currently model invokable operations only.
Instead, add a small shared Keyboard Manager query layer, ideally as a module service project, for example:
- `src/modules/keyboardmanager/KeyboardManager.ModuleServices`
That shared service should reuse the existing native mapping query path already used by the editor and expose normalized read-only DTOs for CmdPal consumption.
The service should cover all current mapping categories:
- single key to key
- single key to shortcut
- single key to text
- shortcut to shortcut
- shortcut to program
- shortcut to URI
- app-specific shortcut mappings
### Interaction Model For The Mappings Page
The mappings page should behave like an inspection page first, not an execution page first.
Recommended interaction:
1. `KeyboardManagerMappingsPage` is a `DynamicListPage` or `ListPage` with `ShowDetails = true`
2. Each mapping is rendered as a `ListItem` with rich `Details`
3. Selecting a mapping shows what it maps to
4. Invoking the item opens a small `KeyboardManagerMappingDetailsPage` or equivalent detail-focused page
5. Executable mappings may expose an extra command such as `Run now` or `Open now`, but that should not be the primary action on the inspection page
This satisfies the requirement that the primary action for a mapping entry is to show what the mapping is, while still leaving room for execution when the mapping type supports it.
### Presentation In CmdPal
Within `ext.powertoys`, the Keyboard Manager provider should emit these command groups:
1. Keyboard Manager state commands
- toggle active state
- open editor
2. Keyboard Manager inspection commands
- `List Keyboard Manager mappings`
3. Keyboard Manager quick actions
- `Run Program` entries
- `Open URI` entries
4. Keyboard Manager settings
- open settings
## UX Guidance
The minimum viable experience is:
- searchable by trigger or target
- clearly labeled as Keyboard Manager actions or mappings
- capable of both inspection and direct execution for supported mapping types
Recommended presentation rules:
1. The `List Keyboard Manager mappings` item should use the Keyboard Manager icon and clearly signal it opens a list, not an action.
2. Mapping list titles should prioritize the trigger:
- `Ctrl+Alt+N`
- `Caps Lock`
3. Mapping list subtitles should say what the trigger maps to:
- `Opens notepad.exe`
- `Maps to Ctrl+C`
- `Types Hello world`
4. Mapping details should carry the rest of the context:
- global vs app-specific
- mapping kind
- target payload
- execution-specific options when relevant
5. Quick-action titles can continue to prioritize the executable action:
- `Run notepad.exe`
- `Open https://contoso.com`
6. Keyboard Manager module icon is sufficient for the first version
The current implementation already covers the quick-action portion. The new work is primarily the all-mappings inspection surface.
## Non-Goals For The First Version
Do not add these in the initial pass:
- editing Keyboard Manager mappings from CmdPal
- enabling or disabling individual mappings from CmdPal
- live push notifications when mappings change
- custom icons per program or URI
- a new `kbm:` command syntax or dedicated parser
These all increase complexity without being necessary to validate the feature.
## Integration Pattern Compared To Other Modules
This feature now combines two existing CmdPal integration styles.
- `Workspaces` loads module-owned data and emits one command per data item
- `FancyZones` uses dedicated pages and details for richer inspection
Keyboard Manager quick actions should follow the lighter `Workspaces` pattern:
- one provider
- one flat list of dynamic items
- generic command invocation
Keyboard Manager all-mappings inspection should follow the `list page with details` pattern already supported by CmdPal:
- one top-level entry that opens a page
- one list item per mapping
- rich `Details` on every row
- optional secondary commands for invokable mappings
The main constraint is the same in both paths: `ext.powertoys` should not duplicate Keyboard Manager's mapping schema by parsing the settings file directly.
## Error Handling
The existing executable-action behavior is the correct baseline:
- hidden or deleted mappings simply disappear from `list_actions`
- stale CmdPal entries fail through `action_not_found`
- disabled Keyboard Manager returns `module_unavailable`
- launch failures return module-defined error messages
CmdPal only needs to surface the returned message as toast text for quick actions.
For the all-mappings inspection page:
- malformed or unreadable mapping snapshots should yield an empty page or an inline error item
- missing targets should still render as mappings, but be tagged as invalid or unavailable
- details rendering should degrade gracefully when optional fields are absent
## Risks
### Two Data Paths
This design intentionally uses two integration paths:
- Runner actions for invokable mappings
- a shared read-only query service for all mappings
That is acceptable because the two paths serve different UX needs. The risk is manageable as long as both paths derive from the same Keyboard Manager mapping model rather than separate ad hoc parsers.
### Duplicate Or Ambiguous Entries
Different mappings may produce similar titles, especially on the quick-action side. This is acceptable in the first iteration because the subtitle already carries scope and trigger details.
### Action Id Churn After Edits
Editing a mapping changes the derived id. This is acceptable because action ids are not a persisted public contract.
### Large Mapping Sets
Very large mapping sets could make the inspection page noisy. This is manageable for the first version if the page supports search and details, but sectioning or filters may be needed later.
## Minimal Implementation Plan
1. Keep Keyboard Manager as the producer of invokable mapping actions.
2. Keep Runner action registry as the discovery path for executable quick actions.
3. Add a small shared read-only Keyboard Manager module service for enumerating all mappings.
4. Add `List Keyboard Manager mappings` to `KeyboardManagerModuleCommandProvider`.
5. Add a `KeyboardManagerMappingsPage` with `ShowDetails = true`.
6. Represent each mapping as an inspection-first `ListItem` with rich `Details`.
7. Add optional secondary execution commands only for mappings that are invokable.
8. Add focused tests around:
- Keyboard Manager action enumeration for `Run Program` and `Open URI`
- mapping snapshot enumeration across all mapping kinds
- CmdPal rendering of the mappings page
- graceful handling of stale, invalid, or missing mappings
## Future Extensions
If the first version lands well, the next step should still preserve the same split architecture:
- enrich action descriptors with more metadata if Runner actions grow argument or icon support
- add sections or filters to the mappings page when the list becomes large
- optionally expose app-specific filtering in CmdPal UI
The extension points should remain:
- Runner actions for execution
- a shared Keyboard Manager query service for inspection

View File

@@ -3,18 +3,28 @@
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define KeyboardManagerAssetsFiles=?>
<?define KeyboardManagerAssetsWinUI3Files=?>
<?define KeyboardManagerAssetsFilesPath=$(var.BinDir)\Assets\KeyboardManager\?>
<?define KeyboardManagerAssetsWinUI3FilesPath=$(var.BinDir)\WinUI3Apps\Assets\KeyboardManagerEditor\?>
<Fragment>
<DirectoryRef Id="BaseApplicationsAssetsFolder">
<Directory Id="KeyboardManagerAssetsInstallFolder" Name="KeyboardManager" />
</DirectoryRef>
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<Directory Id="KeyboardManagerAssetsWinUI3InstallFolder" Name="KeyboardManagerEditor" />
</DirectoryRef>
<DirectoryRef Id="KeyboardManagerAssetsInstallFolder" FileSource="$(var.KeyboardManagerAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--KeyboardManagerAssetsFiles_Component_Def-->
</DirectoryRef>
<DirectoryRef Id="KeyboardManagerAssetsWinUI3InstallFolder" FileSource="$(var.KeyboardManagerAssetsWinUI3FilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--KeyboardManagerAssetsWinUI3Files_Component_Def-->
</DirectoryRef>
<DirectoryRef Id="INSTALLFOLDER">
<Directory Id="KeyboardManagerEditorInstallFolder" Name="KeyboardManagerEditor" />
<Directory Id="KeyboardManagerEngineInstallFolder" Name="KeyboardManagerEngine" />
@@ -57,6 +67,7 @@
<RegistryValue Type="string" Name="RemoveKeyboardManagerFolder" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveFolderKeyboardManagerAssetsInstallFolder" Directory="KeyboardManagerAssetsInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderKeyboardManagerAssetsWinUI3InstallFolder" Directory="KeyboardManagerAssetsWinUI3InstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderKeyboardManagerEditorFolder" Directory="KeyboardManagerEditorInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderKeyboardManagerEngineFolder" Directory="KeyboardManagerEngineInstallFolder" On="uninstall" />
</Component>

View File

@@ -174,7 +174,9 @@ Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PS
#KeyboardManager
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsFiles -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\KeyboardManager"
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsWinUI3Files -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\KeyboardManagerEditor"
Generate-FileComponents -fileListName "KeyboardManagerAssetsFiles" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs
Generate-FileComponents -fileListName "KeyboardManagerAssetsWinUI3Files" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs
# Light Switch Service
Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"

View File

@@ -1,193 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using PowerToys.Interop;
namespace ManagedCommon
{
public sealed class RunnerActionClient
{
public IReadOnlyList<RunnerActionDescriptor> ListActions()
{
var response = SendRequest("list_actions", string.Empty, "{}");
return response.Success && response.Actions.Count > 0 ? response.Actions : Array.Empty<RunnerActionDescriptor>();
}
public RunnerActionInvokeResult InvokeAction(string actionId, string serializedArguments = "{}")
{
if (string.IsNullOrWhiteSpace(actionId))
{
return new RunnerActionInvokeResult
{
Success = false,
ErrorCode = "invalid_action_id",
Message = "Action id is required.",
};
}
var response = SendRequest("invoke_action", actionId, string.IsNullOrWhiteSpace(serializedArguments) ? "{}" : serializedArguments);
return new RunnerActionInvokeResult
{
Success = response.Success,
ErrorCode = response.ErrorCode,
Message = response.Message,
};
}
private static RunnerActionResponse SendRequest(string requestType, string actionId, string arguments)
{
var pipeName = Path.GetFileName(Constants.PowerToysActionsPipe());
using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None);
pipe.Connect(2000);
var payload = BuildRequestPayload(requestType, actionId, arguments);
var lengthBuffer = BitConverter.GetBytes(payload.Length);
pipe.Write(lengthBuffer, 0, lengthBuffer.Length);
pipe.Write(payload, 0, payload.Length);
pipe.Flush();
var responseLengthBuffer = ReadExact(pipe, sizeof(int));
var responseLength = BitConverter.ToInt32(responseLengthBuffer, 0);
var responsePayload = responseLength == 0 ? Array.Empty<byte>() : ReadExact(pipe, responseLength);
return ParseResponse(responsePayload);
}
private static byte[] BuildRequestPayload(string requestType, string actionId, string arguments)
{
using var stream = new MemoryStream();
using (var writer = new Utf8JsonWriter(stream))
{
writer.WriteStartObject();
writer.WriteString("type", requestType);
if (!string.IsNullOrWhiteSpace(actionId))
{
writer.WriteString("action_id", actionId);
}
writer.WriteString("arguments", arguments);
writer.WriteEndObject();
}
return stream.ToArray();
}
private static RunnerActionResponse ParseResponse(byte[] payload)
{
if (payload.Length == 0)
{
return RunnerActionResponse.CreateError("empty_response", "Runner returned an empty response.");
}
using var document = JsonDocument.Parse(payload);
var root = document.RootElement;
var response = new RunnerActionResponse();
if (root.TryGetProperty("success", out var successElement) &&
(successElement.ValueKind == JsonValueKind.True || successElement.ValueKind == JsonValueKind.False))
{
response.Success = successElement.GetBoolean();
}
if (root.TryGetProperty("error_code", out var errorCodeElement) && errorCodeElement.ValueKind == JsonValueKind.String)
{
response.ErrorCode = errorCodeElement.GetString() ?? string.Empty;
}
if (root.TryGetProperty("message", out var messageElement) && messageElement.ValueKind == JsonValueKind.String)
{
response.Message = messageElement.GetString() ?? string.Empty;
}
if (root.TryGetProperty("actions", out var actionsElement) && actionsElement.ValueKind == JsonValueKind.Array)
{
response.Actions = ParseActions(actionsElement);
}
return response;
}
private static List<RunnerActionDescriptor> ParseActions(JsonElement actionsElement)
{
var actions = new List<RunnerActionDescriptor>();
foreach (var element in actionsElement.EnumerateArray())
{
if (element.ValueKind != JsonValueKind.Object)
{
continue;
}
actions.Add(new RunnerActionDescriptor
{
ActionId = GetStringProperty(element, "action_id"),
ModuleKey = GetStringProperty(element, "module_key"),
DisplayName = GetStringProperty(element, "display_name"),
Description = GetStringProperty(element, "description"),
Category = GetStringProperty(element, "category"),
Available = GetBoolProperty(element, "available"),
});
}
return actions;
}
private static string GetStringProperty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.String
? property.GetString() ?? string.Empty
: string.Empty;
}
private static bool GetBoolProperty(JsonElement element, string propertyName)
{
return element.TryGetProperty(propertyName, out var property) &&
(property.ValueKind == JsonValueKind.True || property.ValueKind == JsonValueKind.False) &&
property.GetBoolean();
}
private static byte[] ReadExact(Stream stream, int length)
{
var buffer = new byte[length];
var offset = 0;
while (offset < length)
{
var bytesRead = stream.Read(buffer, offset, length - offset);
if (bytesRead == 0)
{
throw new EndOfStreamException("Unexpected end of stream while reading runner action response.");
}
offset += bytesRead;
}
return buffer;
}
private sealed class RunnerActionResponse
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public List<RunnerActionDescriptor> Actions { get; set; } = new();
public static RunnerActionResponse CreateError(string errorCode, string message)
{
return new RunnerActionResponse
{
Success = false,
ErrorCode = errorCode,
Message = message,
};
}
}
}
}

View File

@@ -1,21 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ManagedCommon
{
public sealed class RunnerActionDescriptor
{
public string ActionId { get; set; } = string.Empty;
public string ModuleKey { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public bool Available { get; set; }
}
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ManagedCommon
{
public static class RunnerActionIds
{
public const string KeyboardManagerToggleActive = "powertoys.keyboardManager.toggleActive";
public const string KeyboardManagerOpenEditor = "powertoys.keyboardManager.openEditor";
}
}

View File

@@ -1,15 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace ManagedCommon
{
public sealed class RunnerActionInvokeResult
{
public bool Success { get; set; }
public string ErrorCode { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
}

View File

@@ -308,9 +308,5 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX;
}
hstring Constants::PowerToysActionsPipe()
{
return CommonSharedConstants::POWERTOYS_ACTIONS_PIPE;
}
}

View File

@@ -80,7 +80,6 @@ namespace winrt::PowerToys::Interop::implementation
static hstring OpenNewKeyboardManagerEvent();
static hstring ToggleKeyboardManagerActiveEvent();
static hstring KeyboardManagerEngineInstanceMutex();
static hstring PowerToysActionsPipe();
};
}

View File

@@ -77,7 +77,6 @@ namespace PowerToys
static String OpenNewKeyboardManagerEvent();
static String ToggleKeyboardManagerActiveEvent();
static String KeyboardManagerEngineInstanceMutex();
static String PowerToysActionsPipe();
}
}
}

View File

@@ -174,7 +174,6 @@ namespace CommonSharedConstants
const wchar_t OPEN_NEW_KEYBOARD_MANAGER_EVENT[] = L"Local\\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f";
const wchar_t TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT[] = L"Local\\PowerToysToggleKeyboardManagerActiveEvent-7f3a1d5c-2e94-4ff4-8b6a-90fd2bc4d2a7";
const wchar_t KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX[] = L"Local\\PowerToys_KBMEngine_InstanceMutex";
const wchar_t POWERTOYS_ACTIONS_PIPE[] = L"\\\\.\\pipe\\PowerToysActionsPipe-e98c2e3d-52ab-4f9b-a65b-5b9bb6f0e312";
// used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";

View File

@@ -14,7 +14,6 @@ void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
Logger::info(L"======= TOPOLOGY INITIALIZATION START =======");
Logger::info(L"Initializing edge-based topology for {} monitors", monitors.size());
m_monitors = monitors;
m_outerEdges.clear();
m_edgeMap.clear();
@@ -692,7 +691,6 @@ int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativ
return static_cast<int>(result);
}
std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const
{
std::vector<GapInfo> gaps;

View File

@@ -94,6 +94,7 @@ public sealed partial class MainWindow : WindowEx,
private WindowPosition _currentWindowPosition = new();
private bool _preventHideWhenDeactivated;
private bool _isLoadedFromDock;
private DevRibbon? _devRibbon;
@@ -142,7 +143,7 @@ public sealed partial class MainWindow : WindowEx,
this.SetIcon();
AppWindow.Title = RS_.GetString("AppName");
RestoreWindowPosition();
RestoreWindowPositionFromSavedSettings();
UpdateWindowPositionInMemory();
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
@@ -245,10 +246,26 @@ public sealed partial class MainWindow : WindowEx,
private void PositionCentered(DisplayArea displayArea)
{
// Use the saved window size when available so that a dock-resized HWND
// (hidden but not destroyed) doesn't dictate the size on normal reopen.
SizeInt32 windowSize;
int windowDpi;
if (_currentWindowPosition.IsSizeValid)
{
windowSize = new SizeInt32(_currentWindowPosition.Width, _currentWindowPosition.Height);
windowDpi = _currentWindowPosition.Dpi;
}
else
{
windowSize = AppWindow.Size;
windowDpi = (int)this.GetDpiForWindow();
}
var rect = WindowPositionHelper.CenterOnDisplay(
displayArea,
AppWindow.Size,
(int)this.GetDpiForWindow());
displayArea,
windowSize,
windowDpi);
if (rect is not null)
{
@@ -256,10 +273,9 @@ public sealed partial class MainWindow : WindowEx,
}
}
private void RestoreWindowPosition()
private void RestoreWindowPosition(WindowPosition? savedPosition)
{
var settings = App.Current.Services.GetService<SettingsModel>();
if (settings?.LastWindowPosition is not { Width: > 0, Height: > 0 } savedPosition)
if (savedPosition?.IsSizeValid != true)
{
// don't try to restore if the saved position is invalid, just recenter
PositionCentered();
@@ -274,6 +290,17 @@ public sealed partial class MainWindow : WindowEx,
MoveAndResizeDpiAware(newRect);
}
private void RestoreWindowPositionFromSavedSettings()
{
var settings = App.Current.Services.GetService<SettingsModel>();
RestoreWindowPosition(settings?.LastWindowPosition);
}
private void RestoreWindowPositionFromMemory()
{
RestoreWindowPosition(_currentWindowPosition);
}
/// <summary>
/// Moves and resizes the window while suppressing WM_DPICHANGED.
/// The caller is expected to provide a rect already scaled for the target display's DPI.
@@ -678,6 +705,8 @@ public sealed partial class MainWindow : WindowEx,
public void Receive(ShowWindowMessage message)
{
_isLoadedFromDock = false;
var settings = App.Current.Services.GetService<SettingsModel>()!;
// Start session tracking
@@ -690,6 +719,13 @@ public sealed partial class MainWindow : WindowEx,
internal void Receive(ShowPaletteAtMessage message)
{
_isLoadedFromDock = true;
// Reset the size in case users have resized a dock window.
// Ideally in the future, we'll have defined sizes that opening
// a dock window will adhere to, but alas, that's the future.
RestoreWindowPositionFromMemory();
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
}
@@ -860,12 +896,17 @@ public sealed partial class MainWindow : WindowEx,
internal void MainWindow_Closed(object sender, WindowEventArgs args)
{
var serviceProvider = App.Current.Services;
UpdateWindowPositionInMemory();
if (!_isLoadedFromDock)
{
UpdateWindowPositionInMemory();
}
var settings = serviceProvider.GetService<SettingsModel>();
if (settings is not null)
{
// a quick sanity check, so we don't overwrite correct values
// If we were last shown from the dock, _currentWindowPosition still holds
// the last non-dock placement because dock sessions intentionally skip updates.
if (_currentWindowPosition.IsSizeValid)
{
settings.LastWindowPosition = _currentWindowPosition;
@@ -960,7 +1001,11 @@ public sealed partial class MainWindow : WindowEx,
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
// Save the current window position before hiding the window
UpdateWindowPositionInMemory();
// but not when opened from dock — preserve the pre-dock size.
if (!_isLoadedFromDock)
{
UpdateWindowPositionInMemory();
}
// If there's a debugger attached...
if (System.Diagnostics.Debugger.IsAttached)

View File

@@ -19,7 +19,6 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem
{
private readonly AppCommand _appCommand;
private readonly AppItem _app;
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
private readonly Lazy<Task<Details>> _detailsLoadTask;

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -70,6 +71,7 @@ internal abstract partial class OnLoadContentPage : OnLoadBasePage, IContentPage
internal abstract partial class OnLoadBasePage : Page
{
private readonly Lock _loadLock = new();
private int _loadCount;
#pragma warning disable CS0067 // The event is never used
@@ -82,22 +84,28 @@ internal abstract partial class OnLoadBasePage : Page
add
{
InternalItemsChanged += value;
if (_loadCount == 0)
lock (_loadLock)
{
Loaded();
}
if (_loadCount == 0)
{
Loaded();
}
_loadCount++;
_loadCount++;
}
}
remove
{
InternalItemsChanged -= value;
_loadCount--;
_loadCount = Math.Max(0, _loadCount);
if (_loadCount == 0)
lock (_loadLock)
{
Unloaded();
_loadCount--;
_loadCount = Math.Max(0, _loadCount);
if (_loadCount == 0)
{
Unloaded();
}
}
}
}

View File

@@ -9,6 +9,7 @@ using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json.Nodes;
using System.Threading;
using CoreWidgetProvider.Helpers;
using CoreWidgetProvider.Widgets.Enums;
using Microsoft.CmdPal.Common;
@@ -262,17 +263,17 @@ internal abstract partial class WidgetPage : OnLoadContentPage
/// </summary>
internal virtual void PushActivate()
{
_loadCount++;
Interlocked.Increment(ref _loadCount);
}
internal virtual void PopActivate()
{
_loadCount--;
Interlocked.Decrement(ref _loadCount);
}
private int _loadCount;
protected bool IsActive => _loadCount > 0;
protected bool IsActive => Volatile.Read(ref _loadCount) > 0;
protected override void Loaded()
{

View File

@@ -1,38 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace PowerToysExtension.Commands;
internal sealed partial class InvokeKeyboardManagerCustomActionCommand : InvokableCommand
{
private static readonly RunnerActionClient ActionClient = new();
private readonly string _actionId;
private readonly string _fallbackError;
public InvokeKeyboardManagerCustomActionCommand(string actionId, string displayName)
{
_actionId = actionId;
_fallbackError = $"Failed to invoke {displayName}.";
Name = displayName;
}
public override CommandResult Invoke()
{
try
{
var result = ActionClient.InvokeAction(_actionId);
return result.Success
? CommandResult.Dismiss()
: CommandResult.ShowToast(string.IsNullOrWhiteSpace(result.Message) ? _fallbackError : result.Message);
}
catch (Exception ex)
{
return CommandResult.ShowToast($"{_fallbackError} {ex.Message}");
}
}
}

View File

@@ -1,71 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
namespace PowerToysExtension.Commands;
internal sealed partial class KeyboardManagerMappingListItem : ListItem
{
public KeyboardManagerMappingListItem(KeyboardManagerMappingRecord mapping, IconInfo icon)
: base(new CommandItem(new KeyboardManagerMappingDetailsPage(mapping, icon)))
{
Title = mapping.TriggerDisplay;
Subtitle = mapping.Subtitle;
Icon = icon;
Details = BuildDetails(mapping, icon);
}
private static Details BuildDetails(KeyboardManagerMappingRecord mapping, IconInfo icon)
{
var metadata = new List<IDetailsElement>
{
DetailText("Type", mapping.Kind.ToString()),
DetailText("Target", mapping.TargetDisplay),
DetailText("Scope", mapping.IsAppSpecific ? $"App-specific ({mapping.TargetApp})" : "Global"),
};
if (!string.IsNullOrWhiteSpace(mapping.ProgramArgs))
{
metadata.Add(DetailText("Args", mapping.ProgramArgs));
}
if (!string.IsNullOrWhiteSpace(mapping.StartInDirectory))
{
metadata.Add(DetailText("Start in", mapping.StartInDirectory));
}
if (!string.IsNullOrWhiteSpace(mapping.TargetText))
{
metadata.Add(DetailText("Text", mapping.TargetText));
}
if (!string.IsNullOrWhiteSpace(mapping.UriToOpen))
{
metadata.Add(DetailText("URI", mapping.UriToOpen));
}
return new Details
{
HeroImage = icon,
Title = mapping.TriggerDisplay,
Body = mapping.Subtitle,
Metadata = metadata.ToArray(),
};
}
private static DetailsElement DetailText(string key, string value)
{
return new DetailsElement
{
Key = key,
Data = new DetailsLink { Text = value },
};
}
}

View File

@@ -3,8 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using System.Threading;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop;
namespace PowerToysExtension.Commands;
@@ -13,8 +14,6 @@ namespace PowerToysExtension.Commands;
/// </summary>
internal sealed partial class OpenNewKeyboardManagerEditorCommand : InvokableCommand
{
private static readonly RunnerActionClient ActionClient = new();
public OpenNewKeyboardManagerEditorCommand()
{
Name = "Open New Keyboard Manager Editor";
@@ -24,10 +23,9 @@ internal sealed partial class OpenNewKeyboardManagerEditorCommand : InvokableCom
{
try
{
var result = ActionClient.InvokeAction(RunnerActionIds.KeyboardManagerOpenEditor);
return result.Success
? CommandResult.Dismiss()
: CommandResult.ShowToast(string.IsNullOrWhiteSpace(result.Message) ? "Failed to open New Keyboard Manager Editor." : result.Message);
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.OpenNewKeyboardManagerEvent());
evt.Set();
return CommandResult.Dismiss();
}
catch (Exception ex)
{

View File

@@ -4,7 +4,6 @@
using System;
using System.Threading;
using ManagedCommon;
using PowerToys.Interop;
namespace PowerToysExtension.Helpers;
@@ -13,7 +12,6 @@ internal static class KeyboardManagerStateService
{
private static readonly object Sync = new();
private static readonly Timer PollingTimer;
private static readonly RunnerActionClient ActionClient = new();
private static bool _lastKnownListeningState = IsListening();
internal static event Action? StatusChanged;
@@ -49,9 +47,10 @@ internal static class KeyboardManagerStateService
{
try
{
var result = ActionClient.InvokeAction(RunnerActionIds.KeyboardManagerToggleActive);
using var evt = EventWaitHandle.OpenExisting(Constants.ToggleKeyboardManagerActiveEvent());
var signaled = evt.Set();
PollStatus();
return result.Success;
return signaled;
}
catch
{

View File

@@ -38,7 +38,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.CommandPalette.Extensions" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
@@ -56,6 +55,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\..\common\Common.Search\Common.Search.csproj" />
<ProjectReference Include="..\..\..\..\common\Common.UI\Common.UI.csproj" />
@@ -63,7 +63,6 @@
<ProjectReference Include="..\..\..\Awake\Awake.ModuleServices\Awake.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\colorPicker\ColorPicker.ModuleServices\ColorPicker.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj" />
<ProjectReference Include="..\..\..\keyboardmanager\KeyboardManager.ModuleServices\KeyboardManager.ModuleServices.csproj" />
<ProjectReference Include="..\..\..\Workspaces\Workspaces.ModuleServices\Workspaces.ModuleServices.csproj" />
</ItemGroup>

View File

@@ -5,13 +5,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using PowerToysExtension.Properties;
using static Common.UI.SettingsDeepLink;
@@ -19,9 +16,6 @@ namespace PowerToysExtension.Modules;
internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvider
{
private const string KeyboardManagerMappingActionPrefix = "powertoys.keyboardManager.mapping.";
private static readonly RunnerActionClient ActionClient = new();
public override IEnumerable<ListItem> BuildCommands()
{
var module = SettingsWindow.KBM;
@@ -39,16 +33,9 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
: GetResourceString("KeyboardManager_ToggleListening_Off_Subtitle", "Keyboard Manager is paused. Invoke to start listening."),
Icon = PowerToysResourcesHelper.KeyboardManagerListeningIcon(isListening),
};
yield return new ListItem(new CommandItem(new KeyboardManagerMappingsPage() { Id = "com.microsoft.powertoys.keyboardManager.mappings" }))
{
Title = "List Keyboard Manager mappings",
Subtitle = "Inspect current remaps and shortcuts from Keyboard Manager.",
Icon = icon,
};
}
if (ModuleEnablementService.IsModuleEnabled(module) && IsUseNewEditorEnabled())
if (IsUseNewEditorEnabled())
{
yield return new ListItem(new OpenNewKeyboardManagerEditorCommand())
{
@@ -58,19 +45,6 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
};
}
if (ModuleEnablementService.IsModuleEnabled(module))
{
foreach (var action in ListExecutableMappingActions())
{
yield return new ListItem(new InvokeKeyboardManagerCustomActionCommand(action.ActionId, action.DisplayName) { Id = $"com.microsoft.powertoys.keyboardManager.action.{action.ActionId}" })
{
Title = action.DisplayName,
Subtitle = string.IsNullOrWhiteSpace(action.Description) ? "Invoke a Keyboard Manager custom action." : action.Description,
Icon = icon,
};
}
}
yield return new ListItem(new OpenInSettingsCommand(module, title) { Id = "com.microsoft.powertoys.keyboardManager.openSettings" })
{
Title = title,
@@ -84,21 +58,6 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
return Resources.ResourceManager.GetString(resourceName, Resources.Culture) ?? fallback;
}
private static IEnumerable<RunnerActionDescriptor> ListExecutableMappingActions()
{
try
{
return ActionClient.ListActions()
.Where(action => action.Available && action.ActionId.StartsWith(KeyboardManagerMappingActionPrefix, StringComparison.Ordinal))
.OrderBy(action => action.DisplayName, StringComparer.CurrentCultureIgnoreCase)
.ToArray();
}
catch
{
return Array.Empty<RunnerActionDescriptor>();
}
}
private static bool IsUseNewEditorEnabled()
{
try

View File

@@ -1,94 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace PowerToysExtension.Pages;
internal sealed partial class KeyboardManagerMappingDetailsPage : ContentPage
{
private readonly KeyboardManagerMappingRecord _mapping;
public KeyboardManagerMappingDetailsPage(KeyboardManagerMappingRecord mapping, IconInfo icon)
{
_mapping = mapping;
Icon = icon;
Name = mapping.TriggerDisplay;
Details = new Details
{
HeroImage = icon,
Title = mapping.TriggerDisplay,
Body = mapping.Subtitle,
Metadata = BuildMetadata(mapping),
};
}
public override IContent[] GetContent()
{
return
[
new MarkdownContent(
$"""
# {EscapeMarkdown(_mapping.TriggerDisplay)}
{EscapeMarkdown(_mapping.Subtitle)}
"""),
];
}
private static IDetailsElement[] BuildMetadata(KeyboardManagerMappingRecord mapping)
{
var metadata = new List<IDetailsElement>
{
DetailText("Type", mapping.Kind.ToString()),
DetailText("Scope", mapping.IsAppSpecific ? $"App-specific ({mapping.TargetApp})" : "Global"),
DetailText("Target", mapping.TargetDisplay),
};
if (!string.IsNullOrWhiteSpace(mapping.ProgramPath))
{
metadata.Add(DetailText("Program", mapping.ProgramPath));
}
if (!string.IsNullOrWhiteSpace(mapping.ProgramArgs))
{
metadata.Add(DetailText("Args", mapping.ProgramArgs));
}
if (!string.IsNullOrWhiteSpace(mapping.StartInDirectory))
{
metadata.Add(DetailText("Start in", mapping.StartInDirectory));
}
if (!string.IsNullOrWhiteSpace(mapping.UriToOpen))
{
metadata.Add(DetailText("URI", mapping.UriToOpen));
}
if (!string.IsNullOrWhiteSpace(mapping.TargetText))
{
metadata.Add(DetailText("Text", mapping.TargetText));
}
return metadata.ToArray();
}
private static DetailsElement DetailText(string key, string value)
{
return new DetailsElement
{
Key = key,
Data = new DetailsLink { Text = value },
};
}
private static string EscapeMarkdown(string value)
{
return value.Replace("\\", "\\\\").Replace("*", "\\*").Replace("_", "\\_");
}
}

View File

@@ -1,66 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using KeyboardManager.ModuleServices;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
namespace PowerToysExtension.Pages;
internal sealed partial class KeyboardManagerMappingsPage : DynamicListPage
{
private readonly CommandItem _emptyMessage;
private readonly IconInfo _icon;
public KeyboardManagerMappingsPage()
{
_icon = PowerToysResourcesHelper.IconFromSettingsIcon("KeyboardManager.png");
Icon = _icon;
Name = Title = "Keyboard Manager mappings";
Id = "com.microsoft.cmdpal.powertoys.keyboardManager.mappings";
ShowDetails = true;
_emptyMessage = new CommandItem
{
Title = "No Keyboard Manager mappings found",
Subtitle = "Create mappings in Keyboard Manager to inspect them here.",
Icon = _icon,
};
EmptyContent = _emptyMessage;
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
RaiseItemsChanged(0);
}
public override IListItem[] GetItems()
{
var result = KeyboardManagerMappingService.Instance.GetMappingsAsync().GetAwaiter().GetResult();
if (!result.Success || result.Value is null)
{
_emptyMessage.Subtitle = result.Error ?? "Failed to read Keyboard Manager mappings.";
return Array.Empty<IListItem>();
}
IEnumerable<KeyboardManagerMappingRecord> mappings = result.Value;
if (!string.IsNullOrWhiteSpace(SearchText))
{
mappings = mappings.Where(mapping =>
mapping.TriggerDisplay.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.TargetDisplay.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.Subtitle.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase) ||
mapping.TargetApp.Contains(SearchText, StringComparison.CurrentCultureIgnoreCase));
}
return mappings
.OrderBy(mapping => mapping.TriggerDisplay, StringComparer.CurrentCultureIgnoreCase)
.Select(mapping => (IListItem)new KeyboardManagerMappingListItem(mapping, _icon))
.ToArray();
}
}

View File

@@ -1,8 +1,7 @@
#pragma once
#include <compare>
#include <cwchar>
#include <common/utils/gpo.h>
#pragma once
#include <compare>
#include <common/utils/gpo.h>
/*
DLL Interface for PowerToys. The powertoy_create() (see below) must return
@@ -98,53 +97,10 @@ public:
virtual bool get_config(wchar_t* buffer, int* buffer_size) = 0;
/* Sets the configuration values. */
virtual void set_config(const wchar_t* config) = 0;
/* Call custom action from settings screen. */
virtual void call_custom_action(const wchar_t* /*action*/){};
/* Returns the actions exposed by the module as a JSON array. */
virtual bool get_actions(wchar_t* buffer, int* buffer_size)
{
constexpr const wchar_t empty_actions[] = L"[]";
if (!buffer_size)
{
return false;
}
const int required_size = static_cast<int>(sizeof(empty_actions) / sizeof(empty_actions[0]));
if (!buffer || *buffer_size < required_size)
{
*buffer_size = required_size;
return false;
}
wcscpy_s(buffer, *buffer_size, empty_actions);
return true;
}
/* Invokes an action exposed by the module and returns a JSON object result. */
virtual bool invoke_action(const wchar_t* /*action_id*/, const wchar_t* /*serialized_args*/, wchar_t* buffer, int* buffer_size)
{
constexpr const wchar_t unsupported_result[] =
L"{\"success\":false,\"error_code\":\"not_supported\",\"message\":\"This module does not expose actions.\"}";
if (!buffer_size)
{
return false;
}
const int required_size = static_cast<int>(sizeof(unsupported_result) / sizeof(unsupported_result[0]));
if (!buffer || *buffer_size < required_size)
{
*buffer_size = required_size;
return false;
}
wcscpy_s(buffer, *buffer_size, unsupported_result);
return true;
}
/* Enables the PowerToy. */
virtual void enable() = 0;
/* Call custom action from settings screen. */
virtual void call_custom_action(const wchar_t* /*action*/){};
/* Enables the PowerToy. */
virtual void enable() = 0;
/* Disables the PowerToy, should free as much memory as possible. */
virtual void disable() = 0;
/* Should return if the PowerToys is enabled or disabled. */

View File

@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<Import Project="$(RepoRoot)src\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\PowerToys.ModuleContracts\PowerToys.ModuleContracts.csproj" />
<ProjectReference Include="..\KeyboardManagerEditorLibraryWrapper\KeyboardManagerEditorLibraryWrapper.vcxproj" />
</ItemGroup>
</Project>

View File

@@ -1,96 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using System.Text;
namespace KeyboardManager.ModuleServices;
internal static class KeyboardManagerInterop
{
private const string DllName = "Powertoys.KeyboardManagerEditorLibraryWrapper.dll";
private const CallingConvention Convention = CallingConvention.Cdecl;
[DllImport(DllName, CallingConvention = Convention)]
internal static extern IntPtr CreateMappingConfiguration();
[DllImport(DllName, CallingConvention = Convention)]
internal static extern void DestroyMappingConfiguration(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool LoadMappingSettings(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetSingleKeyRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyRemap(IntPtr config, int index, ref NativeSingleKeyMapping mapping);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetSingleKeyToTextRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyToTextRemap(IntPtr config, int index, ref NativeKeyboardTextMapping mapping);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern int GetShortcutRemapCount(IntPtr config);
[DllImport(DllName, CallingConvention = Convention)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetShortcutRemap(IntPtr config, int index, ref NativeShortcutMapping mapping);
[DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
internal static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
[DllImport(DllName, CallingConvention = Convention)]
internal static extern void FreeString(IntPtr str);
internal static string GetStringAndFree(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
return string.Empty;
}
var result = Marshal.PtrToStringUni(handle) ?? string.Empty;
FreeString(handle);
return result;
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeSingleKeyMapping
{
public int OriginalKey;
public IntPtr TargetKey;
[MarshalAs(UnmanagedType.Bool)]
public bool IsShortcut;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeKeyboardTextMapping
{
public int OriginalKey;
public IntPtr TargetText;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct NativeShortcutMapping
{
public IntPtr OriginalKeys;
public IntPtr TargetKeys;
public IntPtr TargetApp;
public int OperationType;
public IntPtr TargetText;
public IntPtr ProgramPath;
public IntPtr ProgramArgs;
public IntPtr StartInDirectory;
public int Elevation;
public int IfRunningAction;
public int Visibility;
public IntPtr UriToOpen;
}

View File

@@ -1,56 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace KeyboardManager.ModuleServices;
public enum KeyboardManagerMappingKind
{
SingleKeyToKey,
SingleKeyToShortcut,
SingleKeyToText,
ShortcutToKey,
ShortcutToShortcut,
ShortcutToText,
ShortcutToProgram,
ShortcutToUri,
}
public sealed class KeyboardManagerMappingRecord
{
public string Id { get; init; } = string.Empty;
public KeyboardManagerMappingKind Kind { get; init; }
public string TriggerDisplay { get; init; } = string.Empty;
public string TargetDisplay { get; init; } = string.Empty;
public string Subtitle { get; init; } = string.Empty;
public bool IsAppSpecific { get; init; }
public string TargetApp { get; init; } = string.Empty;
public string OriginalKeys { get; init; } = string.Empty;
public string TargetKeys { get; init; } = string.Empty;
public string TargetText { get; init; } = string.Empty;
public string ProgramPath { get; init; } = string.Empty;
public string ProgramArgs { get; init; } = string.Empty;
public string StartInDirectory { get; init; } = string.Empty;
public int Elevation { get; init; }
public int IfRunningAction { get; init; }
public int Visibility { get; init; }
public string UriToOpen { get; init; } = string.Empty;
public bool IsExecutable => Kind is KeyboardManagerMappingKind.ShortcutToProgram or KeyboardManagerMappingKind.ShortcutToUri;
}

View File

@@ -1,236 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using ManagedCommon;
namespace KeyboardManager.ModuleServices;
public sealed class KeyboardManagerMappingService
{
public static KeyboardManagerMappingService Instance { get; } = new();
public Task<PowerToys.ModuleContracts.OperationResult<IReadOnlyList<KeyboardManagerMappingRecord>>> GetMappingsAsync(CancellationToken cancellationToken = default)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
using var session = new MappingConfigurationSession();
if (!KeyboardManagerInterop.LoadMappingSettings(session.Handle))
{
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>("Failed to load Keyboard Manager mappings."));
}
var mappings = new List<KeyboardManagerMappingRecord>();
LoadSingleKeyMappings(session.Handle, mappings);
LoadSingleKeyTextMappings(session.Handle, mappings);
LoadShortcutMappings(session.Handle, mappings);
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Ok<IReadOnlyList<KeyboardManagerMappingRecord>>(mappings));
}
catch (OperationCanceledException)
{
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>("Keyboard Manager mapping query was cancelled."));
}
catch (Exception ex)
{
Logger.LogError($"Failed to enumerate Keyboard Manager mappings: {ex.Message}");
return Task.FromResult(PowerToys.ModuleContracts.OperationResults.Fail<IReadOnlyList<KeyboardManagerMappingRecord>>($"Failed to enumerate Keyboard Manager mappings: {ex.Message}"));
}
}
private static void LoadSingleKeyMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetSingleKeyRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeSingleKeyMapping);
if (!KeyboardManagerInterop.GetSingleKeyRemap(handle, i, ref native))
{
continue;
}
var originalDisplay = GetKeyDisplayName(native.OriginalKey);
var targetRaw = KeyboardManagerInterop.GetStringAndFree(native.TargetKey);
var targetDisplay = native.IsShortcut ? FormatShortcut(targetRaw) : FormatSingleKey(targetRaw);
var kind = native.IsShortcut ? KeyboardManagerMappingKind.SingleKeyToShortcut : KeyboardManagerMappingKind.SingleKeyToKey;
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("single-key", native.OriginalKey.ToString(CultureInfo.InvariantCulture), targetRaw, native.IsShortcut.ToString()),
Kind = kind,
TriggerDisplay = originalDisplay,
TargetDisplay = targetDisplay,
Subtitle = $"Maps to {targetDisplay}",
OriginalKeys = native.OriginalKey.ToString(CultureInfo.InvariantCulture),
TargetKeys = targetRaw,
});
}
}
private static void LoadSingleKeyTextMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetSingleKeyToTextRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeKeyboardTextMapping);
if (!KeyboardManagerInterop.GetSingleKeyToTextRemap(handle, i, ref native))
{
continue;
}
var originalDisplay = GetKeyDisplayName(native.OriginalKey);
var targetText = KeyboardManagerInterop.GetStringAndFree(native.TargetText);
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("single-key-text", native.OriginalKey.ToString(CultureInfo.InvariantCulture), targetText),
Kind = KeyboardManagerMappingKind.SingleKeyToText,
TriggerDisplay = originalDisplay,
TargetDisplay = targetText,
Subtitle = $"Types {targetText}",
OriginalKeys = native.OriginalKey.ToString(CultureInfo.InvariantCulture),
TargetText = targetText,
});
}
}
private static void LoadShortcutMappings(IntPtr handle, ICollection<KeyboardManagerMappingRecord> mappings)
{
var count = KeyboardManagerInterop.GetShortcutRemapCount(handle);
for (var i = 0; i < count; i++)
{
var native = default(NativeShortcutMapping);
if (!KeyboardManagerInterop.GetShortcutRemap(handle, i, ref native))
{
continue;
}
var originalKeys = KeyboardManagerInterop.GetStringAndFree(native.OriginalKeys);
var targetKeys = KeyboardManagerInterop.GetStringAndFree(native.TargetKeys);
var targetApp = KeyboardManagerInterop.GetStringAndFree(native.TargetApp);
var targetText = KeyboardManagerInterop.GetStringAndFree(native.TargetText);
var programPath = KeyboardManagerInterop.GetStringAndFree(native.ProgramPath);
var programArgs = KeyboardManagerInterop.GetStringAndFree(native.ProgramArgs);
var startInDirectory = KeyboardManagerInterop.GetStringAndFree(native.StartInDirectory);
var uriToOpen = KeyboardManagerInterop.GetStringAndFree(native.UriToOpen);
var triggerDisplay = FormatShortcut(originalKeys);
var (kind, targetDisplay, subtitle) = DescribeShortcutTarget(native.OperationType, targetKeys, targetText, programPath, uriToOpen);
mappings.Add(new KeyboardManagerMappingRecord
{
Id = CreateId("shortcut", originalKeys, targetKeys, targetText, programPath, programArgs, startInDirectory, uriToOpen, targetApp, native.OperationType.ToString(CultureInfo.InvariantCulture)),
Kind = kind,
TriggerDisplay = triggerDisplay,
TargetDisplay = targetDisplay,
Subtitle = string.IsNullOrWhiteSpace(targetApp) ? subtitle : $"{subtitle} in {targetApp}",
IsAppSpecific = !string.IsNullOrWhiteSpace(targetApp),
TargetApp = targetApp,
OriginalKeys = originalKeys,
TargetKeys = targetKeys,
TargetText = targetText,
ProgramPath = programPath,
ProgramArgs = programArgs,
StartInDirectory = startInDirectory,
Elevation = native.Elevation,
IfRunningAction = native.IfRunningAction,
Visibility = native.Visibility,
UriToOpen = uriToOpen,
});
}
}
private static (KeyboardManagerMappingKind Kind, string TargetDisplay, string Subtitle) DescribeShortcutTarget(int operationType, string targetKeys, string targetText, string programPath, string uriToOpen)
{
return operationType switch
{
1 => (KeyboardManagerMappingKind.ShortcutToProgram, programPath, $"Opens {programPath}"),
2 => (KeyboardManagerMappingKind.ShortcutToUri, uriToOpen, $"Opens {uriToOpen}"),
3 => (KeyboardManagerMappingKind.ShortcutToText, targetText, $"Types {targetText}"),
_ => DescribeShortcutRemap(targetKeys),
};
}
private static (KeyboardManagerMappingKind Kind, string TargetDisplay, string Subtitle) DescribeShortcutRemap(string targetKeys)
{
var keyCodes = ParseKeyCodes(targetKeys);
if (keyCodes.Count <= 1)
{
var targetDisplay = FormatSingleKey(targetKeys);
return (KeyboardManagerMappingKind.ShortcutToKey, targetDisplay, $"Maps to {targetDisplay}");
}
var shortcutDisplay = FormatShortcut(targetKeys);
return (KeyboardManagerMappingKind.ShortcutToShortcut, shortcutDisplay, $"Maps to {shortcutDisplay}");
}
private static string FormatSingleKey(string value)
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var keyCode))
{
return GetKeyDisplayName(keyCode);
}
return value;
}
private static string FormatShortcut(string value)
{
var parts = ParseKeyCodes(value)
.Select(GetKeyDisplayName)
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToArray();
return parts.Length == 0 ? value : string.Join(" + ", parts);
}
private static IReadOnlyList<int> ParseKeyCodes(string value)
{
return value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(part => int.TryParse(part, NumberStyles.Integer, CultureInfo.InvariantCulture, out var keyCode) ? keyCode : (int?)null)
.Where(keyCode => keyCode.HasValue)
.Select(keyCode => keyCode!.Value)
.ToArray();
}
private static string GetKeyDisplayName(int keyCode)
{
var buffer = new StringBuilder(64);
KeyboardManagerInterop.GetKeyDisplayName(keyCode, buffer, buffer.Capacity);
return buffer.ToString();
}
private static string CreateId(params string[] values)
{
var payload = string.Join("|", values);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(payload));
return Convert.ToHexString(hash[..8]);
}
private sealed partial class MappingConfigurationSession : IDisposable
{
public MappingConfigurationSession()
{
Handle = KeyboardManagerInterop.CreateMappingConfiguration();
if (Handle == IntPtr.Zero)
{
throw new InvalidOperationException("Failed to create Keyboard Manager mapping configuration.");
}
}
public IntPtr Handle { get; private set; }
public void Dispose()
{
if (Handle != IntPtr.Zero)
{
KeyboardManagerInterop.DestroyMappingConfiguration(Handle);
Handle = IntPtr.Zero;
}
}
}
}

View File

@@ -29,7 +29,7 @@
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<AdditionalIncludeDirectories>./;$(RepoRoot)src\modules\;$(RepoRoot)src\common\Display;$(RepoRoot)src\common\inc;$(RepoRoot)src\common\Telemetry;$(RepoRoot)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>./;$(SolutionDir)src\modules\;$(SolutionDir)src\common\Display;$(SolutionDir)src\common\inc;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
@@ -117,6 +117,6 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
</Target>
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(RepoRoot)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory)\..\KeyboardManagerEditor\ resource.base.h resource.h KeyboardManagerEditor.base.rc KeyboardManagerEditor.rc" />
</Target>
</Project>
</Project>

View File

@@ -335,10 +335,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
@@ -355,10 +351,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->startInDirectory = AllocateAndCopyString(targetShortcut.runProgramStartInDir);
mapping->elevation = static_cast<int>(targetShortcut.elevationLevel);
mapping->ifRunningAction = static_cast<int>(targetShortcut.alreadyRunningAction);
mapping->visibility = static_cast<int>(targetShortcut.startWindowType);
mapping->uriToOpen = AllocateAndCopyString(L"");
break;
@@ -367,10 +359,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
break;
@@ -379,10 +367,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
break;
}
@@ -391,14 +375,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 3;
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
@@ -458,10 +438,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
@@ -477,10 +453,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->startInDirectory = AllocateAndCopyString(targetShortcut.runProgramStartInDir);
mapping->elevation = static_cast<int>(targetShortcut.elevationLevel);
mapping->ifRunningAction = static_cast<int>(targetShortcut.alreadyRunningAction);
mapping->visibility = static_cast<int>(targetShortcut.startWindowType);
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcut.operationType == Shortcut::OperationType::OpenURI)
@@ -489,10 +461,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
}
else
@@ -501,10 +469,6 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
}
@@ -512,14 +476,10 @@ bool GetShortcutRemapByType(void* config, int operationType, int index, Shortcut
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 3;
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->startInDirectory = AllocateAndCopyString(L"");
mapping->elevation = 0;
mapping->ifRunningAction = 0;
mapping->visibility = 0;
mapping->uriToOpen = AllocateAndCopyString(L"");
}
@@ -789,4 +749,4 @@ int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount)
}
return count;
}
}

View File

@@ -32,10 +32,6 @@ struct ShortcutMapping
wchar_t* targetText;
wchar_t* programPath;
wchar_t* programArgs;
wchar_t* startInDirectory;
int elevation;
int ifRunningAction;
int visibility;
wchar_t* uriToOpen;
};

View File

@@ -428,20 +428,20 @@ namespace Helpers
{
while (true)
{
std::wstring text;
{
std::unique_lock<std::mutex> lock(s_queueMutex);
s_queueCV.wait(lock, [] { return !s_clipboardQueue.empty() || s_shutdown.load(); });
if (s_shutdown.load())
{
break;
}
text = std::move(s_clipboardQueue.front());
s_clipboardQueue.pop();
}
try
{
std::wstring text;
{
std::unique_lock<std::mutex> lock(s_queueMutex);
s_queueCV.wait(lock, [] { return !s_clipboardQueue.empty() || s_shutdown.load(); });
if (s_shutdown.load())
{
break;
}
text = std::move(s_clipboardQueue.front());
s_clipboardQueue.pop();
}
// Snapshot current clipboard state
bool hadOriginalText = false;
std::wstring originalClipboardText;
@@ -490,22 +490,15 @@ namespace Helpers
}
}
}
catch (const std::exception& ex)
{
OutputDebugStringA("KBM ClipboardWorker exception: ");
OutputDebugStringA(ex.what());
OutputDebugStringA("\n");
}
catch (...)
{
OutputDebugStringA("KBM ClipboardWorker unknown exception\n");
OutputDebugStringA("KBM ClipboardWorker exception caught, continuing\n");
}
}
}
// Function to send text via clipboard paste (Ctrl+V).
// Saves the previous clipboard content and restores it asynchronously.
bool SendTextViaClipboard(const std::wstring& text)
// Inner implementation that may throw C++ exceptions.
static bool SendTextViaClipboardImpl(const std::wstring& text)
{
// Lazily start the worker on first use.
std::call_once(s_workerInitFlag, [] {
@@ -524,7 +517,7 @@ namespace Helpers
});
// Enqueue the text and return immediately so we never block the
// low-level keyboard hook (WH_KEYBOARD_LL).
// low-level keyboard hook (WH_KEYBOARD_LL).
{
std::lock_guard<std::mutex> lock(s_queueMutex);
if (!s_clipboardQueue.empty())
@@ -538,6 +531,24 @@ namespace Helpers
return true;
}
// Function to send text via clipboard paste (Ctrl+V).
// Saves the previous clipboard content and restores it asynchronously.
// This function MUST NOT throw — it's called from noexcept hook handlers.
// We use __try/__except (SEH) because C++ try/catch may be optimized away
// in Release builds when the caller is noexcept.
bool SendTextViaClipboard(const std::wstring& text) noexcept
{
__try
{
return SendTextViaClipboardImpl(text);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
OutputDebugStringA("KBM SendTextViaClipboard SEH exception caught\n");
return false;
}
}
// Function to filter the key codes for artificial key codes
int32_t FilterArtificialKeys(const int32_t& key)
{

View File

@@ -42,7 +42,7 @@ namespace Helpers
void SetTextKeyEvents(std::vector<INPUT>& keyEventArray, const std::wstring& remapping);
// Function to send text via clipboard paste (Ctrl+V). Saves and restores previous clipboard content.
bool SendTextViaClipboard(const std::wstring& text);
bool SendTextViaClipboard(const std::wstring& text) noexcept;
// Function to return window handle for a full screen UWP app
HWND GetFullscreenUWPWindowHandle();

View File

@@ -33,11 +33,11 @@
<AdditionalIncludeDirectories>$(RepoRoot)src\;$(RepoRoot)src\modules;$(RepoRoot)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>Shcore.lib;Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<Link>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="Generated Files\resource.h" />

View File

@@ -1,21 +1,16 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/resources.h>
#include "Generated Files/resource.h"
#include <keyboardmanager/common/KeyboardManagerConstants.h>
#include <keyboardmanager/common/MappingConfiguration.h>
#include <common/utils/winapi_error.h>
#include <keyboardmanager/dll/trace.h>
#include <shellapi.h>
#include <common/utils/logger_helper.h>
#include <common/interop/shared_constants.h>
#include <thread>
#include <atomic>
#include <TlHelp32.h>
#include <future>
#include <iomanip>
#include <sstream>
#include <common/utils/resources.h>
#include "Generated Files/resource.h"
#include <keyboardmanager/common/KeyboardManagerConstants.h>
#include <common/utils/winapi_error.h>
#include <keyboardmanager/dll/trace.h>
#include <shellapi.h>
#include <common/utils/logger_helper.h>
#include <common/interop/shared_constants.h>
#include <thread>
#include <atomic>
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
@@ -42,634 +37,11 @@ namespace
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ToggleShortcut";
const wchar_t JSON_KEY_EDITOR_SHORTCUT[] = L"EditorShortcut";
const wchar_t JSON_KEY_USE_NEW_EDITOR[] = L"useNewEditor";
const wchar_t ACTION_ID_TOGGLE_ACTIVE[] = L"powertoys.keyboardManager.toggleActive";
const wchar_t ACTION_ID_OPEN_EDITOR[] = L"powertoys.keyboardManager.openEditor";
const wchar_t ACTION_ID_MAPPING_PREFIX[] = L"powertoys.keyboardManager.mapping.";
const wchar_t ACTION_CATEGORY[] = L"Keyboard Manager";
struct ActionInvokeResult
{
bool success = true;
std::wstring error_code;
std::wstring message;
};
struct handle_data
{
unsigned long process_id;
HWND window_handle;
};
ActionInvokeResult action_error(std::wstring error_code, std::wstring message)
{
return ActionInvokeResult{ .success = false, .error_code = std::move(error_code), .message = std::move(message) };
}
bool is_keyboard_manager_custom_action(const KeyShortcutTextUnion& target)
{
if (target.index() != 1)
{
return false;
}
const auto& shortcut = std::get<Shortcut>(target);
return shortcut.IsRunProgram() || shortcut.IsOpenURI();
}
BOOL CALLBACK enum_windows_callback_allow_non_visible(HWND handle, LPARAM l_param)
{
handle_data& data = *reinterpret_cast<handle_data*>(l_param);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id == process_id)
{
data.window_handle = handle;
return FALSE;
}
return TRUE;
}
BOOL CALLBACK enum_windows_callback(HWND handle, LPARAM l_param)
{
handle_data& data = *reinterpret_cast<handle_data*>(l_param);
unsigned long process_id = 0;
GetWindowThreadProcessId(handle, &process_id);
if (data.process_id != process_id || !(GetWindow(handle, GW_OWNER) == static_cast<HWND>(0) && IsWindowVisible(handle)))
{
return TRUE;
}
data.window_handle = handle;
return FALSE;
}
HWND find_main_window(unsigned long process_id, bool allow_non_visible)
{
handle_data data{ .process_id = process_id, .window_handle = nullptr };
EnumWindows(allow_non_visible ? enum_windows_callback_allow_non_visible : enum_windows_callback, reinterpret_cast<LPARAM>(&data));
return data.window_handle;
}
std::vector<DWORD> get_processes_id_by_name(const std::wstring& process_name)
{
std::vector<DWORD> process_ids;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE)
{
return process_ids;
}
PROCESSENTRY32 process_entry{};
process_entry.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(snapshot, &process_entry))
{
do
{
if (_wcsicmp(process_entry.szExeFile, process_name.c_str()) == 0)
{
process_ids.push_back(process_entry.th32ProcessID);
}
} while (Process32Next(snapshot, &process_entry));
}
CloseHandle(snapshot);
return process_ids;
}
DWORD get_process_id_by_name(const std::wstring& process_name)
{
const auto process_ids = get_processes_id_by_name(process_name);
return process_ids.empty() ? 0 : process_ids.front();
}
std::wstring get_file_name_from_path(const std::wstring& full_path)
{
const size_t found = full_path.find_last_of(L"\\/");
return found == std::wstring::npos ? full_path : full_path.substr(found + 1);
}
void close_process_by_name(const std::wstring& file_name_part)
{
auto process_ids = get_processes_id_by_name(file_name_part);
if (process_ids.empty())
{
return;
}
std::thread([file_name_part]() {
auto retry_count = 10;
auto local_process_ids = get_processes_id_by_name(file_name_part);
while (!local_process_ids.empty() && retry_count-- > 0)
{
for (DWORD pid : local_process_ids)
{
HWND hwnd = find_main_window(pid, false);
if (hwnd)
{
SendMessage(hwnd, WM_CLOSE, 0, 0);
}
Sleep(10);
}
local_process_ids = get_processes_id_by_name(file_name_part);
if (!local_process_ids.empty())
{
Sleep(100);
}
}
}).detach();
}
void terminate_processes_by_name(const std::wstring& file_name_part)
{
for (DWORD pid : get_processes_id_by_name(file_name_part))
{
HANDLE process = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
if (process)
{
TerminateProcess(process, 0);
CloseHandle(process);
}
}
}
bool hide_program(DWORD pid, const std::wstring& program_name, int retry_count)
{
Logger::trace(L"KeyboardManager action: hide {} pid={} retry={}", program_name, pid, retry_count);
HWND hwnd = find_main_window(pid, false);
if (hwnd == nullptr && retry_count < 20)
{
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return hide_program(pid, program_name, retry_count + 1);
});
}
hwnd = FindWindow(nullptr, nullptr);
while (hwnd)
{
DWORD pid_for_hwnd = 0;
GetWindowThreadProcessId(hwnd, &pid_for_hwnd);
if (pid == pid_for_hwnd && IsWindowVisible(hwnd))
{
ShowWindow(hwnd, SW_HIDE);
}
hwnd = FindWindowEx(nullptr, hwnd, nullptr, nullptr);
}
return true;
}
bool show_program(DWORD pid, const std::wstring& program_name, bool is_new_process, bool minimize_if_visible, int retry_count)
{
Logger::trace(L"KeyboardManager action: show {} pid={} retry={}", program_name, pid, retry_count);
HWND hwnd = find_main_window(pid, false);
if (hwnd == nullptr)
{
if (retry_count < 20)
{
auto future = std::async(std::launch::async, [=] {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
return show_program(pid, program_name, is_new_process, minimize_if_visible, retry_count + 1);
});
}
}
else
{
if (hwnd == GetForegroundWindow())
{
if (!is_new_process && minimize_if_visible)
{
return ShowWindow(hwnd, SW_MINIMIZE);
}
return false;
}
if (IsIconic(hwnd) && !ShowWindow(hwnd, SW_RESTORE))
{
Logger::warn(L"KeyboardManager action: failed restoring {}", program_name);
}
INPUT inputs[1] = { { .type = INPUT_MOUSE } };
SendInput(ARRAYSIZE(inputs), inputs, sizeof(INPUT));
return SetForegroundWindow(hwnd);
}
if (is_new_process)
{
return true;
}
hwnd = FindWindow(nullptr, nullptr);
while (hwnd)
{
DWORD pid_for_hwnd = 0;
GetWindowThreadProcessId(hwnd, &pid_for_hwnd);
if (pid == pid_for_hwnd)
{
const int length = GetWindowTextLength(hwnd);
if (length > 0)
{
ShowWindow(hwnd, SW_RESTORE);
if (SetForegroundWindow(hwnd))
{
return true;
}
}
}
hwnd = FindWindowEx(nullptr, hwnd, nullptr, nullptr);
}
return false;
}
std::wstring expand_environment_string(const std::wstring& value)
{
if (value.empty())
{
return {};
}
const DWORD size = ExpandEnvironmentStrings(value.c_str(), nullptr, 0);
if (size == 0)
{
return value;
}
std::wstring expanded(size, L'\0');
if (ExpandEnvironmentStrings(value.c_str(), expanded.data(), size) == 0)
{
return value;
}
if (!expanded.empty() && expanded.back() == L'\0')
{
expanded.pop_back();
}
return expanded;
}
std::wstring build_command_line(const std::wstring& file, const std::wstring& params)
{
std::wstring command_line = L"\"" + file + L"\"";
if (!params.empty())
{
command_line += L" " + params;
}
return command_line;
}
bool launch_process_same_user(const std::wstring& file, const std::wstring& params, const wchar_t* working_directory, bool show_window, DWORD& process_id)
{
auto command_line = build_command_line(file, params);
STARTUPINFOW startup_info{};
startup_info.cb = sizeof(startup_info);
DWORD creation_flags = 0;
if (!show_window)
{
startup_info.dwFlags = STARTF_USESHOWWINDOW;
startup_info.wShowWindow = SW_HIDE;
creation_flags = CREATE_NO_WINDOW;
}
PROCESS_INFORMATION process_info{};
const auto launched = CreateProcessW(
file.c_str(),
command_line.data(),
nullptr,
nullptr,
FALSE,
creation_flags,
nullptr,
working_directory,
&startup_info,
&process_info);
if (!launched)
{
Logger::error(L"KeyboardManager action: CreateProcessW failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
process_id = process_info.hProcess ? GetProcessId(process_info.hProcess) : 0;
if (process_info.hThread)
{
CloseHandle(process_info.hThread);
}
if (process_info.hProcess)
{
CloseHandle(process_info.hProcess);
}
return process_id != 0;
}
bool launch_process_shell_execute(const wchar_t* verb, const std::wstring& file, const std::wstring& params, const wchar_t* working_directory, bool show_window, DWORD& process_id)
{
SHELLEXECUTEINFOW execute_info{};
execute_info.cbSize = sizeof(execute_info);
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
execute_info.lpVerb = verb;
execute_info.lpFile = file.c_str();
execute_info.lpParameters = params.empty() ? nullptr : params.c_str();
execute_info.lpDirectory = working_directory;
execute_info.nShow = show_window ? SW_SHOWDEFAULT : SW_HIDE;
if (!ShellExecuteExW(&execute_info))
{
Logger::error(L"KeyboardManager action: ShellExecuteExW failed. {}", get_last_error_or_default(GetLastError()));
return false;
}
process_id = execute_info.hProcess ? GetProcessId(execute_info.hProcess) : 0;
if (execute_info.hProcess)
{
CloseHandle(execute_info.hProcess);
}
return process_id != 0;
}
ActionInvokeResult invoke_run_program_action(const Shortcut& shortcut)
{
const std::wstring full_expanded_file_path = expand_environment_string(shortcut.runProgramFilePath);
const std::wstring file_name_part = get_file_name_from_path(full_expanded_file_path);
const DWORD target_pid = get_process_id_by_name(file_name_part);
if (target_pid != 0 && shortcut.alreadyRunningAction != Shortcut::ProgramAlreadyRunningAction::StartAnother)
{
if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::EndTask)
{
terminate_processes_by_name(file_name_part);
}
else if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::Close)
{
close_process_by_name(file_name_part);
}
else if (shortcut.alreadyRunningAction == Shortcut::ProgramAlreadyRunningAction::ShowWindow)
{
const auto process_ids = get_processes_id_by_name(file_name_part);
for (DWORD pid : process_ids)
{
show_program(pid, file_name_part, false, false, 0);
}
}
return {};
}
if (GetFileAttributesW(full_expanded_file_path.c_str()) == INVALID_FILE_ATTRIBUTES)
{
return action_error(L"program_not_found", L"The program '" + file_name_part + L"' was not found.");
}
const std::wstring expanded_args = expand_environment_string(shortcut.runProgramArgs);
const std::wstring expanded_start_dir = expand_environment_string(shortcut.runProgramStartInDir);
const wchar_t* current_dir_ptr = expanded_start_dir.empty() ? nullptr : expanded_start_dir.c_str();
if (current_dir_ptr && GetFileAttributesW(current_dir_ptr) == INVALID_FILE_ATTRIBUTES)
{
return action_error(L"invalid_start_directory", L"The configured start-in path is invalid.");
}
DWORD process_id = 0;
const bool show_window = shortcut.startWindowType == Shortcut::StartWindowType::Normal;
bool launched = false;
if (shortcut.elevationLevel == Shortcut::ElevationLevel::Elevated)
{
launched = launch_process_shell_execute(L"runas", full_expanded_file_path, expanded_args, current_dir_ptr, show_window, process_id);
}
else if (shortcut.elevationLevel == Shortcut::ElevationLevel::DifferentUser)
{
launched = launch_process_shell_execute(L"runAsUser", full_expanded_file_path, expanded_args, current_dir_ptr, show_window, process_id);
}
else
{
launched = launch_process_same_user(full_expanded_file_path, expanded_args, current_dir_ptr, show_window, process_id);
}
if (!launched || process_id == 0)
{
return action_error(L"launch_failed", L"The application might not have started.");
}
if (shortcut.startWindowType == Shortcut::StartWindowType::Hidden)
{
hide_program(process_id, file_name_part, 0);
}
return {};
}
ActionInvokeResult invoke_open_uri_action(const Shortcut& shortcut)
{
std::wstring target = shortcut.uriToOpen;
if (target.empty())
{
return action_error(L"invalid_uri", L"The configured path or URI is empty.");
}
if (!PathIsURLW(target.c_str()))
{
wchar_t url[2048]{};
DWORD buffer_size = ARRAYSIZE(url);
if (UrlCreateFromPathW(target.c_str(), url, &buffer_size, 0) != S_OK)
{
return action_error(L"invalid_uri", L"Could not understand the configured path or URI.");
}
target = url;
}
const auto result = reinterpret_cast<INT_PTR>(ShellExecuteW(nullptr, L"open", target.c_str(), nullptr, nullptr, SW_SHOWNORMAL));
if (result <= 32)
{
return action_error(L"invoke_failed", L"Could not open the configured path or URI.");
}
return {};
}
ActionInvokeResult invoke_keyboard_manager_custom_action(const Shortcut& shortcut)
{
if (shortcut.IsRunProgram())
{
return invoke_run_program_action(shortcut);
}
if (shortcut.IsOpenURI())
{
return invoke_open_uri_action(shortcut);
}
return action_error(L"unsupported_action", L"Keyboard Manager only exposes Run Program and Open URL mappings as invokable actions.");
}
uint64_t fnv1a_hash(const std::wstring& value)
{
uint64_t hash = 1469598103934665603ull;
for (wchar_t ch : value)
{
hash ^= static_cast<uint64_t>(ch);
hash *= 1099511628211ull;
}
return hash;
}
std::wstring mapping_action_id(const Shortcut& source, const Shortcut& target, const std::wstring& app)
{
std::wstringstream identity;
identity << source.ToHstringVK().c_str() << L'|' << source.exactMatch << L'|' << app << L'|' << static_cast<int>(target.operationType) << L'|';
if (target.IsRunProgram())
{
identity << target.runProgramFilePath << L'|' << target.runProgramArgs << L'|' << target.runProgramStartInDir << L'|' << static_cast<int>(target.elevationLevel) << L'|' << static_cast<int>(target.alreadyRunningAction) << L'|' << static_cast<int>(target.startWindowType);
}
else if (target.IsOpenURI())
{
identity << target.uriToOpen;
}
std::wstringstream action_id;
action_id << ACTION_ID_MAPPING_PREFIX << std::hex << std::setfill(L'0') << std::setw(16) << fnv1a_hash(identity.str());
return action_id.str();
}
std::wstring mapping_action_display_name(const Shortcut& target)
{
if (target.IsRunProgram())
{
const auto expanded_path = expand_environment_string(target.runProgramFilePath);
const auto file_name = get_file_name_from_path(expanded_path.empty() ? target.runProgramFilePath : expanded_path);
return file_name.empty() ? L"Run Keyboard Manager program action" : (L"Run " + file_name);
}
if (target.IsOpenURI())
{
return L"Open " + target.uriToOpen;
}
return L"Keyboard Manager action";
}
std::wstring mapping_action_description(const Shortcut& source, const Shortcut& target, const std::wstring& app)
{
const std::wstring scope = app.empty() ? L"Global" : (L"App-specific (" + app + L")");
const std::wstring target_description = target.IsRunProgram() ? target.runProgramFilePath : target.uriToOpen;
return scope + L" Keyboard Manager shortcut " + source.ToHstringVK().c_str() + L" -> " + target_description;
}
void append_mapping_action(json::JsonArray& actions, const Shortcut& source, const Shortcut& target, const std::wstring& app)
{
json::JsonObject action;
action.SetNamedValue(L"action_id", json::value(mapping_action_id(source, target, app)));
action.SetNamedValue(L"display_name", json::value(mapping_action_display_name(target)));
action.SetNamedValue(L"description", json::value(mapping_action_description(source, target, app)));
action.SetNamedValue(L"category", json::value(ACTION_CATEGORY));
action.SetNamedValue(L"argument_definitions", json::JsonArray{});
actions.Append(action);
}
void append_mapping_actions(json::JsonArray& actions, const MappingConfiguration& config)
{
for (const auto& [source, remap] : config.osLevelShortcutReMap)
{
if (is_keyboard_manager_custom_action(remap.targetShortcut))
{
append_mapping_action(actions, source, std::get<Shortcut>(remap.targetShortcut), L"");
}
}
for (const auto& [app, remaps] : config.appSpecificShortcutReMap)
{
for (const auto& [source, remap] : remaps)
{
if (is_keyboard_manager_custom_action(remap.targetShortcut))
{
append_mapping_action(actions, source, std::get<Shortcut>(remap.targetShortcut), app);
}
}
}
}
std::optional<Shortcut> find_mapping_action(const std::wstring& action_id)
{
MappingConfiguration config;
if (!config.LoadSettings())
{
return std::nullopt;
}
for (const auto& [source, remap] : config.osLevelShortcutReMap)
{
if (!is_keyboard_manager_custom_action(remap.targetShortcut))
{
continue;
}
const auto& target = std::get<Shortcut>(remap.targetShortcut);
if (mapping_action_id(source, target, L"") == action_id)
{
return target;
}
}
for (const auto& [app, remaps] : config.appSpecificShortcutReMap)
{
for (const auto& [source, remap] : remaps)
{
if (!is_keyboard_manager_custom_action(remap.targetShortcut))
{
continue;
}
const auto& target = std::get<Shortcut>(remap.targetShortcut);
if (mapping_action_id(source, target, app) == action_id)
{
return target;
}
}
}
return std::nullopt;
}
bool write_marshaled_json(const std::wstring& json, wchar_t* buffer, int* buffer_size)
{
if (!buffer_size)
{
return false;
}
const int required_size = static_cast<int>(json.size() + 1);
if (!buffer || *buffer_size < required_size)
{
*buffer_size = required_size;
return false;
}
wcscpy_s(buffer, *buffer_size, json.c_str());
return true;
}
}
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ToggleShortcut";
const wchar_t JSON_KEY_EDITOR_SHORTCUT[] = L"EditorShortcut";
const wchar_t JSON_KEY_USE_NEW_EDITOR[] = L"useNewEditor";
}
// Implement the PowerToy Module Interface and all the required methods.
class KeyboardManager : public PowertoyModuleIface
@@ -983,91 +355,13 @@ public:
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Signal from the Settings editor to call a custom action.
virtual void call_custom_action(const wchar_t* /*action*/) override
{
}
virtual bool get_actions(wchar_t* buffer, int* buffer_size) override
{
json::JsonArray actions;
json::JsonObject toggle_action;
toggle_action.SetNamedValue(L"action_id", json::value(ACTION_ID_TOGGLE_ACTIVE));
toggle_action.SetNamedValue(L"display_name", json::value(L"Toggle Keyboard Manager active state"));
toggle_action.SetNamedValue(L"description", json::value(L"Turns the Keyboard Manager engine on or off."));
toggle_action.SetNamedValue(L"category", json::value(L"Keyboard Manager"));
toggle_action.SetNamedValue(L"argument_definitions", json::JsonArray{});
actions.Append(toggle_action);
json::JsonObject open_editor_action;
open_editor_action.SetNamedValue(L"action_id", json::value(ACTION_ID_OPEN_EDITOR));
open_editor_action.SetNamedValue(L"display_name", json::value(L"Open Keyboard Manager editor"));
open_editor_action.SetNamedValue(L"description", json::value(L"Opens the new Keyboard Manager editor window."));
open_editor_action.SetNamedValue(L"category", json::value(L"Keyboard Manager"));
open_editor_action.SetNamedValue(L"argument_definitions", json::JsonArray{});
actions.Append(open_editor_action);
MappingConfiguration config;
if (config.LoadSettings())
{
append_mapping_actions(actions, config);
}
return write_marshaled_json(actions.Stringify().c_str(), buffer, buffer_size);
}
virtual bool invoke_action(const wchar_t* action_id, const wchar_t* /*serialized_args*/, wchar_t* buffer, int* buffer_size) override
{
json::JsonObject result;
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(true));
std::wstring requested_action = action_id ? action_id : L"";
if (requested_action == ACTION_ID_TOGGLE_ACTIVE)
{
toggle_engine();
}
else if (requested_action == ACTION_ID_OPEN_EDITOR)
{
if (!launch_editor())
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(L"launch_failed"));
result.SetNamedValue(L"message", json::value(L"Failed to open the Keyboard Manager editor."));
}
}
else if (requested_action.rfind(ACTION_ID_MAPPING_PREFIX, 0) == 0)
{
const auto mapped_action = find_mapping_action(requested_action);
if (!mapped_action.has_value())
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(L"action_not_found"));
result.SetNamedValue(L"message", json::value(L"The requested Keyboard Manager shortcut action no longer exists."));
}
else
{
const auto action_result = invoke_keyboard_manager_custom_action(*mapped_action);
if (!action_result.success)
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(action_result.error_code));
result.SetNamedValue(L"message", json::value(action_result.message));
}
}
}
else
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(L"unsupported_action"));
result.SetNamedValue(L"message", json::value(L"Keyboard Manager does not recognize the requested action."));
}
return write_marshaled_json(result.Stringify().c_str(), buffer, buffer_size);
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
// Signal from the Settings editor to call a custom action.
virtual void call_custom_action(const wchar_t* /*action*/) override
{
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
{
try
{

View File

@@ -1,227 +0,0 @@
#include "pch.h"
#include "action_pipe_server.h"
#include "action_registry.h"
#include <common/interop/shared_constants.h>
#include <common/logger/logger.h>
#include <vector>
namespace
{
constexpr DWORD MESSAGE_LENGTH_BYTES = sizeof(std::uint32_t);
constexpr DWORD BUFFER_BYTES = 64 * 1024;
std::wstring Utf8ToUtf16(const std::string& value)
{
if (value.empty())
{
return {};
}
const int size = MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
if (size <= 0)
{
return {};
}
std::wstring result(static_cast<size_t>(size), L'\0');
MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), size);
return result;
}
std::string Utf16ToUtf8(const std::wstring& value)
{
if (value.empty())
{
return {};
}
const int size = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
if (size <= 0)
{
return {};
}
std::string result(static_cast<size_t>(size), '\0');
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), size, nullptr, nullptr);
return result;
}
bool ReadExact(HANDLE handle, void* buffer, DWORD bytes_to_read)
{
auto* target = static_cast<std::byte*>(buffer);
DWORD total_read = 0;
while (total_read < bytes_to_read)
{
DWORD bytes_read = 0;
if (!ReadFile(handle, target + total_read, bytes_to_read - total_read, &bytes_read, nullptr))
{
return false;
}
if (bytes_read == 0)
{
return false;
}
total_read += bytes_read;
}
return true;
}
bool WriteExact(HANDLE handle, const void* buffer, DWORD bytes_to_write)
{
const auto* source = static_cast<const std::byte*>(buffer);
DWORD total_written = 0;
while (total_written < bytes_to_write)
{
DWORD bytes_written = 0;
if (!WriteFile(handle, source + total_written, bytes_to_write - total_written, &bytes_written, nullptr))
{
return false;
}
total_written += bytes_written;
}
return true;
}
json::JsonObject ErrorResponse(const std::wstring& error_code, const std::wstring& message)
{
json::JsonObject response;
response.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
response.SetNamedValue(L"error_code", json::value(error_code));
response.SetNamedValue(L"message", json::value(message));
return response;
}
json::JsonObject HandleRequestPayload(const std::string& payload)
{
json::JsonObject request;
if (!json::JsonObject::TryParse(Utf8ToUtf16(payload), request))
{
return ErrorResponse(L"invalid_request", L"The action pipe request payload was not valid JSON.");
}
const std::wstring request_type = request.GetNamedString(L"type", L"").c_str();
if (request_type == L"list_actions")
{
json::JsonObject response;
response.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(true));
response.SetNamedValue(L"actions", PowerToysActionRegistry::Instance().ListActions());
return response;
}
if (request_type == L"invoke_action")
{
const std::wstring action_id = request.GetNamedString(L"action_id", L"").c_str();
const std::wstring arguments = request.GetNamedString(L"arguments", L"{}").c_str();
return PowerToysActionRegistry::Instance().InvokeAction(action_id, arguments);
}
return ErrorResponse(L"unsupported_request", L"The action pipe request type is not supported.");
}
void WakeActionPipeServer()
{
HANDLE client = CreateFileW(
CommonSharedConstants::POWERTOYS_ACTIONS_PIPE,
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
0,
nullptr);
if (client != INVALID_HANDLE_VALUE)
{
CloseHandle(client);
}
}
}
RunnerActionPipeServer::~RunnerActionPipeServer()
{
Stop();
}
void RunnerActionPipeServer::Start()
{
if (_server_thread.joinable())
{
return;
}
_stop_requested = false;
_server_thread = std::thread(&RunnerActionPipeServer::Run, this);
}
void RunnerActionPipeServer::Stop()
{
_stop_requested = true;
WakeActionPipeServer();
if (_server_thread.joinable())
{
_server_thread.join();
}
}
void RunnerActionPipeServer::Run()
{
while (!_stop_requested)
{
HANDLE pipe = CreateNamedPipeW(
CommonSharedConstants::POWERTOYS_ACTIONS_PIPE,
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
BUFFER_BYTES,
BUFFER_BYTES,
0,
nullptr);
if (pipe == INVALID_HANDLE_VALUE)
{
Logger::error(L"RunnerActionPipeServer: failed to create named pipe. error={}", GetLastError());
return;
}
const BOOL connected = ConnectNamedPipe(pipe, nullptr) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
if (!connected)
{
Logger::warn(L"RunnerActionPipeServer: client failed to connect. error={}", GetLastError());
CloseHandle(pipe);
continue;
}
std::uint32_t request_length = 0;
if (ReadExact(pipe, &request_length, MESSAGE_LENGTH_BYTES))
{
std::vector<char> request_payload(request_length);
const bool payload_ok = request_length == 0 || ReadExact(pipe, request_payload.data(), request_length);
json::JsonObject response = payload_ok
? HandleRequestPayload(std::string(request_payload.begin(), request_payload.end()))
: ErrorResponse(L"read_failed", L"Failed to read the action pipe request payload.");
const auto response_utf8 = Utf16ToUtf8(response.Stringify().c_str());
const auto response_length = static_cast<std::uint32_t>(response_utf8.size());
WriteExact(pipe, &response_length, MESSAGE_LENGTH_BYTES);
if (response_length > 0)
{
WriteExact(pipe, response_utf8.data(), response_length);
}
}
FlushFileBuffers(pipe);
DisconnectNamedPipe(pipe);
CloseHandle(pipe);
}
}

View File

@@ -1,20 +0,0 @@
#pragma once
#include <atomic>
#include <thread>
class RunnerActionPipeServer
{
public:
RunnerActionPipeServer() = default;
~RunnerActionPipeServer();
void Start();
void Stop();
private:
std::atomic<bool> _stop_requested{ false };
std::thread _server_thread;
void Run();
};

View File

@@ -1,162 +0,0 @@
#include "pch.h"
#include "action_registry.h"
#include "powertoy_module.h"
#include <common/logger/logger.h>
namespace
{
constexpr wchar_t ACTION_ID_PROPERTY[] = L"action_id";
constexpr wchar_t MODULE_KEY_PROPERTY[] = L"module_key";
constexpr wchar_t AVAILABLE_PROPERTY[] = L"available";
}
PowerToysActionRegistry& PowerToysActionRegistry::Instance()
{
static PowerToysActionRegistry instance;
return instance;
}
json::JsonObject PowerToysActionRegistry::ErrorResult(const std::wstring& error_code, const std::wstring& message)
{
json::JsonObject result;
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(false));
result.SetNamedValue(L"error_code", json::value(error_code));
result.SetNamedValue(L"message", json::value(message));
return result;
}
void PowerToysActionRegistry::RefreshLocked()
{
actions.clear();
duplicate_action_ids.clear();
for (const auto& [module_key, module] : modules())
{
json::JsonArray descriptors;
try
{
descriptors = module.json_actions();
}
catch (...)
{
Logger::error(L"PowerToysActionRegistry: malformed actions from module {}", module_key);
continue;
}
for (const auto& value : descriptors)
{
if (value.ValueType() != json::JsonValueType::Object)
{
Logger::warn(L"PowerToysActionRegistry: ignoring non-object action descriptor from module {}", module_key);
continue;
}
auto descriptor = value.GetObjectW();
const auto action_id_hstring = descriptor.GetNamedString(ACTION_ID_PROPERTY, L"");
const std::wstring action_id = action_id_hstring.c_str();
if (action_id.empty())
{
Logger::warn(L"PowerToysActionRegistry: ignoring action without action_id from module {}", module_key);
continue;
}
if (actions.contains(action_id))
{
actions.erase(action_id);
duplicate_action_ids.insert(action_id);
Logger::error(L"PowerToysActionRegistry: duplicate action_id {} detected", action_id);
continue;
}
if (duplicate_action_ids.contains(action_id))
{
continue;
}
actions.emplace(action_id, RegisteredAction{
.module_key = module_key,
.descriptor = descriptor,
});
}
}
}
json::JsonArray PowerToysActionRegistry::ListActions()
{
std::scoped_lock lock{ mutex };
RefreshLocked();
json::JsonArray result;
for (const auto& [action_id, registered_action] : actions)
{
auto descriptor = registered_action.descriptor;
descriptor.SetNamedValue(ACTION_ID_PROPERTY, json::value(action_id));
descriptor.SetNamedValue(MODULE_KEY_PROPERTY, json::value(registered_action.module_key));
const auto module_it = modules().find(registered_action.module_key);
const bool is_available = module_it != modules().end() && module_it->second->is_enabled();
descriptor.SetNamedValue(AVAILABLE_PROPERTY, json::JsonValue::CreateBooleanValue(is_available));
result.Append(descriptor);
}
return result;
}
json::JsonObject PowerToysActionRegistry::InvokeAction(const std::wstring& action_id, const std::wstring& serialized_args)
{
std::scoped_lock lock{ mutex };
RefreshLocked();
if (duplicate_action_ids.contains(action_id))
{
return ErrorResult(L"duplicate_action_id", L"Multiple modules registered the same action identifier.");
}
const auto action_it = actions.find(action_id);
if (action_it == actions.end())
{
return ErrorResult(L"action_not_found", L"The requested PowerToys action is not registered.");
}
const auto module_it = modules().find(action_it->second.module_key);
if (module_it == modules().end())
{
return ErrorResult(L"module_not_found", L"The module that owns this action is not loaded.");
}
if (!module_it->second->is_enabled())
{
return ErrorResult(L"module_unavailable", L"The module that owns this action is currently disabled.");
}
std::wstring raw_result;
try
{
raw_result = module_it->second.invoke_action(action_id, serialized_args);
}
catch (...)
{
return ErrorResult(L"invoke_failed", L"The module threw while invoking the requested action.");
}
if (raw_result.empty())
{
return ErrorResult(L"empty_result", L"The module returned an empty result.");
}
json::JsonObject result;
if (!json::JsonObject::TryParse(raw_result, result))
{
return ErrorResult(L"invalid_result", L"The module returned malformed action result JSON.");
}
if (!result.HasKey(L"success"))
{
result.SetNamedValue(L"success", json::JsonValue::CreateBooleanValue(true));
}
return result;
}

View File

@@ -1,33 +0,0 @@
#pragma once
#include <common/utils/json.h>
#include <map>
#include <mutex>
#include <set>
#include <string>
class PowerToysActionRegistry
{
public:
static PowerToysActionRegistry& Instance();
json::JsonArray ListActions();
json::JsonObject InvokeAction(const std::wstring& action_id, const std::wstring& serialized_args);
private:
struct RegisteredAction
{
std::wstring module_key;
json::JsonObject descriptor;
};
std::mutex mutex;
std::map<std::wstring, RegisteredAction> actions;
std::set<std::wstring> duplicate_action_ids;
static json::JsonObject ErrorResult(const std::wstring& error_code, const std::wstring& message);
void RefreshLocked();
};

View File

@@ -27,9 +27,8 @@
#include <common/utils/resources.h>
#include <common/utils/clean_video_conference.h>
#include "UpdateUtils.h"
#include "ActionRunnerUtils.h"
#include "action_pipe_server.h"
#include "UpdateUtils.h"
#include "ActionRunnerUtils.h"
#include <winrt/Windows.System.h>
@@ -290,8 +289,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
// L"PowerToys.PowerDisplayModuleInterface.dll", // TEMPORARILY_DISABLED: PowerDisplay
};
for (auto moduleSubdir : knownModules)
{
for (auto moduleSubdir : knownModules)
{
try
{
auto pt_module = load_powertoy(moduleSubdir);
@@ -315,13 +314,9 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
MB_OK | MB_ICONERROR);
#endif
}
}
RunnerActionPipeServer action_pipe_server;
action_pipe_server.Start();
// Start initial powertoys
start_enabled_powertoys();
}
// Start initial powertoys
start_enabled_powertoys();
std::wstring product_version = get_product_version();
Trace::EventLaunch(product_version, isProcessElevated);
PTSettingsHelper::save_last_version_run(product_version);
@@ -346,10 +341,9 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
open_scoobe_window();
}
settings_telemetry::init();
result = run_message_loop();
action_pipe_server.Stop();
}
settings_telemetry::init();
result = run_message_loop();
}
catch (std::runtime_error& err)
{
std::string err_what = err.what();

View File

@@ -29,59 +29,15 @@ PowertoyModule load_powertoy(const std::wstring_view filename)
return PowertoyModule(pt_module, handle);
}
json::JsonObject PowertoyModule::json_config() const
{
int size = 0;
pt_module->get_config(nullptr, &size);
json::JsonObject PowertoyModule::json_config() const
{
int size = 0;
pt_module->get_config(nullptr, &size);
std::wstring result;
result.resize(static_cast<size_t>(size) - 1);
pt_module->get_config(result.data(), &size);
return json::JsonObject::Parse(result);
}
json::JsonArray PowertoyModule::json_actions() const
{
int size = 0;
pt_module->get_actions(nullptr, &size);
if (size <= 0)
{
return json::JsonArray{};
}
std::wstring result;
result.resize(static_cast<size_t>(size) - 1);
if (!pt_module->get_actions(result.data(), &size))
{
return json::JsonArray{};
}
json::JsonArray actions;
if (!json::JsonArray::TryParse(result, actions))
{
return json::JsonArray{};
}
return actions;
}
std::wstring PowertoyModule::invoke_action(const std::wstring& action_id, const std::wstring& serialized_args) const
{
int size = 0;
pt_module->invoke_action(action_id.c_str(), serialized_args.c_str(), nullptr, &size);
if (size <= 0)
{
return {};
}
std::wstring result;
result.resize(static_cast<size_t>(size) - 1);
if (!pt_module->invoke_action(action_id.c_str(), serialized_args.c_str(), result.data(), &size))
{
return {};
}
return result;
}
pt_module->get_config(result.data(), &size);
return json::JsonObject::Parse(result);
}
PowertoyModule::PowertoyModule(PowertoyModuleIface* pt_module, HMODULE handle) :
handle(handle), pt_module(pt_module), hkmng(HotkeyConflictDetector::HotkeyConflictManager::GetInstance())

View File

@@ -39,13 +39,9 @@ public:
return pt_module.get();
}
json::JsonObject json_config() const;
json::JsonArray json_actions() const;
std::wstring invoke_action(const std::wstring& action_id, const std::wstring& serialized_args) const;
void update_hotkeys();
json::JsonObject json_config() const;
void update_hotkeys();
void UpdateHotkeyEx();

View File

@@ -59,10 +59,8 @@
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\common\interop\two_way_pipe_message_ipc.cpp" />
<ClCompile Include="action_pipe_server.cpp" />
<ClCompile Include="action_registry.cpp" />
<ClCompile Include="auto_start_helper.cpp" />
<ClCompile Include="..\common\interop\two_way_pipe_message_ipc.cpp" />
<ClCompile Include="auto_start_helper.cpp" />
<ClCompile Include="bug_report.cpp" />
<ClCompile Include="centralized_hotkeys.cpp" />
<ClCompile Include="general_settings.cpp" />
@@ -82,10 +80,8 @@
<ClCompile Include="unhandled_exception_handler.cpp" />
<ClCompile Include="UpdateUtils.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="action_pipe_server.h" />
<ClInclude Include="action_registry.h" />
<ClInclude Include="ActionRunnerUtils.h" />
<ItemGroup>
<ClInclude Include="ActionRunnerUtils.h" />
<ClInclude Include="ai_detection.h" />
<ClInclude Include="auto_start_helper.h" />
<ClInclude Include="bug_report.h" />
@@ -180,4 +176,4 @@
</ClCompile>
</ItemGroup>
</Target>
</Project>
</Project>

View File

@@ -214,7 +214,7 @@
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<InfoBar
<!-- <InfoBar
x:Name="PowerDisplayDisabledWarningBar"
x:Uid="LightSwitch_PowerDisplayDisabledWarningBar"
Background="Transparent"
@@ -265,7 +265,7 @@
SelectedItem="{x:Bind ViewModel.SelectedLightModeProfile, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</tkcontrols:SettingsExpander>-->
</controls:SettingsGroup>
<!-- Force mode buttons -->
<!--<tkcontrols:SettingsCard