Compare commits

..

104 Commits

Author SHA1 Message Date
Michael Jolley
1d11b732b7 [CmdPal] Fix memory leak in PerformanceWidgetsPage network band items (#48880)
## Summary

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

## Problem

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

## Fix

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

## Validation

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 28e078897a)
2026-06-26 13:35:45 +08:00
Mike Griese
ab4792a4b8 Revert "[CmdPal][Dock] Fix performance meter showing '???' after restart" (#48835)
Reverts microsoft/PowerToys#48682

I'm quite sure that OP did not build or test these changes, and they
should not have merged.

(cherry picked from commit 386a16ff94)
2026-06-26 13:35:45 +08:00
Niels Laute
dc3a9da968 [Shortcut Guide] Prevent overlay crash on section navigation (#48448) (#48481)
## Summary of the Pull Request

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

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

### Navigation-race fix (commit 1)

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

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

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

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

### Crash hardening (commit 2)

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

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

## Validation Steps Performed

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 502dc40aa4)
2026-06-22 17:15:23 +08:00
moooyo
baee472288 [PowerDisplay] Detect built-in panel when driven by the discrete GPU (#48637)
On dual-GPU laptops, Power Display stopped detecting the built-in panel
(and adjusting its brightness) when the **discrete GPU** drives the
display — it showed "can't detect the display". This fixes that by
classifying displays by **capability** (does WMI brightness work on it?)
instead of by the nominal `OutputTechnology` value, which the discrete
GPU misreports for the internal panel.

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

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

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

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

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

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

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 32ad98a0dd)
2026-06-22 16:30:19 +08:00
Bryce Cindrich
142833d49a fix(shortcut-guide): use <N> token for literal digit keys in manifests (#48757)
## Summary of the Pull Request

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

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

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

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

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

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

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

## Validation Steps Performed

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

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit cabb71108a)
2026-06-22 16:29:53 +08:00
Bryce Cindrich
46d594a7f5 feat(shortcut-guide): add Postman manifest and fix numbered-key display (#48461)
## Summary of the Pull Request

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

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

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

## Validation Steps Performed

Built and ran locally (x64 Debug):

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

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

Co-Authored-By: Claude Opus 4.8

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit a0e53de825)
2026-06-22 16:29:52 +08:00
Dave Rayment
673a41512c [ColorPicker] Fix the main window UI appearing in the zoomed-in view (#48762)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

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

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
This uses `SetWindowAffinity()` with the `WDA_EXCLUDEFROMCAPTURE`
constant on the main Color Picker window to ensure the ZoomWindow bitmap
creation excludes the corner of the picker UI. The ability to capture
the picker window is restored immediately after, so it's still visible
in Snipping Tool, Remote Desktop and other tools.

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

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

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

3. Repeated zooming in and out over a normal session.

(cherry picked from commit eab305334b)
2026-06-22 16:29:52 +08:00
Eymard Silva
f57062c206 Fix VS Code Workspaces shared storage lookup (#47505)
## Summary

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

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

It also deduplicates results that may appear in both locations.

Fixes #47445

## Validation

- Reviewed the change against the issue's documented VS Code 1.118
storage paths.
- Attempted local focused build from
`src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.VSCodeWorkspaces`
with `tools/build/build.cmd -Platform x64 -Configuration Debug`.
- Build was blocked by local environment/tooling issues unrelated to
this C# change: missing/invalid VC tooling/Windows SDK.

(cherry picked from commit e1b1a8d7ed)
2026-06-22 16:29:52 +08:00
Mike Griese
f339b48324 CmdPal: Only list available docks when pinning (#48723)
This is a totally minor nitpick.

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

This PR mitigates that situation, but only listing the enabled docks
when pinning.

(cherry picked from commit 6dcda8a6aa)
2026-06-22 16:29:52 +08:00
ABHIJEET KALE
f590871d6d [CmdPal][Dock] Fix performance meter showing '???' after restart (#48682)
This PR fixes Issue #48680 where the CmdPal dock performance meter shows
'???' after restart.

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

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

Fixes #48680

(cherry picked from commit 0d12a62abb)
2026-06-22 16:29:52 +08:00
William
bdcedb142e Changed sleep icon to hibernation icon in Command Palette (#48689)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Changed the icon from the sleep icon to the hibernate icon, which fixes
the issue in #48535

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

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

## Validation Steps Performed
I have visually checked that the icon is now updated to the correct
icon.

(cherry picked from commit 2125d739a3)
2026-06-22 16:29:52 +08:00
Mike Griese
7da85cac40 CmdPal: fix initializing the Run history in AOT builds (#48463)
Regressed in one of the last two releases.

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

So we have to just _not do that_.

Closes #48445

(cherry picked from commit fb0f4292eb)
2026-06-22 16:29:52 +08:00
Clint Rutkas
750ef385b8 [QuickAccess] Suppress unhandled XAML exceptions in flyout host (#48457)
## Summary

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

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

## Changes

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

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

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

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

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

## Why both handlers, not just one

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

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

## Testing

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

## Risk

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

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

---

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit ab4947579b)
2026-06-22 16:29:51 +08:00
Matheus Mol
7279bfd9ec [KBM] Fix modifier key remapped to non-modifier delivering WM_SYSKEYDOWN (#47192)
## Summary of the Pull Request

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

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

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

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

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

## Validation Steps Performed

1. Remapped Left Alt → Backspace in Keyboard Manager
2. Opened a text editor, typed text, pressed the remapped key
3. Confirmed single characters are deleted instead of whole words
4. All 93 unit tests pass (KeyboardManager.Engine.UnitTests)

(cherry picked from commit 5688441127)
2026-06-22 16:29:51 +08:00
Mario Hewardt
8d6bb43c26 ZoomIt - Fix race condition in audio init (#48685)
It was a race condition.

(cherry picked from commit d9216f0fc7)
2026-06-22 16:29:51 +08:00
moooyo
b82e3535da [PowerDisplay] Allow waking a monitor from standby via power-state On (#48628)
<!-- 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

Power Display could put a monitor to sleep but never wake it back up.
Selecting **On** in the per-monitor power-state list was a hard-coded
no-op, so the DDC/CI wake command (VCP `0xD6` = `0x01`) was never sent.
This removes that guard so selecting **On** wakes the display, and
cleans up the dead code/comment left behind by the original
one-directional design.

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

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

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

`MonitorViewModel.HandlePowerStateSelectionChanged` early-returned when
the selected power state was **On** (`0x01`), so `SetPowerStateAsync`
was never called for On and the wake write never reached the monitor. As
a result Power Display's power control was one-directional: it could
send Standby/Suspend/Off but could never turn a monitor back on.

The guard dates back to the very first power-state commit and was paired
with a single-monitor assumption — *"the monitor must be on to see the
UI"*, so On was treated as the always-current state and skipped. A later
change made the selection reflect the monitor's real power state (so a
monitor in the list can legitimately be asleep), and multi-monitor
support means the flyout can be shown on monitor A while the user wants
to wake monitor B. Those changes invalidated the assumption, but the
action-side guard survived a subsequent refactor.

The lower layers already do the right thing:
`MonitorManager.SetPowerStateAsync` →
`DdcCiController.SetPowerStateAsync` → `SetVcpFeatureAsync(monitor,
0xD6, value)` passes the value through unchanged, so the fix is purely
removing the UI-layer guard. DDC/CI stays reachable while the panel is
in Standby/Suspend/Off(DPM), so writing `0x01` turns it back on (this is
the same mechanism Twinkle Tray uses). `Off (Hard)` / `0x05` may still
require a physical wake on some monitors, since that state can cut the
DDC command channel.

Cleanup included in this PR:
- Removed the now-unused `PowerStateItem.PowerStateOn` constant (its
only consumer was the deleted guard).
- Removed the dead `SetPowerState` `[RelayCommand]` (the generated
`SetPowerStateCommand` had zero references — the XAML wires
`SelectionChanged`, not a command).
- Updated the `SetPowerStateAsync` doc comment from the one-directional
framing to a neutral bidirectional description.

Net change: 2 files, +5 / −24.

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

- **Build:** `MSBuild PowerDisplay.csproj -p:Platform=x64
-p:Configuration=Debug` (CoreCompile) — **0 errors / 0 warnings**.
- **Static check:** repo-wide grep confirms no remaining references to
`PowerStateOn` or `SetPowerStateCommand`; the power-state ListView binds
only `ItemsSource` + `SelectionChanged` (no `SelectedItem` binding), so
opening the flyout cannot spuriously re-fire a selection.
- **Manual (requires a DDC/CI monitor):** enable *Power state control*
for a monitor → open its flyout and select **Standby** or **Off (DPM)**
(screen blanks) → reopen the flyout and select **On** → the display
wakes.

No automated test was added: with the guard removed the handler is an
unconditional pass-through (identical in shape to
`HandleInputSourceSelectionChanged`), and it is an `async void` WinUI
event handler over real DDC/CI hardware, which is outside the
`PowerDisplay.Lib` unit-test seam.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit ff3c1f9252)
2026-06-22 16:29:13 +08:00
Boliang Zhang (from Dev Box)
d81186267d Merge main into stable for 0.100 release (rev 14) 2026-06-09 23:45:55 +08:00
Boliang Zhang (from Dev Box)
d22589bcf6 Revert "Add copy monitor diagnostics button to Power Display (#48209)"
This reverts commit 76eb6eaac5.
2026-06-05 15:42:28 +08:00
Boliang Zhang (from Dev Box)
0aed70cc87 Merge main into stable for 0.100 release (rev 13) 2026-06-05 11:33:54 +08:00
Boliang Zhang (from Dev Box)
ba70becdef Merge main into stable for 0.100 release (rev 12) 2026-06-03 17:43:12 +08:00
Mike Griese
820d98cb43 cmdpal: fix the dock window border being visible, redux (#48180)
Addenda to #47187

that fix only works if the _hwnd is already set. Actually it's crazy it
ever worked.

Tested by disconnecting and reconnecting RDP a couple times, which
pretty consistently reproduces the problem.

(cherry picked from commit c6a9ad2ad0)
2026-06-02 21:41:29 +08:00
Dustin L. Howett
cbeeefcaf9 Remove our dependency on expected-lite (#48159)
This removes our last git submodule dependency!

We were using `expected-lite` in one place, which was being compiled out
_anyway_ in favor of using `std::expected`.

(cherry picked from commit 109c63ba33)
2026-06-02 21:41:29 +08:00
Boliang Zhang
6ceb53336f Migrate spdlog from submodule to vcpkg (#48039)
## Summary

Migrate `deps/spdlog` from a git submodule to **vcpkg manifest mode**
with an overlay port pinned to the **exact same commit**
(`gabime/spdlog@616866fc`). Replaces the polyfill shim added in #47910
with a proper port-level patch.

This is the follow-up to PR #47928, which I closed after @zadjii-msft /
@DHowett clarified that the intended direction was a single combined
"move to vcpkg **and** apply a patch file" (one change, not two stepping
stones).

## Guidance honored

Per @zadjii-msft (offline):
-  Convert each submodule to vcpkg **one at a time** — this PR is
**spdlog only**. `deps/expected-lite` stays a submodule (separate PR
next).
-  Atomic commit per dep (multiple commits on the branch for review
traceability; squash on merge gives the requested single commit).
-  **Don't bump the version.** Only variable changed: submodule →
vcpkg. Same commit (`616866fc`, v1.8.5 + 38) the submodule pointed at.

Per @DHowett
([review](https://github.com/microsoft/PowerToys/pull/48039#pullrequestreview-4338835150)):
-  No vcpkg submodule — vswhere-first detection via a Terminal-style
`steps-install-vcpkg.yml` template; three-tier `VcpkgRoot` fallback (env
var → VS-shipped → runtime clone pinned to manifest baseline).

## Design

- **Repo-root manifest**: `vcpkg.json` declares only `spdlog`, with
`builtin-baseline` pinned. `vcpkg-configuration.json` registers
`deps/vcpkg-overlays/` as overlay-ports.
- **Overlay port** `deps/vcpkg-overlays/spdlog/`: `vcpkg_from_github(REF
616866fc...)` with bundled fmt preserved (`-DSPDLOG_FMT_EXTERNAL=OFF`);
the MSVC 14.51 fix from #47910 carried as a proper vcpkg patch on
`include/spdlog/fmt/bundled/format.h`.
- **vcpkg integration is global** (set in `Cpp.Build.props`, imported
via `ForceImportBeforeCppProps` for every `.vcxproj`). An earlier
attempt to make vcpkg per-project-opt-in via `deps/spdlog.props` failed
because ~85 PowerToys `.vcxproj` files import `spdlog.props` AFTER
`Microsoft.Cpp.targets`, by which point `vcpkg.props`' `ClCompile` hook
is dead-on-arrival. The trade-off (every C++ project invokes `vcpkg
install` once at build time, ~0.5 s on cache hits, manifest declares
only spdlog so install set is fixed) is documented in the expanded
`Cpp.Build.props` comment.
- **`deps/spdlog.props`** is now a thin shim that only sets the
historical `SPDLOG_*` preprocessor defines for source-compat.
- **`Cpp.Build.targets`** is a new file imported via
`ForceImportAfterCppTargets` to load `vcpkg.targets` after
`Microsoft.Cpp.targets`. A fail-fast `<Target>` errors with a clear
message if `vcpkg.props` can't be found at the resolved `VcpkgRoot`.
- **Removes** `deps/spdlog-msvc-fix/` polyfill, in-tree wrapper
`src/logging/`, spdlog submodule, the single `<ProjectReference>` in
`logger.vcxproj`, plus 3 `.slnf` refs and 2 `.slnx` refs
(`PowerToys.slnx` + `installer/PowerToysSetup.slnx`), plus 3 hard-coded
`..\deps\spdlog\include` entries in `<AdditionalIncludeDirectories>`.
- **CI**: new reusable `.pipelines/v2/templates/steps-install-vcpkg.yml`
(vswhere-first, manifest-baseline-pinned fallback clone, respects
`useVSPreview`). Gated `Cache@2` for `%LOCALAPPDATA%\vcpkg\archives`
keyed on overlay-port contents. Same vcpkg detection added to
`tools\build\build-essentials.ps1` for local devs.

## Verification

Local build matrix (all 4 configs of `logger.vcxproj` and a
representative late-import consumer):

| Config | Result | Notes |
|--------|--------|-------|
| Release \| x64    |  | vcpkg install ~21 s, `logger.lib` produced |
| Debug \| x64 |  | **Validates patch fixes the actual MSVC 14.51 bug**
(`_ITERATOR_DEBUG_LEVEL > 0` → `_SECURE_SCL`) |
| Release \| ARM64 |  | vcpkg cross-installs `arm64-windows-static`
spdlog in ~16 s |
| Debug \| ARM64 |  | **Previously DISABLED for the in-tree spdlog**
(per `<Build Solution="Debug\|ARM64" Project="false" />` in
`PowerToysSetup.slnx`); this migration FIXES that latent gap |
| FancyZonesLib (Release \| x64) |  | Late-import-pattern consumer;
previously broke in v2 |

Full PowerToys CI (x64 + arm64 + CmdPal SDK + all GitHub Actions checks)
green.

**Consumer audit**: 72 `.vcxproj` files reference `logger.vcxproj`; all
72 also import `deps/spdlog.props`. No transitive-link breakage.

## Out of scope (intentional)

- `deps/expected-lite` migration — next PR per "one-at-a-time" rule.
- Remote vcpkg binary cache (Azure Artifacts NuGet feed). Local pipeline
`Cache@2` works for now, but a remote feed survives across pipelines and
is the long-term answer. Happy to split this into a follow-up.

## Notes for review

- Patch in the overlay port is identical content to PR #47928's patch
but regenerated with LF line endings (vcpkg's `vcpkg_apply_patches` is
strict; no `--ignore-whitespace`).
- Once PowerToys eventually bumps spdlog past v1.14 (which ships fmt
10.2 and drops the affected code path), the overlay port can be deleted
and we can use upstream vcpkg's `spdlog` directly.
- Re. official-release pipelines and terrapin / less-restricted network
isolation: VS-shipped vcpkg is the primary path (no network); the
fallback clone is only exercised when VS doesn't ship vcpkg. Happy to
wire terrapin into the fallback as a follow-up if the official build
template needs it.

Closes the work tracked in #47928 (which was closed unmerged).

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
(cherry picked from commit 8a7933c0b2)
2026-06-02 21:41:29 +08:00
Niels Laute
5812b55710 Rework Power Display warning dialog (#48249)
## Summary of the Pull Request

Rework the confirmation dialog shown when enabling Power Display (and
its potentially-destructive sub-features) so it is shorter, friendlier,
and consistent across all entry points. The five separate prefix-driven
variants are now a single `PowerDisplayWarningDialog` user control
selected via an enum, sharing the title, learn-more link, and
Enable/Cancel buttons. The previous hand-rolled red warning text is
replaced with a Fluent `InfoBar Severity=""Warning""`.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

### Before
- `DangerousFeatureWarningDialog` took a resource-key prefix string and
probed up to five optional keys per variant (`_WarningHeader`,
`_WarningConfirm`, `_WarningList_Item1/2`, etc.).
- Each of the five flows (EnableModule, ColorTemperature, PowerState,
InputSource, MaxCompatibility) had a slightly different title (some
questions, some statements, mixed `Warning:` prefixes) and a hand-rolled
red `TextBlock` warning header with a `⚠️` emoji and
`SystemFillColorCriticalBrush`.
- ~30 fragmented `PowerDisplay_*_Warning*` resw keys.

### After
- New `PowerDisplayWarningDialog` selected via `PowerDisplayWarningKind`
enum (`EnableModule`, `ColorTemperature`, `PowerState`, `InputSource`,
`MaxCompatibility`).
- Shared chrome lives in the control:
  - Single title `Before you continue` for every variant.
  - `InfoBar Severity=""Warning""` replaces the hand-rolled red header.
- Learn-more `HyperlinkButton` pointing at
`aka.ms/powerToysOverview_PowerDisplay_Note` (URL is a `private const`
so translators don't see it).
  - Consistent Enable / Cancel buttons.
- Per-variant content collapses to one InfoBar message + one body
paragraph in resw (12 keys total, down from ~30). Bullets are inlined as
`• ` + newlines with `xml:space=""preserve""`.
- `PowerDisplayViewModel.ConfirmDangerousFeatureAsync` and
`TryCommitDangerousChangeAsync` now take the enum instead of a magic
string.

### Files
- **Added:** `ViewModels/PowerDisplayWarningKind.cs`,
`SettingsXAML/Views/PowerDisplayWarningDialog.xaml{,.cs}`
- **Removed:**
`SettingsXAML/Views/DangerousFeatureWarningDialog.xaml{,.cs}`
- **Updated:** `Strings/en-us/Resources.resw`,
`ViewModels/PowerDisplayViewModel.cs`,
`SettingsXAML/Views/PowerDisplayPage.xaml.cs`

## Validation Steps Performed

- Built `PowerToys.Settings.csproj` (Debug arm64) — clean.
- Manually exercised the EnableModule and MaxCompatibility flows;
verified the new title, InfoBar, body paragraph, learn-more link, and
Enable/Cancel button behavior.
- Verified `aka.ms/powerToysOverview_PowerDisplay_Note` opens in the
default browser.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 7f19817182)
2026-06-02 15:34:10 +08:00
Niels Laute
1be856c573 [KBM] Enable new editor by default (#48245)
## Summary

Make the new WinUI 3 Keyboard Manager editor the default by flipping
`useNewEditor` from `false` to `true` everywhere a default is supplied.

## Behavior

- **New installs / new `settings.json`** → new editor enabled
- **Upgrades from a version before the `useNewEditor` key existed** →
new editor enabled (the key is missing, so the default kicks in)
- **Users who have explicitly toggled the setting** → unchanged (we only
change the default, not stored values)
- The "Go back to classic" button in the KBM settings page is untouched

## Changes

- `src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs` —
`UseNewEditor` property initializer now `true`
- `src/modules/keyboardmanager/dll/dllmain.cpp` — `m_useNewEditor`
member init + `GetNamedBoolean` fallback now `true`; warn-log message
updated to match
-
`src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs`
— both fallback paths in `IsUseNewEditorEnabled` (file missing /
unreadable) now return `true`, so the "Open New Editor" CmdPal command
surfaces by default

## Validation

Built locally on ARM64 Debug (exit code 0):
- `src/modules/keyboardmanager/dll`
- `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys`
- `src/settings-ui/Settings.UI.Library`

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit a33fd3c474)
2026-06-02 15:34:09 +08:00
Niels Laute
c356d65e0d Reword Shortcut Guide module and OOBE descriptions (#48248)
## Summary of the Pull Request

Reword the Shortcut Guide module description and OOBE description so
they describe the feature without referring to `your apps`. The module
description now reads as a single sentence covering Windows and the
active app; the OOBE description is shortened to one paragraph and
refers to `various applications` instead of enumerating the bundled
apps.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

Two strings in
`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw` changed:

- `ShortcutGuide.ModuleDescription` — now: `Shows an on-screen overlay
of keyboard shortcuts for Windows and the active application.`
- `Oobe_ShortcutGuide.Description` — collapsed to one sentence
describing Windows + various applications, no longer enumerating bundled
apps or mentioning future additions.

No code or behavioral changes.

## Validation Steps Performed

- Built Settings.UI (Debug arm64) – clean.
- Verified the Shortcut Guide module card in Settings UI shows the new
description and the OOBE Shortcut Guide page shows the new paragraph.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
(cherry picked from commit b712fa4d85)
2026-06-02 15:34:09 +08:00
Michael Jolley
2dd1d457eb CmdPal: Synchronize fallback title/subtitle format for consistent scoring (#48085)
## Summary

Fixes #46055 — Standardizes built-in fallback title/subtitle format so
scoring is consistent across all action fallbacks.

## Problem

`MainListPage.cs` scores fallback items by fuzzy-matching the query
against both Title and Subtitle, but with different weights:
- `nameScore = FuzzyScore(query, Title)`
- `descriptionScore = (FuzzyScore(query, Subtitle) - 4) / 2`

Fallbacks that embedded the raw query in Title got artificially higher
scores than those using Subtitle. This made ranking unpredictable.

## Fix

All "action" fallbacks now follow a consistent pattern:
- **Title** = static action description (no query text)
- **Subtitle** = raw query string (unquoted)

"Result" fallbacks (that found a specific matched item) are left
unchanged — they correctly show the matched item name in Title.

## Full Fallback Audit (example query: `notepad`)

| Fallback | Title | Subtitle | Status |
|---|---|---|---|
| WebSearch: Search | "Search the web with Edge" | `Search for notepad`
| **Changed** — was `Search for "notepad"` in subtitle |
| WebSearch: Open URL | "Open in Microsoft Edge" | `Open notepad.com` |
**Changed** — was `Open "notepad.com"` in title |
| Shell: Run | "Run" | `notepad` | Unchanged — already correct |
| Calculator | "3" (result) | `1+2` (query) | Unchanged — intentional
exception |
| Indexer: single result | "notepad.exe" | `C:\Windows\notepad.exe` |
Unchanged — result fallback |
| Indexer: multiple results | "File search" | `Search for notepad in
files` | **Changed** — was `Search for "notepad" in files` in title |
| Windows Settings: single | "Notepad settings" | `Settings > Apps` |
Unchanged — result fallback |
| Windows Settings: multiple | "Search Windows settings..." | `Search
for notepad` | **Changed** — was `Search for "notepad" in Windows
settings` in title |
| Remote Desktop: exact match | "MyPC" (connection) | "Connect to MyPC"
| Unchanged — result fallback |
| Remote Desktop: arbitrary host | "Remote Desktop" | `Connect to
notepad-host` | **Changed** — was `Connect to notepad-host` in title |
| TimeDate | "Monday, May 23" (result) | "Current date" | Unchanged —
result fallback |
| System | "Shut down" (result) | "Shuts down the computer" | Unchanged
— result fallback |
| PowerToys | "Color Picker" (static) | "Pick a color..." | Unchanged —
result fallback |

## Changes

- `FallbackExecuteSearchItem.cs` — Subtitle uses raw query instead of
`"Search for \"{query}\""` format
- `FallbackOpenURLItem.cs` — Title shows browser name (was query),
Subtitle shows raw query (was browser)
- `FallbackOpenFileItem.cs` — Multi-result: Title is static display
name, Subtitle is raw query
- `FallbackWindowsSettingsItem.cs` — Multi-result: Title is static
description, Subtitle is raw query
- `FallbackRemoteDesktopItem.cs` — Arbitrary host: Title is static
"Remote Desktop", Subtitle is raw query

---------

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit b66b044210)
2026-06-02 11:42:13 +08:00
Michael Jolley
bb276d3be9 CmdPal: Fix dock subtitle visibility in compact mode after async update (#48088)
## Summary

When an extension updates its `Subtitle` property asynchronously after
initial render, the `TextVisibilityStates` visual state group
transitions from `TitleOnly` → `TextVisible`. This transition sets
`SubtitleText.Visibility = Visible`, overriding the `CompactStates`
setter that had hidden it.

## Fix

Added `control.UpdateCompactState()` to `OnTextPropertyChanged` in
`DockItemControl.xaml.cs`. This re-applies the compact state after any
text property change. When `IsCompact` is `false`, `UpdateCompactState`
is a no-op — no behavior change for the non-compact path.

Fixes #47980

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 18919eaa40)
2026-06-02 11:42:12 +08:00
Michael Jolley
b7aca7c3fc CmdPal: Reorder dock network stats to match Task Manager order (#48098)
## Summary

Fixes #47939

The Performance Monitor dock band displayed network stats as Receive →
Send, but Task Manager shows Send → Receive. This swaps the order to
match Task Manager.

## Changes

- `PerformanceWidgetsPage.cs`: Swap `_networkUpItem` (Send) before
`_networkDownItem` (Receive) in the band items array.

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit e0854fbaf3)
2026-06-02 11:42:12 +08:00
Michael Jolley
8e3a88df2c CmdPal: Fix hotkey navigation when palette is showing transient dock page (#48089)
## Summary

When a keyboard shortcut opens CmdPal to an extension while the palette
is already showing a dock-launched transient page, `GoHome(false)`
cannot restore the root page — the frame's back stack is empty because
the transient dock page was never pushed on top of root. The user ends
up with only the hotkey-target page in the frame with no way to navigate
back to the main list.

## Root Cause

In `ShellPage.SummonOnUiThread()`, the hotkey-to-page branch called
`GoHome(false)` before sending `ShowWindowMessage`. But when the active
page is a transient dock page, `_currentlyTransient` is still `true` and
the frame back stack is empty, so `GoHome` can't re-establish the root
page as the frame base.

## Fix

Added `ResetToHome()` to `ShellViewModel`, mirroring the pattern already
used in `WindowHiddenMessage` handling:
1. Clears `_currentlyTransient`
2. Calls `_rootPageService.GoHome()` to reset extension state
3. Sends `PerformCommandMessage` for `_rootPage` — navigating
MainListPage into the frame as the base

In `ShellPage.SummonOnUiThread()`, the `GoHome(false)` call in the
`isPage` branch is replaced with `ViewModel.ResetToHome()`. The root
page is then cleanly in the frame before the hotkey target's
`PerformCommandMessage` navigates on top of it.

Fixes #47994

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit ba20da1611)
2026-06-02 11:42:12 +08:00
Niels Laute
fb028e8fdc CmdPal: Reorder Pin to Dock dialog so controls precede the preview (#48250)
## Summary of the Pull Request

Reorder the Pin to Dock dialog content so the configuration controls
(monitor selector, dock section, label options) appear at the top and
the live preview is shown below them. The user now configures the pin
first and sees the resulting preview directly underneath, instead of
staring at the preview and having to scan past it to find the controls.

<img width="515" height="440" alt="image"
src="https://github.com/user-attachments/assets/0d1d0543-2b30-48f5-a1aa-676a165870f5"
/>

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

XAML-only change to
`src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/PinToDockDialogContent.xaml`.
The new visual order inside the `ScrollViewer`/`StackPanel` is:

1. Monitor selector (still `Visibility=""Collapsed""` by default; shown
when more than one monitor is available)
2. Dock section `Segmented` (Start / Center / End)
3. Label options (`Show title` / `Show subtitle` checkboxes)
4. Divider `Rectangle`
5. Preview `Border`

No logic, bindings, `x:Name` identifiers, event handlers, or `x:Uid`
keys are changed.

## Validation Steps Performed

- Built `Microsoft.CmdPal.UI` (Debug arm64) — clean.
- Verified the Pin to Dock dialog renders with controls on top and the
preview underneath; segmented selection, label-option checkboxes, and
the multi-monitor combo still behave as before.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit 35f2ed839e)
2026-06-02 11:42:12 +08:00
Michael Jolley
d980697849 CmdPal: Fix GPU index out of range crash in PerfMon widget (#48103)
## Summary

Fixes #47821

The GPU Performance Monitor widget crashes with
`IndexOutOfRangeException` on systems where GPU performance counters
fail to enumerate (common on Intel Arc and hybrid GPU configurations).
The dock band shows `???` and opening the flyout causes an error.

## Root Cause

`GPUStats.CreateGPUImageUrl()` accessed `_stats[index]` without bounds
checking. When `GetGPUPerfCounters()` finds no matching counter
instances, `_stats` remains empty but callers still pass index 0.

`GetGPUName()`, `GetGPUUsage()`, and `GetGPUTemperature()` already have
proper guards (`if (_stats.Count <= index) return ...`) — this fix adds
the same pattern to the one remaining unguarded method.

## Changes

- `GPUStats.cs`: Add bounds check to `CreateGPUImageUrl()` — return
empty string if index out of range

Co-authored-by: root <root@io.bbq>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit e20b5b9c51)
2026-06-02 11:42:12 +08:00
Mike Griese
d42b15f380 cmdpal: fix the dock window border being visible, redux (#48180)
Addenda to #47187

that fix only works if the _hwnd is already set. Actually it's crazy it
ever worked.

Tested by disconnecting and reconnecting RDP a couple times, which
pretty consistently reproduces the problem.

(cherry picked from commit c6a9ad2ad0)
2026-06-02 11:41:59 +08:00
Muyuan Li
b49f722e88 Fix ShortcutGuide v2 crash when Manifests directory is missing (#48171)
## Summary

Fixes #48170 — ShortcutGuide v2 crashes on launch when the bundled
`Manifests` directory is absent from the install path.

### Root Cause

The `Assets\ShortcutGuide\Manifests\*.yml` files were never reaching the
build output directory during the CI solution-level build (`msbuild
PowerToys.slnx /t:Build -graph`). The `CopyToOutputDirectory` metadata
on `<Content>` items does not reliably copy files to a shared
`OutputPath` in this build configuration. As a result, the WiX installer
generator found no yml files to package, and the installed product was
missing the Manifests directory entirely.

At runtime, `PowerToysShortcutsPopulator.Populate()` threw an unhandled
`FileNotFoundException` causing a crash loop.

### Fix (3 layers)

1. **Code resilience** (`Program.cs`, `PowerToysShortcutsPopulator.cs`):
- Wrap `Populate()` in try/catch so a missing manifest degrades
gracefully instead of crashing
   - Add `File.Exists` guard before `File.ReadAllText`

2. **Build output** (`ShortcutGuide.Ui.csproj`):
- Add explicit `CopyManifestsToOutputDir` MSBuild target
(`AfterTargets="Build"`) that copies yml files to
`$(OutDir)Assets\ShortcutGuide\Manifests\` — same pattern as the
existing `CopyPRIFileToOutputDir` target
- Keep `<Content Include>` with `CopyToOutputDirectory` as a fallback
for publish scenarios

3. **Installer packaging** (`generateAllFileComponents.ps1`,
`ShortcutGuide.wxs`):
   - Add `*.yml` to the file inclusion list
- Add `Generate-FileList` / `Generate-FileComponents` calls for
`ShortcutGuideManifestsFiles`
- Add WiX directory definition and `RemoveFolder` component for the
Manifests directory

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
(cherry picked from commit a67fc2d9b7)
2026-06-01 16:32:35 +08:00
Jessica Dene Earley-Cha
c2ea654b3c [CmdPal] Toggle "Show details" / "Hide details" with icon in context menu (#48140)
<!-- 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

Converts the "Show details" context menu command into a toggle that
switches between "Show details" and "Hide details" with appropriate
icons, and fixes the icon not rendering in the context menu.

Address internal a11y bug.

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

(cherry picked from commit c78f6e52a0)
2026-06-01 16:32:35 +08:00
thetsaw
80e0a4d09e Add DiskAnalyzer to third-party Run plugins list (#48106)
## Summary of the Pull Request

Adds **DiskAnalyzer** to the General plugins table in
`doc/thirdPartyRunPlugins.md`.

- **Plugin:**
[Community.PowerToys.Run.Plugin.DiskAnalyzer](https://github.com/thetsaw/PowerToys.Plugin)
- - **Author:** thetsaw
- - **Keyword:** `ds`
- - **License:** MIT
- - **Platforms:** x64 and ARM64
### What it does
Scan any folder or drive to find the largest files and subfolders, view
drive usage with visual progress bars, and navigate your filesystem all
from PowerToys Run.

## PR Checklist

- [x] Plugin has been publicly available
- [ ] - [x] MIT licensed
- [ ] - [x] Releases include x64 and ARM64 zips
- [ ] - [x] plugin.json is correctly formatted
- [ ] - [x] README includes install instructions
## Detailed Description

This is a documentation-only change adding one row to the third-party
plugins table. No source code, binaries, or build files are modified.

(cherry picked from commit 9a55209d13)
2026-05-29 12:49:14 +08:00
Copilot
8c93854bbb Rename Settings UI label from “Shortcut Guide V2” to “Shortcut Guide” (#48151)
## Summary of the Pull Request

This updates PowerToys Settings to remove the obsolete “V2” suffix from
the Shortcut Guide module name. The UI now consistently shows **Shortcut
Guide**.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

- **Settings navigation label**
  - Updated `Shell_ShortcutGuide.Content` to `Shortcut Guide`.
- **Module title**
  - Updated `ShortcutGuide.ModuleTitle` to `Shortcut Guide`.
- **OOBE title**
  - Updated `Oobe_ShortcutGuide.Title` to `Shortcut Guide`.

```xml
<data name="Shell_ShortcutGuide.Content" xml:space="preserve">
  <value>Shortcut Guide</value>
</data>
```

## Validation Steps Performed

- N/A for behavior-level validation in this description (change is
limited to localized display strings).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
(cherry picked from commit 0cb6fe250b)
2026-05-29 12:49:14 +08:00
moooyo
1aba1e170c Remove "NEW" tag from Power Display and Grab and Move (#48174)
## Summary of the Pull Request
Both Power Display and Grab and Move have matured beyond their initial
release phase. This removes the "NEW" `InfoBadge` from their navigation
items in Settings, the two parent navigation groups (Windowing &
Layouts, Input / Output) that surfaced the badge when collapsed, and
clears the `IsNew` flag for Power Display in the OOBE shell.

<img width="1867" height="973" alt="image"
src="https://github.com/user-attachments/assets/533f271c-c70f-414f-a76a-43fd9ffbbd44"
/>
<img width="497" height="575" alt="image"
src="https://github.com/user-attachments/assets/fe1e97c3-c806-4f42-a836-76e042630d61"
/>
<img width="1619" height="1027" alt="image"
src="https://github.com/user-attachments/assets/f5db715b-bc69-4505-803a-18a9b2716280"
/>

## PR Checklist

- [x] Closes: #48153
- [x] **Communication:** Tracked by the linked issue
- [x] **Tests:** Markup-only change; Settings.UI builds clean with WinUI
markup compiler (no XAML errors)
- [x] **Localization:** No end-user-facing strings changed
- [ ] **Dev docs:** N/A
- [ ] **New binaries:** N/A
- [ ] **Documentation updated:** N/A

## Detailed Description of the Pull Request / Additional comments
Files touched:
- `src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml` —
removed four `<InfoBadge Style="{StaticResource NewInfoBadge}" />`
blocks on `GrabAndMoveNavigationItem`, `PowerDisplayNavigationItem`, and
on the two parent group headers `WindowingAndLayoutsNavigationItem` and
`InputOutputNavigationItem` (the parent badges existed only to surface a
NEW child when the group was collapsed; with no NEW children left in
those groups, the parent badges are now stale).
- `src/settings-ui/Settings.UI/OOBE/ViewModel/OobeShellViewModel.cs` —
flipped `(PowerToysModules.PowerDisplay, true)` to
`(PowerToysModules.PowerDisplay, false)`. Grab and Move was already
`false`.

No other modules or strings affected.

## Validation Steps Performed
- Built `src\settings-ui\Settings.UI\PowerToys.Settings.csproj`
(Release|x64) with MSBuild from VS 18 Enterprise;
`PowerToys.Settings.dll` produced with 0 errors related to this change.
WinUI markup compiler would have aborted before producing the DLL if the
XAML had syntax issues.
- Diff inspected: only the five intended deletions/edits, no collateral
changes.
- Visual run-time verification of the Settings navigation pane is
recommended before merge.

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit c0cb9417ad)
2026-05-29 12:49:14 +08:00
moooyo
c5a18aa488 [PowerDisplay] Fix false-positive crash detection on cooperative shutdown (#48173)
<!-- 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

Cooperative shutdowns of `PowerDisplay.exe` — Runner's `TerminateApp`
NamedPipe message, the `Terminate` named event, tray-quit, Runner-exit
detection, and PowerToys upgrades — all call `Environment.Exit(0)`
immediately. If DDC/CI discovery is mid-flight, that path skips the
`try/finally` that owns `CrashDetectionScope`, leaving `discovery.lock`
on disk. Phase 0 at the next `PowerDisplay.exe` startup then treats this
orphan as evidence of a real crash and auto-disables the module,
surfacing the "PowerDisplay has crashed" InfoBar in Settings UI.

This PR adds an `AppDomain.ProcessExit` safety-net inside
`CrashDetectionScope`. ProcessExit fires for `Environment.Exit` but
**not** for `FailFast` / BSOD / external `TerminateProcess` — exactly
the partition we need: cooperative exit → best-effort delete the lock;
involuntary kill → leave the lock for Phase 0 to detect (original design
intent preserved).

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

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

### Root cause

`CrashDetectionScope.Begin()` writes `discovery.lock` before DDC/CI
capability fetch and `Dispose()` deletes it when the `using` block
exits. The lock is intentionally designed to survive any code path that
cannot run user-mode cleanup (BSOD, kernel OOM, `TerminateProcess`), so
that the next `PowerDisplay.exe` start can see it and run Phase 0 (write
`crash_detected.flag`, set `enabled.PowerDisplay=false` in global
`settings.json`, signal `AutoDisablePowerDisplayEvent`).

The bug is that several **cooperative** shutdown paths route to
`Environment.Exit(0)` immediately:

| Path | Code |
|---|---|
| Runner's `TerminateApp` NamedPipe | `App.xaml.cs::OnNamedPipeMessage`
→ `Shutdown()` → `Environment.Exit(0)` |
| `Terminate` named event | `App.xaml.cs::OnLaunched` →
`RegisterEvent(..., () => Environment.Exit(0), "Terminate")` |
| Tray-quit | `TrayIconService` callback → `Environment.Exit(0)` |
| Runner-exit detection | `RunnerHelper.WaitForPowerToysRunner` callback
→ `Environment.Exit(0)` |

`Environment.Exit` calls `ExitProcess` under the hood, which terminates
all threads abruptly. Background `Task.WhenAll` doing DDC capability
fetch is killed mid-flight; the `finally` block that calls
`scope.Dispose()` never runs; `discovery.lock` orphans; Phase 0 next
time false-positives.

Concrete repro from logs:
- `15:08:42.510` lock written
- `15:08:42.79` probe monitor #1
- `15:08:46.92` probe monitor #2 (started, not finished — typical probe
takes ~5s)
- `15:08:49.03` `TerminateApp` received → `Environment.Exit(0)` → no
`Dispose` log line
- `15:10:10.03` next startup: Phase 0 sees orphan lock with `pid:17712,
startedAt:2026-05-28T07:08:42Z` → writes `crash_detected.flag` →
auto-disables

### Fix

`CrashDetectionScope.Begin()` now also subscribes to
`AppDomain.CurrentDomain.ProcessExit`. The handler does a best-effort
`File.Delete(_lockPath)` (swallowing exceptions, as required for
ProcessExit handlers). `Dispose()` unsubscribes before deleting. An
`Interlocked.Exchange` guards the race between Dispose and ProcessExit
so only one of the two performs the delete.

ProcessExit's semantics match the cooperative/involuntary partition
exactly:

| Shutdown path | ProcessExit fires? | Behavior after this PR |
|---|---|---|
| `Environment.Exit(code)` (all 4 paths above) | yes | lock deleted by
handler |
| `Environment.FailFast` | no | lock survives → Phase 0 catches it
(correct: explicit FailFast = real failure) |
| BSOD / external `TerminateProcess` / kernel OOM | no | lock survives →
Phase 0 catches it (correct: original design) |
| Discovery completes normally / throws | n/a | `try/finally` calls
`Dispose()` as before; handler unsubscribed first |

### Testability

A new `IProcessExitHook` interface abstracts the subscription so unit
tests can simulate ProcessExit without terminating the test runner.
Production code uses the default `AppDomainProcessExitHook` singleton;
tests inject a fake whose `RaiseExit()` invokes subscribed handlers
synchronously.

### Files touched

-
`src/modules/powerdisplay/PowerDisplay.Lib/Services/IProcessExitHook.cs`
*(new)* — interface + production singleton
-
`src/modules/powerdisplay/PowerDisplay.Lib/Services/CrashDetectionScope.cs`
— subscribe in `Begin`, unsubscribe in `Dispose`, add `OnProcessExit`
handler, expanded class doc
-
`src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/CrashDetectionScopeTests.cs`
*(new)* — 10 unit tests

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

### Automated

10 new unit tests in `CrashDetectionScopeTests`, all passing:

```
Passed Begin_WritesLockFileAtomically
Passed Begin_SubscribesToProcessExit
Passed Dispose_UnsubscribesFromProcessExit
Passed Dispose_DeletesLockFile
Passed ProcessExitFired_BeforeDispose_DeletesLock      (core scenario)
Passed ProcessExitFired_AfterDispose_DoesNothing
Passed Dispose_AfterProcessExit_DoesNotThrow
Passed ProcessExitFired_LockFileMissing_DoesNotThrow
Passed Dispose_IsIdempotent
Passed MultipleScopes_DoNotShareState
```

Full `PowerDisplay.Lib.UnitTests` suite: **129 / 132 passing**. The 3
failures (`DetectOrphanAndDisable_RunsFullSequenceWhenOrphanPresent`,
`DetectOrphanAndDisable_HandlesUnknownVersionAsOrphan`,
`DetectOrphanAndDisable_LeavesLockIntactOnSignalFailure`) are
**pre-existing on `main`** — they fail with `REGDB_E_CLASSNOTREG` from
`Constants.AutoDisablePowerDisplayEvent()` (WinRT activation factory not
COM-registered in the test environment). Verified by stashing this PR's
changes and re-running the same 3 tests on baseline `main` — same
failures, same cause, unrelated to this change.

### Manual

1. Reproduced the original false-positive on `main`:
- Enable PowerDisplay → open Settings UI → quickly toggle PowerDisplay
off
- Observe `discovery.lock` left in
`%LOCALAPPDATA%\Microsoft\PowerToys\PowerDisplay\`
- Re-enable PowerDisplay → Phase 0 writes `crash_detected.flag` →
InfoBar appears
2. Repeated the same steps with this branch:
- Toggling PowerDisplay off cleanly deletes `discovery.lock`
(ProcessExit handler ran)
- Re-enabling PowerDisplay shows no InfoBar, no `crash_detected.flag`
created
3. BSOD path is unchanged (verified by inspecting the conditional logic
— `AppDomain.ProcessExit` does not fire for involuntary terminations;
the lock survives just as before).

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit cd5027fa1a)
2026-05-29 12:49:14 +08:00
Copilot
df019c09e6 Rename OOBE overview Learn link label to “Documentation” (#48155)
## Summary of the Pull Request

Renames the OOBE welcome/overview hyperlink label from **“Documentation
on Microsoft Learn”** to **“Documentation”** for brevity and
consistency.
Scope is limited to the localized string resource used by the OOBE
overview page.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

- **Resource update (OOBE Overview)**
- Updated `Oobe_Overview_DescriptionLinkText.Text` in
`src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`.

```xml
<data name="Oobe_Overview_DescriptionLinkText.Text" xml:space="preserve">
  <value>Documentation</value>
</data>
```

## Validation Steps Performed

- Confirmed the OOBE overview string key now resolves to
**“Documentation”**.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
(cherry picked from commit 7da62cdb0a)
2026-05-29 12:49:14 +08:00
Eymard Silva
d97301f06f Handle complex calculator results (#47506)
## Summary of the Pull Request

Return a friendly calculator error when Mages evaluates an expression to
a complex number instead of letting decimal conversion throw.

This fixes the PowerToys Run Calculator result for expressions such as
`sqrt(-1)` by detecting `System.Numerics.Complex` results before decimal
conversion and showing a localized error message instead.

Fixes #43937

## PR Checklist

- [x] Closes: #43937
- [ ] **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
- [x] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places
- [x] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [x] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [x] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [x] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [x] **Documentation updated:** Not required for this bug fix.

## Detailed Description of the Pull Request / Additional comments

The Calculator plugin previously passed complex results from Mages into
`Convert.ToDecimal`, which caused an exception for expressions like
`sqrt(-1)`.

This PR updates the calculator result transformation logic to detect
`System.Numerics.Complex` and return a localized user-facing error
message: `Complex numbers are not supported`.

It also updates calculator query tests to cover both direct keyword and
global query behavior.

## Validation Steps Performed

- Added unit test coverage for `=sqrt(-1)` returning `Complex numbers
are not supported`.
- Added unit test coverage for global query `sqrt(-1)` returning no
result instead of surfacing an unhandled exception.
- Ran `git diff --check`.
- Attempted local build/test with the PowerToys build scripts, but local
validation was blocked by Visual Studio/VC tooling configuration issues
unrelated to this change: `PlatformToolsetVersion` resolves to an empty
value during restore/build.

(cherry picked from commit c46083dd8d)
2026-05-29 12:49:13 +08:00
Mike Griese
86ca7c5661 Move CmdPal API spec back to cmdpal/ directory (#48160)
Reverts 0819a62 / #46926

The cmdpal API is literally generated from this spec document. It needs
to live with the rest of the code to work correctly.

Docs for authoring cmdpal extensions are on
https://learn.microsoft.com/en-us/windows/powertoys/command-palette/extension-development,
and we should direct docs commentary there.

(cherry picked from commit 65112a7b05)
2026-05-29 12:49:13 +08:00
🄂ʏᴇᴅ 🄰ʙᴅᴜʟ 🄰ᴍᴀ🄝 ✧
e37d1d5d04 Fix project template settings heading (#48148)
## Summary
- Fix a grammar typo in the PowerToy project template README heading.
- Change "Settings Informations" to "Settings Information".

## Validation
- Ran `git diff --check`.

(cherry picked from commit 6be6509c46)
2026-05-29 12:48:44 +08:00
Boliang Zhang (from Dev Box)
ac6aa80401 Merge main into stable for 0.100 release (rev 6) 2026-05-26 15:19:20 +08:00
Boliang Zhang (from Dev Box)
c4a83be733 Merge main into stable for 0.100 release (rev 5) 2026-05-26 11:28:54 +08:00
Boliang Zhang (from Dev Box)
34d01e998c Merge main into stable for 0.100 release (rev 4) 2026-05-25 13:03:13 +08:00
Boliang Zhang (from Dev Box)
b1adc8548f Merge main into stable for 0.100 release (rev 3) 2026-05-23 00:06:56 +08:00
Boliang Zhang (from Dev Box)
0e71f616de Merge main into stable for 0.100 release (rev 2) 2026-05-22 14:04:49 +08:00
Boliang Zhang (from Dev Box)
4f0f614f67 Merge main into stable for 0.100 release 2026-05-21 13:58:32 +08:00
Boliang Zhang (from Dev Box)
ddf9815613 Merge remote-tracking branch 'origin/main' into stable 2026-05-19 10:55:40 +08:00
Boliang Zhang (from Dev Box)
184ccb75ec Merge origin/main into stable 2026-04-29 13:38:21 +08:00
Boliang Zhang (from Dev Box)
455dc52430 Merge origin/main into stable 2026-04-26 21:05:06 +08:00
Boliang Zhang
ed1d15f9a1 Fix: Install CommandPalette.Extensions.winmd to WinUI3Apps for COM marshalling (#47210)
## Summary

Follow-up fix for #47177. Installs `CommandPalette.Extensions.winmd` to
the `WinUI3Apps\` directory (ExternalLocation) instead of the root
install folder.

## Problem

PR #47177 moved the sparse package's `ExternalLocation` from root to
`WinUI3Apps\`, but the `CommandPalette.Extensions.winmd` was still
installed to root via `DirectoryRef=INSTALLFOLDER`. The WinRT runtime
needs this winmd in the ExternalLocation directory for COM proxy/stub
creation during cross-process CmdPal extension activation. Without it,
`CoCreateInstance` returns `E_NOINTERFACE` and the PowerToys extension
fails to load in Command Palette.

## Fix

Split the `BaseApplications.wxs` `DirectoryRef` into two blocks:
- **winmd component** -> `WinUI3AppsInstallFolder` (for WinRT COM
marshalling)
- **auto-generated components** -> `INSTALLFOLDER` (unchanged, avoids
ICE30 conflict with `WinUI3ApplicationsFiles`)

## Validation

- [x] Local WiX compilation: no ICE30 errors
- [x] ADO CI build 145429943 (v0.99.1): passed
- [x] Manual verification on 25H2: CmdPal loads 55 commands after winmd
placed in WinUI3Apps

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-25 23:48:22 +08:00
Boliang Zhang (from Dev Box)
31da4fa112 Merge origin/main into stable 2026-04-24 22:38:59 +08:00
Boliang Zhang (from Dev Box)
3ee7518850 Merge origin/main into stable 2026-04-24 22:34:19 +08:00
Boliang Zhang (from Dev Box)
1bfa42b270 Merge origin/main into stable 2026-04-24 14:15:50 +08:00
Boliang Zhang (from Dev Box)
e0979fce59 Merge origin/main into stable 2026-04-24 10:59:26 +08:00
Boliang Zhang (from Dev Box)
b2684f74f5 Merge origin/main into stable 2026-04-24 10:55:14 +08:00
Muyuan Li
b67e11d000 Fix CmdPal crash when typing in search box (#47148)
Add reentrancy guard for FilteredItems ObservableCollection mutations.

WinUI3's native XAML renderer can pump the message loop while processing
a CollectionChanged notification from InPlaceUpdateList. This allows a
second DoOnUiThread task to begin mutating FilteredItems while the first
is still mid-update, causing heap corruption and an access violation
(0xc0000005) in ntdll.dll.

The fix introduces RunFilteredItemsUpdate() which uses a boolean flag to
detect same-thread reentrancy (C# lock is reentrant so _listLock cannot
prevent this). When reentrancy is detected, only the latest pending
update is stored and executed after the in-flight mutation completes,
ensuring the UI converges to the newest state without overlapping
mutations.

Fixes: 100% reproducible crash in CmdPal when typing any character in
the search box (build ID 145015494).

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

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

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-23 17:19:11 +08:00
Boliang Zhang (from Dev Box)
a5bc91028f Merge remote-tracking branch 'origin/main' into stable 2026-04-23 16:18:06 +08:00
Boliang Zhang (from Dev Box)
1eebd47af0 Merge remote-tracking branch 'origin/main' into stable 2026-04-22 17:18:11 +08:00
Boliang Zhang (from Dev Box)
1f840000f5 Merge remote-tracking branch 'origin/main' into stable 2026-04-21 14:29:24 +08:00
Boliang Zhang (from Dev Box)
04fe688e7a Merge remote-tracking branch 'origin/main' into stable 2026-04-20 16:34:06 +08:00
Boliang Zhang (from Dev Box)
8fc31de489 Merge main into stable for 0.99 release 2026-04-16 15:36:07 +08:00
Boliang Zhang (from Dev Box)
0a510119c8 Remove duplicate PowerDisplay model classes after merge from main
The merge from main added a PowerDisplay.Models project reference to
Settings.UI.Library, but the old duplicate model classes (CustomVcpValueMapping
and ColorPresetItem) in Settings.UI.Library/Models/ were not removed, causing
CS0104 ambiguous reference errors. Additionally, two files had stale using
aliases and a XAML DataTemplate referenced the old namespace.

Changes:
- Delete Settings.UI.Library/Models/CustomVcpValueMapping.cs (duplicate)
- Delete Settings.UI.Library/Models/ColorPresetItem.cs (duplicate)
- Update PowerDisplayPage.xaml.cs and PowerDisplayViewModel.cs using directives
- Update PowerDisplayPage.xaml x:DataType to use pdmodels namespace

Verified with local build of both Settings.UI and PowerDisplay projects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-14 11:08:53 +08:00
Boliang Zhang (from Dev Box)
38dfc55a2d Merge branch 'main' into stable
# Conflicts:
#	src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs
#	src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs
#	src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
#	src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
#	src/modules/keyboardmanager/common/Helpers.cpp
#	src/settings-ui/Settings.UI.Library/MonitorInfo.cs
#	src/settings-ui/Settings.UI.Library/PowerDisplayProperties.cs
#	src/settings-ui/Settings.UI.Library/Settings.UI.Library.csproj
#	src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs
#	src/settings-ui/Settings.UI/SettingsXAML/Views/CustomVcpMappingEditorDialog.xaml.cs
2026-04-13 16:33:34 +08:00
Niels Laute
89b2dfe9ac [KBM] Manual key selection — code review fixes (#46377)
Addresses code review feedback on the KBM manual key selection feature.
No new user-facing behavior; all changes are correctness, robustness,
and maintainability fixes.

## Summary of the Pull Request

- **Localization:** All hard-coded `RemappingDialog.Title` assignments
replaced with `ResourceHelper.GetString()`; added
`RemappingDialog_TitleEdit` resource key
- **VK_DISABLED centralization:** Replaced scattered `0x100`/`"256"`
literals and local `const string vkDisabledCode` with `private const int
VkDisabled = 0x100` / `private const string VkDisabledString = "256"` on
`MainPage`
- **Disable action validation:** Added
`ValidationHelper.ValidateDisableMapping()` — same trigger-key rules as
other action types (empty keys, modifier-only, illegal shortcuts,
duplicates, conflicting modifier variants); wired into
`ValidateMapping()` switch
- **Binding-safe dropdown revert:** `TriggerKeyDropDown_KeyChanged` /
`ActionKeyDropDown_KeyChanged` no longer set `dropDown.KeyName =
e.OldKeyName` on failure (breaks `{Binding}` expression); now use
`RevertKeySelection(keys, index)` which does
`ObservableCollection.RemoveAt` + `Insert` to force a binding-tracked
refresh without touching the DP directly. `NewKeyCode == 0` ("None") is
rejected via the same path
- **Dropdown validation:** `ValidateDropDownSelection` skips
`string.IsNullOrEmpty` placeholder slots (added by
`HandleAutoGrowShrink`) when checking repeated-modifier and max-size
rules
- **`SetActionType`:** Replaced hard-coded `SelectedIndex` with
tag-matching iteration over `ActionTypeComboBox.Items`; immune to XAML
item reorder
- **`ServiceStatusHelper`:** Dispose all `Process` objects returned by
`GetProcessesByName` before returning; prevents handle accumulation on
the 3-second polling timer
- **`KeyDropDownButton.GetKeyList()`:** Filter out `KeyCode == 0`
entries (native "None" sentinel for shortcut lists) before caching
- **`SettingsManager`:** `_mappingService!` used consistently in
`CreateSettingsFromKeyboardManagerService`
- **`KeyboardHookHelper`:** Constructor catch broadened from
`DllNotFoundException or InvalidOperationException` to `Exception`

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

The `RevertKeySelection` pattern: in WinUI, assigning directly to a
bound `DependencyProperty` overwrites the binding expression. Using
`ObservableCollection.RemoveAt` + `Insert` instead raises
`CollectionChanged(Replace)`, causing the binding to re-read from the
source without clearing the expression.

```csharp
private static void RevertKeySelection(ObservableCollection<string> keys, int index)
{
    string current = keys[index];
    keys.RemoveAt(index);
    keys.Insert(index, current);
}
```

## Validation Steps Performed

Manually verified: dropdown key selection and revert on invalid
selection, Disable mapping save/load with the new validation, "None"
absent from key picker flyout, `SetActionType` correctly selects items
with preserved XAML order.

---------

Co-authored-by: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-04-09 13:10:05 -04:00
Jaylyn Barbee
2ef65e7d63 [KBM] Fixes to text replacement issues (#46794)
<!-- 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 attempts to fix some of the issues that were introduced in
0.98.0 with text replacement

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

- [x] Closes: #46498
- [x] Closes: #46440
- [x] Closes: #46366

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments
In 0.98.0 I made a change to support multiline text replacement using
Ctrl + V. This was very inconsistent so I have reverted back to
_basically_ the same approach we were using before except now when we
encounter a newline indicator we send "Shift + Enter" so that this works
in chat boxes and plan editors.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Manual testing with single line and multiline replacements in Teams,
Terminal, Git bash, Edge, etc
2026-04-09 13:10:05 -04:00
Zach Teutsch
3dc5b3a3ed [Keyboard Manager] Remove service enable/disable separate from module, fix editor clear shortcut (#46530)
Two changes to shortcuts here:
1) Remove toggling the KBM service with a shortcut or via command
palette
2) Ensure that shortcut is disabled for editor when shortcut is cleared
2026-03-26 13:35:11 -04:00
Zach Teutsch
fc5b65c5c3 [Keyboard Manager] Allow whitespace-only TextRemappings (#46510)
Title.

Closes #46453
2026-03-26 13:35:11 -04:00
Zach Teutsch
da5448b169 fix merge mixup for hotfix 0.98.1 2026-03-24 22:17:10 -04:00
Zach Teutsch
1ab685ee07 Merge 0.98.1 hotfixes into stable
Cherry-picked commits:
- Make KBM Editor pinnable (#46482)
- CmdPal: Fix missing primary context command for late-bound items (#46131)
- CmdPal: Ensure DockWindow property cleans up after itself (#46303)
- CmdPal: Hotfix commonCallbacks array initial count to prevent negative number (#46215)
- CmdPal: Fix missing app context menu actions on the main page (#46293)
- CmdPal: Fix dock popup XamlRoot handling on DockControl (#46305)
- CmdPal: Reduce DockWindow backdrop switching and visual artifacts (#46309)
- Always On Top: The opacity should be able to configure the hotkey individually (#46410)
- [OOBE] Ensure the Settings button on the SCOOBE page opens Home, not a blank page (#46203)
- CmdPal: Fix scroller scrolling and down glyph (#46447)
- [Settings] Decouple Settings.UI.Library from PowerDisplay.Lib to fix (#46325)
2026-03-24 21:25:13 -04:00
Zach Teutsch
cf137ccbbc Merge branch 'main' into stable 2026-03-16 21:39:04 -04:00
Zach Teutsch
2cc051ee33 Merge branch 'main' into stable 2026-03-12 14:29:12 -04: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
135 changed files with 1734 additions and 7319 deletions

View File

@@ -416,7 +416,6 @@ DISPLAYFLAGS
DISPLAYFREQUENCY
displayname
DISPLAYORIENTATION
DISPLAYPORT
divyan
DLGFRAME
dlgmodalframe
@@ -863,7 +862,6 @@ jjw
jobject
JOBOBJECT
jpe
JPN
jpnime
jrsoftware
Jsons
@@ -996,7 +994,6 @@ LTM
LTRREADING
luid
lusrmgr
LVDS
LWA
LWIN
LZero
@@ -1061,8 +1058,6 @@ MINIMIZESTART
MINMAXINFO
minwindef
Mip
Miracast
miracast
mkdn
mlcfg
mmc
@@ -1391,6 +1386,7 @@ popups
POPUPWINDOW
portfile
POSITIONITEM
Postbot
POWERBROADCAST
powerdisplay
POWERDISPLAYMODULEINTERFACE
@@ -1821,7 +1817,6 @@ svchost
SVGIn
SVGIO
svgz
SVIDEO
SVSI
SWFO
swp

View File

@@ -238,7 +238,6 @@
"PowerToys.WorkspacesLauncherUI.dll",
"PowerToys.WorkspacesModuleInterface.dll",
"PowerToys.WorkspacesCsharpLibrary.dll",
"WorkspacesSettingsService\\PowerToys.PTSettingsSvc.exe",
"WinUI3Apps\\PowerToys.RegistryPreviewExt.dll",
"WinUI3Apps\\PowerToys.RegistryPreviewUILib.dll",

View File

@@ -59,28 +59,6 @@ steps:
**/PowerToysSetupCustomActionsVNext.dll
**/SilentFilesInUseBAFunction.dll
# Pack the PTSettingsSvc MSIX from the ALREADY-SIGNED service binary (core ESRP
# signing ran before this template) and then sign the package itself, so the
# per-user installer can stage a signed, immutable service package (Design
# §12.1). Must run after core signing and before the MSI build, which embeds
# the .msix as the per-user payload.
- pwsh: |-
$svcDir = "$(Build.SourcesDirectory)\$(BuildPlatform)\$(BuildConfiguration)\WorkspacesSettingsService"
& "$(Build.SourcesDirectory)\src\modules\Workspaces\WorkspacesSettingsService\devtools\build-msix.ps1" `
-ExePath "$svcDir\PowerToys.PTSettingsSvc.exe" `
-OutMsix "$svcDir\PTSettingsSvc.msix" `
-Version "${{ parameters.versionNumber }}" `
-Arch "$(BuildPlatform)"
displayName: Pack PTSettingsSvc MSIX
- ${{ if eq(parameters.codeSign, true) }}:
- template: steps-esrp-sign-files-authenticode.yml
parameters:
displayName: Sign PTSettingsSvc MSIX
signingIdentity: ${{ parameters.signingIdentity }}
folder: '$(BuildPlatform)\$(BuildConfiguration)\WorkspacesSettingsService'
pattern: '**/PTSettingsSvc.msix'
## INSTALLER START
#### MSI BUILDING AND SIGNING
#

View File

@@ -1004,6 +1004,10 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuide.UnitTests/ShortcutGuide.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj" Id="e487304a-b1fb-4e6b-8e70-014051af5b99" />
</Folder>
<Folder Name="/modules/Workspaces/">
@@ -1028,8 +1032,6 @@
<Project Path="src/modules/Workspaces/WorkspacesModuleInterface/WorkspacesModuleInterface.vcxproj" Id="45285df2-9742-4eca-9ac9-58951fc26489" />
<Project Path="src/modules/Workspaces/WorkspacesSnapshotTool/WorkspacesSnapshotTool.vcxproj" Id="3d63307b-9d27-44fd-b033-b26f39245b85" />
<Project Path="src/modules/Workspaces/WorkspacesWindowArranger/WorkspacesWindowArranger.vcxproj" Id="37d07516-4185-43a4-924f-3c7a5d95ecf6" />
<Project Path="src/modules/Workspaces/WorkspacesSettingsService/WorkspacesSettingsService.vcxproj" Id="8b6a7c32-5c8d-4ad1-9f60-7e1b3d17a220" />
<Project Path="src/modules/Workspaces/WorkspacesSettingsClient/WorkspacesSettingsClient.vcxproj" Id="d24e2c12-9911-4e51-b102-39e7b62b22f1" />
</Folder>
<Folder Name="/modules/Workspaces/Tests/">
<Project Path="src/modules/Workspaces/WorkspacesEditorUITest/Workspaces.Editor.UITests.csproj">
@@ -1037,7 +1039,6 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/Workspaces/WorkspacesLib.UnitTests/WorkspacesLibUnitTests.vcxproj" Id="a85d4d9f-9a39-4b5d-8b5a-9f2d5c9a8b4c" />
<Project Path="src/modules/Workspaces/WorkspacesSettingsService/smoketest/WorkspacesSvcSmokeTest.vcxproj" Id="8b6a7c32-5c8d-4ad1-9f60-7e1b3d17a221" />
</Folder>
<Folder Name="/modules/Workspaces/WindowProperties/">
<File Path="src/modules/Workspaces/WindowProperties/WorkspacesWindowPropertyUtils.h" />

View File

@@ -1,93 +0,0 @@
# v6 prototype — one-click test setup
The CLI agent that wrote the prototype runs as a non-admin user, so it cannot
install the Windows service or apply the ACL on `%ProgramData%` itself.
Everything that needs admin has been bundled into one script.
## Step 1 — run the elevated setup (one time)
Open **PowerShell as Administrator** and run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File D:\PowerToys-Workspaces-EoP-v6\setup-ptworkspacessvc.ps1
```
It will:
1. Remove any prior `PTWorkspacesSvc` install (idempotent).
2. Register `PTWorkspacesSvc` against `NT SERVICE\PTWorkspacesSvc`
(virtual account, demand start, restart-on-failure).
3. Create `C:\ProgramData\Microsoft\PowerToys\Workspaces` with a PROTECTED DACL:
- `NT SERVICE\PTWorkspacesSvc` → FullControl
- `BUILTIN\Administrators` → FullControl
- `Authenticated Users` → ReadAndExecute
- inheritance from ProgramData stripped
4. Start the service and confirm it runs under the virtual account.
Log: `%TEMP%\ptworkspacessvc-setup.log`. Window stays open until you hit Enter.
## Step 2 — smoke test (as your normal user)
Open a **regular** PowerShell (not admin) and run:
```powershell
# 1) Build a fake "install folder" so the auth check accepts us.
$fake = "$env:TEMP\PTFakeInstall"
New-Item -ItemType Directory -Force $fake | Out-Null
Copy-Item D:\PowerToys-Workspaces-EoP-v6\x64\Debug\WorkspacesSvcSmokeTest\PowerToys.WorkspacesSvcSmokeTest.exe `
"$fake\PowerToys.WorkspacesEditor.exe" -Force
$env:PT_DEV_INSTALL_FOLDER = $fake # prototype-only override
# 2) Negative: the smoke test from its real location must be rejected.
& D:\PowerToys-Workspaces-EoP-v6\x64\Debug\WorkspacesSvcSmokeTest\PowerToys.WorkspacesSvcSmokeTest.exe ping
# Expected: AuthRejected
# 3) Positive: same exe, allowed name, allowed location.
& "$fake\PowerToys.WorkspacesEditor.exe" ping # expect Ok
& "$fake\PowerToys.WorkspacesEditor.exe" get # expect Ok (empty for new user)
# 4) Write a settings file through the service.
'{"workspaces":[]}' | Set-Content -Encoding UTF8 "$env:TEMP\sample.json"
& "$fake\PowerToys.WorkspacesEditor.exe" put "$env:TEMP\sample.json" # expect Ok
# 5) Verify the service actually wrote the file.
$me = (whoami /user /fo csv /nh).Split(',')[1].Trim('"')
Get-Content "C:\ProgramData\Microsoft\PowerToys\Workspaces\$me\workspaces.json"
# 6) CORE EoP TEST — try to write directly as the same user.
# Must be DENIED (this is the whole point of v6).
try {
Set-Content "C:\ProgramData\Microsoft\PowerToys\Workspaces\$me\workspaces.json" '{"evil":true}'
Write-Host "FAIL — direct write succeeded; DACL is not protecting the file" -ForegroundColor Red
} catch {
Write-Host "PASS — direct write rejected: $($_.Exception.Message)" -ForegroundColor Green
}
```
## Cleanup (when done testing)
Elevated PowerShell:
```powershell
sc.exe stop PTWorkspacesSvc
sc.exe delete PTWorkspacesSvc
Remove-Item -Recurse -Force C:\ProgramData\Microsoft\PowerToys\Workspaces
```
Normal PowerShell:
```powershell
Remove-Item Env:\PT_DEV_INSTALL_FOLDER
Remove-Item -Recurse -Force $env:TEMP\PTFakeInstall, $env:TEMP\sample.json
```
## Pass criteria
| Step | Expected |
|---|---|
| Setup script | "Setup complete" + service Running + owner = `NT SERVICE\PTWorkspacesSvc` |
| Smoke test step 2 | `AuthRejected` |
| Smoke test step 3 | `Ping=Ok`, `Get=Ok` (empty) |
| Smoke test step 4 | `Put=Ok` |
| Smoke test step 5 | JSON content prints |
| **Smoke test step 6** | **`PASS — direct write rejected: ...`** ← core EoP fix |

View File

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

View File

@@ -26,7 +26,6 @@
#include <processthreadsapi.h>
#include <UserEnv.h>
#include <winnt.h>
#include <shellapi.h>
using namespace std;
@@ -807,286 +806,10 @@ LExit:
return WcaFinalize(er);
}
// --- PTSettingsSvc MSIX (Design-v6-Final.md §12.4 unification) ---------------
// Per-MACHINE registration of the PowerToys Settings Service MSIX. Replaces the
// former MSI <ServiceInstall> of the loose exe: provisioning the MSIX for all
// users makes the MSIX windows.service extension the single owner of the
// machine-wide PTSettingsSvc, so per-machine and per-user no longer compete for
// the service name. Per-USER registration stays in the deferred managed
// ServiceProvisioner (a non-elevated per-user MSI cannot register a service).
namespace
{
const wchar_t* const kPTSettingsSvcFamilyName = L"Microsoft.PowerToys.SettingsService_8wekyb3d8bbwe";
const wchar_t* const kPTSettingsSvcPackageName = L"Microsoft.PowerToys.SettingsService";
const wchar_t* const kPTSettingsSvcMsixRelative = L"WorkspacesSettingsService\\PTSettingsSvc.msix";
// Best-effort STOP (not delete) of a service so its (packaged) exe is not
// held open while a new MSIX version is staged/registered — a running
// packaged windows.service otherwise blocks an in-place update with
// 0x80073D02 ("resources ... currently in use"). No-op on a fresh install.
void StopServiceIfRunning(const wchar_t* serviceName)
{
SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
if (!scm)
{
return;
}
SC_HANDLE svc = OpenServiceW(scm, serviceName, SERVICE_STOP | SERVICE_QUERY_STATUS);
if (svc)
{
SERVICE_STATUS ss{};
if (ControlService(svc, SERVICE_CONTROL_STOP, &ss))
{
for (int i = 0; i < 10; ++i)
{
if (!QueryServiceStatus(svc, &ss) || ss.dwCurrentState == SERVICE_STOPPED)
{
break;
}
Sleep(500);
}
}
CloseServiceHandle(svc);
}
CloseServiceHandle(scm);
}
// Prompts UAC once to remove the PTSettingsSvc package elevated (deleting a
// service-bearing package needs admin, which a per-user uninstall lacks).
// Mirrors the shared run_elevated() helper (common/utils/elevation.h) — the
// same UAC-via-ShellExecute("runas") mechanism used elsewhere in the product
// — kept self-contained here so the installer CA project need not take a new
// WIL/header dependency (elevation.h transitively pulls in wil/resource.h).
// Returns true only if the elevated removal completed (exit 0). False if the
// user declines UAC, there is no interactive session (silent uninstall), or
// removal fails — the caller then leaves the signed/immutable orphan WITHOUT
// blocking the uninstall (Design §12.5).
bool TryRemovePackageElevated(const wchar_t* packageName)
{
std::wstring params =
L"-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "
L"\"Get-AppxPackage -Name '";
params += packageName;
params += L"' | Remove-AppxPackage\"";
SHELLEXECUTEINFOW sei{};
sei.cbSize = sizeof(sei);
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC;
sei.lpVerb = L"runas"; // triggers the UAC consent prompt
sei.lpFile = L"powershell.exe";
sei.lpParameters = params.c_str();
sei.nShow = SW_HIDE;
if (!ShellExecuteExW(&sei) || !sei.hProcess)
{
// ERROR_CANCELLED (1223) == user declined UAC; or no shell/session.
return false;
}
WaitForSingleObject(sei.hProcess, 120000);
DWORD exitCode = 1;
GetExitCodeProcess(sei.hProcess, &exitCode);
CloseHandle(sei.hProcess);
return exitCode == 0;
}
}
UINT __stdcall InstallPTSettingsSvcCA(MSIHANDLE hInstall)
{
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Management::Deployment;
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder;
hr = WcaInitialize(hInstall, "InstallPTSettingsSvcCA");
ExitOnFailure(hr, "Failed to initialize");
hr = getInstallFolder(hInstall, installationFolder);
ExitOnFailure(hr, "Failed to get install folder");
try
{
std::filesystem::path msixPath = std::filesystem::path(installationFolder) / kPTSettingsSvcMsixRelative;
if (!std::filesystem::exists(msixPath))
{
Logger::error(L"PTSettingsSvc MSIX not found: " + msixPath.wstring());
er = ERROR_INSTALL_FAILURE;
ExitFunction();
}
Uri packageUri{ msixPath.wstring() };
PackageManager pm;
// Upgrade case: if a previous version's service is still running, stop it
// first so its packaged exe isn't held open (else the update fails with
// 0x80073D02). No-op on a fresh install. The service auto-restarts
// (AUTO_START) once the new version is registered (Design §12.6).
StopServiceIfRunning(L"PTSettingsSvc");
// Per-machine: stage once, then provision for all users. The MSIX
// windows.service extension registers the machine-wide PTSettingsSvc.
StagePackageOptions stageOptions;
auto stageResult = pm.StagePackageByUriAsync(packageUri, stageOptions).get();
uint32_t stageErrorCode = static_cast<uint32_t>(stageResult.ExtendedErrorCode());
if (stageErrorCode != 0)
{
Logger::error(L"PTSettingsSvc staging failed: 0x{:08X} - {}", stageErrorCode, stageResult.ErrorText());
er = ERROR_INSTALL_FAILURE;
ExitFunction();
}
auto provisionResult = pm.ProvisionPackageForAllUsersAsync(kPTSettingsSvcFamilyName).get();
uint32_t provisionErrorCode = static_cast<uint32_t>(provisionResult.ExtendedErrorCode());
if (provisionErrorCode != 0)
{
Logger::error(L"PTSettingsSvc provisioning failed: 0x{:08X}", provisionErrorCode);
er = ERROR_INSTALL_FAILURE;
ExitFunction();
}
Logger::info(L"PTSettingsSvc MSIX staged + provisioned for all users.");
}
catch (const winrt::hresult_error& ex)
{
Logger::error(L"PTSettingsSvc MSIX install exception: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
er = ERROR_INSTALL_FAILURE;
}
catch (const std::exception& ex)
{
std::string errorMessage{ "Exception while installing PTSettingsSvc MSIX: " };
errorMessage += ex.what();
Logger::error(errorMessage);
er = ERROR_INSTALL_FAILURE;
}
LExit:
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
return WcaFinalize(er);
}
UINT __stdcall UnRegisterPTSettingsSvcCA(MSIHANDLE hInstall)
{
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Management::Deployment;
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
LPWSTR installScope = nullptr;
bool isMachineLevel = false;
hr = WcaInitialize(hInstall, "UnRegisterPTSettingsSvcCA");
ExitOnFailure(hr, "Failed to initialize");
// Removing a windows.service-bearing package deletes the SCM service, which
// requires admin. Per-machine uninstall runs elevated → full cleanup.
// Per-user uninstall is non-elevated → this is best-effort (Return="ignore",
// Impersonate="yes", mirroring UninstallServicesTask); when it cannot
// elevate, the signed+immutable WindowsApps package is left as a harmless
// orphan, removed later by a per-machine install or a manual elevated
// Remove-AppxPackage (Design §12.5).
hr = WcaGetProperty(L"InstallScope", &installScope);
if (SUCCEEDED(hr) && installScope && wcscmp(installScope, L"perMachine") == 0)
{
isMachineLevel = true;
}
Logger::info(L"Unregistering PTSettingsSvc MSIX - perUser: {}", !isMachineLevel);
try
{
PackageManager pm;
if (isMachineLevel)
{
// Per-machine: deprovision, then remove for all users.
try
{
pm.DeprovisionPackageForAllUsersAsync(kPTSettingsSvcFamilyName).get();
}
catch (const winrt::hresult_error& ex)
{
Logger::warn(L"PTSettingsSvc deprovision failed: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
}
auto packages = pm.FindPackagesForUserWithPackageTypes({}, kPTSettingsSvcFamilyName, PackageTypes::Main);
for (const auto& package : packages)
{
try
{
auto removeResult = pm.RemovePackageAsync(package.Id().FullName(), RemovalOptions::RemoveForAllUsers).get();
uint32_t errorCode = static_cast<uint32_t>(removeResult.ExtendedErrorCode());
if (errorCode != 0)
{
Logger::error(L"PTSettingsSvc removal failed: 0x{:08X} - {}", errorCode, removeResult.ErrorText());
}
}
catch (const winrt::hresult_error& ex)
{
Logger::error(L"PTSettingsSvc removal exception: HRESULT 0x{:08X}", static_cast<uint32_t>(ex.code()));
}
}
}
else
{
// Per-user uninstall is non-elevated, but deleting a service-bearing
// package needs admin. 1) Try in-proc removal (succeeds only if this
// uninstall already happens to be elevated). 2) If anything remains,
// prompt UAC ONCE to remove it elevated. 3) If the user declines or
// there's no interactive session (silent uninstall), leave the
// signed/immutable orphan WITHOUT blocking the uninstall (Design §12.5).
bool foundAny = false;
bool removed = false;
auto packages = pm.FindPackagesForUserWithPackageTypes({}, kPTSettingsSvcFamilyName, PackageTypes::Main);
for (const auto& package : packages)
{
foundAny = true;
try
{
auto removeResult = pm.RemovePackageAsync(package.Id().FullName()).get();
if (static_cast<uint32_t>(removeResult.ExtendedErrorCode()) == 0)
{
removed = true;
}
}
catch (const winrt::hresult_error&)
{
// Expected when non-elevated; fall through to the UAC prompt.
}
}
if (foundAny && !removed)
{
if (TryRemovePackageElevated(kPTSettingsSvcPackageName))
{
Logger::info(L"PTSettingsSvc removed via one-time elevation at uninstall.");
}
else
{
Logger::warn(L"PTSettingsSvc left registered (UAC declined or no interactive session); "
L"removable later by an elevated Remove-AppxPackage or a per-machine install.");
}
}
}
}
catch (const std::exception& ex)
{
std::string errorMessage{ "Exception while unregistering PTSettingsSvc MSIX: " };
errorMessage += ex.what();
Logger::error(errorMessage);
// Don't fail the whole uninstall over service-package cleanup.
Logger::warn(L"Continuing uninstall despite PTSettingsSvc MSIX error");
}
LExit:
ReleaseStr(installScope);
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
UINT __stdcall RemoveWindowsServiceByName(std::wstring serviceName)
{
SC_HANDLE hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_CONNECT);
if (!hSCManager)
{
return ERROR_INSTALL_FAILURE;

View File

@@ -36,7 +36,5 @@ EXPORTS
SetBundleInstallLocationCA
InstallPackageIdentityMSIXCA
UninstallPackageIdentityMSIXCA
InstallPTSettingsSvcCA
UnRegisterPTSettingsSvcCA
CreateWinAppSDKHardlinksCA
DeleteWinAppSDKHardlinksCA

View File

@@ -1,52 +0,0 @@
<#
.SYNOPSIS
PTSettingsSvc - uninstall cleanup (Design-v6-Final.md section 11 uninstall/cleanup).
Runs as SYSTEM from the per-machine MSI (deferred CustomAction, on uninstall).
Removes the service and recursively deletes the protected data tree.
This recursive delete is REQUIRED: the per-user <SID>\blob.bin nodes are created
by the service at runtime and are NOT in the MSI component table, so the MSI's
default RemoveFolder won't touch them. A non-elevated per-user uninstall cannot
do this (the tree is SYSTEM-owned, user has only RX) - only the elevated/SYSTEM
per-machine uninstall can.
.PARAMETER RemoveService Stop + delete the PTSettingsSvc service (default: on).
.PARAMETER RemoveData Recursively delete the SettingsSvc data tree (default: on).
#>
[CmdletBinding()]
param(
[string]$ServiceName = 'PTSettingsSvc',
[switch]$RemoveService = $true,
[switch]$RemoveData = $true
)
$ErrorActionPreference = 'Continue'
if ($RemoveService)
{
$svc = Get-Service $ServiceName -ErrorAction SilentlyContinue
if ($svc)
{
if ($svc.Status -ne 'Stopped') { sc.exe stop $ServiceName | Out-Null; Start-Sleep -Milliseconds 800 }
sc.exe delete $ServiceName | Out-Null
Write-Output "service '$ServiceName' removed."
}
else { Write-Output "service '$ServiceName' not present." }
}
if ($RemoveData)
{
$root = Join-Path ([Environment]::GetFolderPath('CommonApplicationData')) 'Microsoft\PowerToys\Settings'
if (Test-Path $root)
{
# Recursive delete works because this runs as SYSTEM/admin (the tree is
# SYSTEM-owned with the user only RX; a non-elevated user could not).
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path $root) { Write-Output "WARNING: '$root' not fully removed." }
else { Write-Output "data tree '$root' removed." }
}
else { Write-Output "data tree not present." }
}
exit 0

View File

@@ -1,114 +0,0 @@
<#
.SYNOPSIS
PTSettingsSvc - install-time per-machine seeding (Design-v6-Final.md section 11 MIGRATION).
Runs as SYSTEM from the per-machine MSI (deferred CustomAction). Seeds every
existing user's protected blob from their legacy %LocalAppData% Workspaces file:
%LocalAppData%\Microsoft\PowerToys\Workspaces\workspaces.json (user U)
-> %ProgramData%\Microsoft\PowerToys\SettingsSvc\<ns>\<SID(U)>\blob.bin
Direct SYSTEM file write - no service round-trip, no migration opcode. The blob
is created with owner=SYSTEM and a PROTECTED DACL (svc:F, admin:F, system:F,
<user>:RX) so the user can read but never tamper. Idempotent (skips a SID that
already has a blob).
.NOTES
Standalone (no modules); safe to invoke via `powershell -ExecutionPolicy Bypass -File`.
#>
[CmdletBinding()]
param(
[string]$NamespaceId = 'Workspaces',
[string]$FileName = 'workspaces.json',
[string]$LegacyRelative = 'AppData\Local\Microsoft\PowerToys\Workspaces\workspaces.json',
[string]$ServiceAccount = 'NT SERVICE\PTSettingsSvc'
)
$ErrorActionPreference = 'Stop'
$programData = [Environment]::GetFolderPath('CommonApplicationData')
# SID-first layout: <storeRoot>\<sid>\<namespace>\<file>
$storeRoot = Join-Path $programData 'Microsoft\PowerToys\Settings'
# Store root: SYSTEM/Admins/service Full, Authenticated Users RX (so each user
# can traverse to their own <sid> node), owner SYSTEM, PROTECTED.
function New-RootDir([string]$path)
{
if (-not (Test-Path $path)) { New-Item -ItemType Directory -Force $path | Out-Null }
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false)
$acl.SetOwner([System.Security.Principal.SecurityIdentifier]'S-1-5-18') # SYSTEM
$inherit = 'ContainerInherit,ObjectInherit'
foreach ($p in @('NT AUTHORITY\SYSTEM','BUILTIN\Administrators',$ServiceAccount))
{
$acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($p,'FullControl',$inherit,'None','Allow')))
}
$acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule(
'NT AUTHORITY\Authenticated Users','ReadAndExecute',$inherit,'None','Allow')))
Set-Acl -Path $path -AclObject $acl
}
function New-ProtectedDir([string]$path, [string]$userSid)
{
if (-not (Test-Path $path)) { New-Item -ItemType Directory -Force $path | Out-Null }
# Build a PROTECTED DACL: SYSTEM:F, Administrators:F, service:F, <user>:RX.
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false) # protected, drop inheritance
$acl.SetOwner([System.Security.Principal.SecurityIdentifier]'S-1-5-18') # SYSTEM
$inherit = 'ContainerInherit,ObjectInherit'
$rules = @(
(New-Object Security.AccessControl.FileSystemAccessRule('NT AUTHORITY\SYSTEM','FullControl',$inherit,'None','Allow')),
(New-Object Security.AccessControl.FileSystemAccessRule('BUILTIN\Administrators','FullControl',$inherit,'None','Allow')),
(New-Object Security.AccessControl.FileSystemAccessRule($ServiceAccount,'FullControl',$inherit,'None','Allow')),
(New-Object Security.AccessControl.FileSystemAccessRule(
(New-Object Security.Principal.SecurityIdentifier($userSid)),'ReadAndExecute,Synchronize',$inherit,'None','Allow'))
)
foreach ($r in $rules) { $acl.AddAccessRule($r) }
Set-Acl -Path $path -AclObject $acl
}
# Enumerate real user profiles from ProfileList (SID -> profile path).
$profileListKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
$seeded = 0; $skipped = 0
# Ensure the store root exists with the traversable root DACL (once).
New-RootDir -path $storeRoot
Get-ChildItem $profileListKey -ErrorAction SilentlyContinue | ForEach-Object {
$sid = $_.PSChildName
# Only real interactive users: local (S-1-5-21-*) or AAD/MSA (S-1-12-1-*).
if ($sid -notmatch '^(S-1-5-21-|S-1-12-1-)') { return }
$profilePath = (Get-ItemProperty $_.PSPath -Name ProfileImagePath -ErrorAction SilentlyContinue).ProfileImagePath
if ([string]::IsNullOrEmpty($profilePath)) { return }
$legacy = Join-Path $profilePath $LegacyRelative
if (-not (Test-Path $legacy)) { return }
$userRoot = Join-Path $storeRoot $sid # per-user node (protected, inherits down)
$nsFolder = Join-Path $userRoot $NamespaceId
$file = Join-Path $nsFolder $FileName
if (Test-Path $file) { $skipped++; return } # idempotent
try
{
# Protect the <sid> node once; the namespace folder + file inherit it.
New-ProtectedDir -path $userRoot -userSid $sid
if (-not (Test-Path $nsFolder)) { New-Item -ItemType Directory -Force $nsFolder | Out-Null }
[System.IO.File]::WriteAllBytes($file, [System.IO.File]::ReadAllBytes($legacy))
$bytes = ([System.IO.FileInfo]::new($file)).Length
Write-Output "seeded: $sid ($bytes bytes)"
$seeded++
}
catch
{
Write-Output "FAILED for $sid : $($_.Exception.Message)"
}
}
Write-Output "PTSettingsSvc seeding done: $seeded seeded, $skipped already present."
exit 0

View File

@@ -138,7 +138,6 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="Resources.wxs" />
<Compile Include="WinAppSDK.wxs" />
<Compile Include="Workspaces.wxs" />
<Compile Include="WorkspacesSettingsService.wxs" />
</ItemGroup>
<ItemGroup>
<Folder Include="CustomDialogs" />

View File

@@ -69,21 +69,6 @@
<ComponentGroupRef Id="ToolComponentGroup" />
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
<ComponentGroupRef Id="WorkspacesComponentGroup" />
<!--
PowerToys Settings Service (PTSettingsSvc).
Per-machine: the MSI is elevated, so it registers the service and lays
down the protected store eagerly (PTSettingsServiceComponentGroup).
Per-user: the MSI is non-elevated and cannot register a service, so it
only stages the service payload (exe + hardening script); the service is
registered + the store hardened lazily via a one-time elevation the first
time protection is needed (Design §11 / §15 #5 d), driven by
SettingsBootstrapper / ServiceProvisioner in the managed code.
-->
<?if $(var.PerUser) != "true" ?>
<ComponentGroupRef Id="PTSettingsServiceComponentGroup" />
<?else?>
<ComponentGroupRef Id="PTSettingsServicePayloadComponentGroup" />
<?endif?>
<ComponentGroupRef Id="CmdPalComponentGroup" />
</Feature>
@@ -132,11 +117,6 @@
<Custom Action="SetApplyModulesRegistryChangeSetsParam" Before="ApplyModulesRegistryChangeSets" />
<Custom Action="SetInstallPackageIdentityMSIXParam" Before="InstallPackageIdentityMSIX" />
<?if $(var.PerUser) != "true" ?>
<!-- Per-machine: provision the PTSettingsSvc MSIX for all users (Design §12.4). -->
<Custom Action="SetInstallPTSettingsSvcParam" Before="InstallPTSettingsSvc" />
<?endif?>
<?if $(var.PerUser) = "true" ?>
<Custom Action="SetInstallDSCModuleParam" Before="InstallDSCModule" />
<?endif?>
@@ -149,9 +129,6 @@
<Custom Action="CreateWinAppSDKHardlinks" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED OR REINSTALL" />
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed AND WINDOWSBUILDNUMBER &gt;= 22000" />
<?if $(var.PerUser) != "true" ?>
<Custom Action="InstallPTSettingsSvc" After="InstallFiles" Condition="NOT Installed" />
<?endif?>
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
<!-- TODO: Use to activate embedded MSIX -->
@@ -175,12 +152,6 @@
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
<!-- PTSettingsSvc MSIX teardown (Design §12.5). Per-machine runs elevated
and removes the service package for all users; per-user is best-effort
(non-elevated uninstall cannot delete the service, mirroring
UninstallServicesTask) — the signed/immutable orphan is cleaned by a
later per-machine install or a manual elevated Remove-AppxPackage. -->
<Custom Action="UnRegisterPTSettingsSvc" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<!-- TODO: Use to activate embedded MSIX -->
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">
@@ -247,15 +218,6 @@
<CustomAction Id="UninstallPackageIdentityMSIX" Return="ignore" Impersonate="yes" DllEntry="UninstallPackageIdentityMSIXCA" BinaryRef="PTCustomActions" />
<!-- PTSettingsSvc MSIX provisioning (per-machine, Design §12.4). Mirrors the
PackageIdentity CAs: deferred+impersonated install provisions for all
users; immediate uninstall deprovisions + removes for all users. -->
<CustomAction Id="SetInstallPTSettingsSvcParam" Property="InstallPTSettingsSvc" Value="[INSTALLFOLDER]" />
<CustomAction Id="InstallPTSettingsSvc" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallPTSettingsSvcCA" BinaryRef="PTCustomActions" />
<CustomAction Id="UnRegisterPTSettingsSvc" Return="ignore" Impersonate="yes" DllEntry="UnRegisterPTSettingsSvcCA" BinaryRef="PTCustomActions" />
<CustomAction Id="InstallDSCModule" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="InstallDSCModuleCA" BinaryRef="PTCustomActions" />
<CustomAction Id="UninstallDSCModule" Return="ignore" Impersonate="yes" DllEntry="UninstallDSCModuleCA" BinaryRef="PTCustomActions" />

View File

@@ -1,243 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PowerToys Settings Service (PTSettingsSvc) installer fragment.
Implements Design-v6-Final.md §9 (storage DACL) and §12.1/§12.4 (MSIX service
distribution + scope unification).
The service binary is distributed as a signed MSIX (windows.service extension,
LocalSystem). Both scopes use the SAME MSIX, so there is a single machine-wide
PTSettingsSvc and a single staged package per version (no MSI <ServiceInstall>).
Per-machine: this fragment stages PTSettingsSvc.msix and the per-machine MSI
(elevated) provisions it for all users via the InstallPTSettingsSvc custom
action (Product.wxs) — the MSIX windows.service extension registers the
service.
Per-user: the per-user payload fragment (below) stages the same MSIX; the
managed ServiceProvisioner deploys it under one UAC on first editor open (a
non-elevated per-user MSI cannot register a service at install time).
Responsibilities:
1. Stage PTSettingsSvc.msix under <InstallFolder>\WorkspacesSettingsService\
2. Provision it for all users (per-machine CA) / deferred deploy (per-user)
3. Create %ProgramData%\Microsoft\PowerToys\Settings with a root DACL
(Administrators:FullControl, SYSTEM:FullControl, Authenticated Users:RX).
Per-user (<sid>) and per-namespace subfolders are created and tightened
lazily by the LocalSystem service on first write (SID-first layout).
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define PTSettingsSvcFilesPath=$(var.BinDir)\WorkspacesSettingsService\?>
<Fragment>
<!-- Service binary directory -->
<DirectoryRef Id="INSTALLFOLDER">
<Directory Id="WorkspacesSettingsServiceFolder" Name="WorkspacesSettingsService" />
</DirectoryRef>
<!-- Service MSIX payload (Design §12.4 unification). Per-machine ships the
signed PTSettingsSvc.msix and provisions it for all users via the
InstallPTSettingsSvc custom action (Product.wxs). The MSIX
windows.service extension owns the single machine-wide PTSettingsSvc,
so there is no MSI <ServiceInstall> competing with the per-user MSIX. -->
<DirectoryRef Id="WorkspacesSettingsServiceFolder"
FileSource="$(var.PTSettingsSvcFilesPath)">
<Component Id="RegisterPTSettingsService"
Guid="{F1F4C1B3-2E11-4F11-9E0E-2DBB4D9E0001}">
<File Id="PTSettingsSvcMsixPerMachine"
Name="PTSettingsSvc.msix"
KeyPath="yes" />
</Component>
<!-- Remove the per-install service folder on uninstall -->
<Component Id="RemovePTSettingsServiceFolder"
Guid="{F1F4C1B3-2E11-4F11-9E0E-2DBB4D9E0002}"
Directory="INSTALLFOLDER">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string"
Name="RemovePTSettingsServiceFolder"
Value=""
KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveFolderPTSettingsServiceFolder"
Directory="WorkspacesSettingsServiceFolder"
On="uninstall" />
</Component>
</DirectoryRef>
<!--
%ProgramData%\Microsoft\PowerToys\Settings — created at install with a
PROTECTED DACL (Design §9). util:PermissionEx replaces the inherited
DACL with the explicit ACEs below, so the default %ProgramData%
"Users can create" ACE does not carry through. Authenticated Users get
RX so each user can traverse to their own \<sid> node. Per-user (\<sid>)
and per-namespace (\<sid>\Workspaces) subfolders are created and tightened
by the service on first write (SID-first layout).
-->
<StandardDirectory Id="CommonAppDataFolder">
<Directory Id="PTSettingsDataMicrosoft" Name="Microsoft">
<Directory Id="PTSettingsDataPT" Name="PowerToys">
<Directory Id="PTSettingsDataRoot" Name="Settings">
<Component Id="CreatePTSettingsDataRoot"
Guid="{F1F4C1B3-2E11-4F11-9E0E-2DBB4D9E0003}">
<CreateFolder>
<util:PermissionEx User="Administrators" Domain="BUILTIN"
GenericAll="yes" />
<util:PermissionEx User="SYSTEM" Domain="NT AUTHORITY"
GenericAll="yes" />
<util:PermissionEx User="Authenticated Users" Domain="NT AUTHORITY"
GenericRead="yes" GenericExecute="yes" />
</CreateFolder>
<RegistryKey Root="HKLM"
Key="Software\Microsoft\PowerToys\SettingsSvc">
<RegistryValue Type="integer"
Name="DataRootCreated"
Value="1"
KeyPath="yes" />
</RegistryKey>
</Component>
</Directory>
</Directory>
</Directory>
</StandardDirectory>
<!--
INSTALLFOLDER hardening (Design §8/§11). Replaces the install folder's
DACL with the admin-only-writable set the runtime check expects. The
service runs as LocalSystem (MSIX), which is covered by the SYSTEM ACE
below, so it can read this DACL during caller authentication — no separate
virtual-account ACE is needed.
NOTE: util:PermissionEx replaces the whole DACL, so the full intended
ACL is specified here. This touches the shared INSTALLFOLDER and MUST
be validated end-to-end (installer validation is deferred).
-->
<DirectoryRef Id="INSTALLFOLDER">
<Component Id="HardenInstallFolderDacl"
Guid="{F1F4C1B3-2E11-4F11-9E0E-2DBB4D9E0004}">
<CreateFolder>
<util:PermissionEx User="SYSTEM" Domain="NT AUTHORITY"
GenericAll="yes" />
<util:PermissionEx User="Administrators" Domain="BUILTIN"
GenericAll="yes" />
<util:PermissionEx User="TrustedInstaller" Domain="NT SERVICE"
GenericAll="yes" />
<util:PermissionEx User="Users" Domain="BUILTIN"
GenericRead="yes" GenericExecute="yes" />
</CreateFolder>
<RegistryKey Root="HKLM"
Key="Software\Microsoft\PowerToys\SettingsSvc">
<RegistryValue Type="integer"
Name="InstallFolderHardened"
Value="1"
KeyPath="yes" />
</RegistryKey>
</Component>
</DirectoryRef>
<!--
Migration / cleanup CustomActions (Design-v6-Final.md §11). The seeding
and cleanup logic lives in PowerShell scripts (installed next to the
service binary); these CAs invoke them as SYSTEM. Installer validation
is deferred — authored, not yet MSI-validated.
* Seed at install: enumerate user profiles → create each user's protected
blob from their legacy %LocalAppData% file (direct SYSTEM write).
* Cleanup at uninstall: recursively delete the SettingsSvc data tree
(the runtime-created <SID>\blob.bin nodes aren't MSI-tracked, so the
default RemoveFolder can't remove them; the service itself is removed
by ServiceControl above).
-->
<DirectoryRef Id="WorkspacesSettingsServiceFolder"
FileSource="$(var.ProjectDir)\CustomActions">
<Component Id="PTSettingsCustomActionScripts"
Guid="{F1F4C1B3-2E11-4F11-9E0E-2DBB4D9E0005}">
<File Id="PtSeedScript" Name="Seed-PtSettingsStore.ps1" KeyPath="yes" />
<File Id="PtCleanupScript" Name="Remove-PtSettingsStore.ps1" />
</Component>
</DirectoryRef>
<!-- Run the seeding script as SYSTEM after files are laid down (fresh install only). -->
<CustomAction Id="PtSeedStore"
Directory="WorkspacesSettingsServiceFolder"
ExeCommand="powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -File &quot;[#PtSeedScript]&quot;"
Execute="deferred" Impersonate="no" Return="ignore" />
<!-- Recursively remove the protected data tree as SYSTEM on uninstall. -->
<CustomAction Id="PtCleanupStore"
Directory="WorkspacesSettingsServiceFolder"
ExeCommand="powershell.exe -NonInteractive -NoProfile -ExecutionPolicy Bypass -File &quot;[#PtCleanupScript]&quot; -RemoveService:$false -RemoveData"
Execute="deferred" Impersonate="no" Return="ignore" />
<InstallExecuteSequence>
<!-- After the data root + files exist; only on a fresh install. -->
<Custom Action="PtSeedStore" After="InstallFiles" Condition="NOT Installed" />
<!-- While the script still exists; only on full uninstall. -->
<Custom Action="PtCleanupStore" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
</InstallExecuteSequence>
<ComponentGroup Id="PTSettingsServiceComponentGroup">
<ComponentRef Id="RegisterPTSettingsService" />
<ComponentRef Id="RemovePTSettingsServiceFolder" />
<ComponentRef Id="CreatePTSettingsDataRoot" />
<ComponentRef Id="HardenInstallFolderDacl" />
<ComponentRef Id="PTSettingsCustomActionScripts" />
</ComponentGroup>
</Fragment>
<!--
Per-user payload (Design §12.1). A per-user MSI is non-elevated and cannot
register a service, so it only STAGES the SIGNED service MSIX under the
install folder. The managed ServiceProvisioner deploys it via one elevated
Add-AppxPackage (the windows.service extension auto-registers PTSettingsSvc
as LocalSystem; the service hardens the store on first PutBlob). No
user-writable hardening script (the prior Harden ps1 was an EoP hole).
-->
<Fragment>
<DirectoryRef Id="INSTALLFOLDER">
<Directory Id="PTSettingsPayloadFolder" Name="WorkspacesSettingsService" />
</DirectoryRef>
<DirectoryRef Id="PTSettingsPayloadFolder"
FileSource="$(var.PTSettingsSvcFilesPath)">
<Component Id="PTSettingsServicePayloadMsix"
Guid="{F1F4C1B3-2E11-4F11-9E0E-2DBB4D9E0006}">
<File Id="PTSettingsSvcMsixPerUser"
Name="PTSettingsSvc.msix" />
<!-- Per-user (user-profile) component: KeyPath must be an HKCU
registry value, not a file (WiX ICE38). -->
<RegistryKey Root="HKCU" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string"
Name="PTSettingsServicePayloadMsix"
Value=""
KeyPath="yes" />
</RegistryKey>
</Component>
</DirectoryRef>
<!-- Remove the staged payload folder on uninstall. -->
<DirectoryRef Id="PTSettingsPayloadFolder">
<Component Id="RemovePTSettingsPayloadFolder"
Guid="{F1F4C1B3-2E11-4F11-9E0E-2DBB4D9E0008}">
<RegistryKey Root="HKCU" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string"
Name="RemovePTSettingsPayloadFolder"
Value=""
KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveFolderPTSettingsPayload"
Directory="PTSettingsPayloadFolder"
On="uninstall" />
</Component>
</DirectoryRef>
<ComponentGroup Id="PTSettingsServicePayloadComponentGroup">
<ComponentRef Id="PTSettingsServicePayloadMsix" />
<ComponentRef Id="RemovePTSettingsPayloadFolder" />
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -210,7 +210,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- SectionName: Formatting
Properties:
- Name: Bold

View File

@@ -1542,7 +1542,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 3
- "<3>"
- Name: Move earlier or later by number of frames specified for stroke Duration
Shortcut:
- Win: false

View File

@@ -642,7 +642,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 3
- "<3>"
- Name: Show document template
Shortcut:
- Win: false
@@ -810,7 +810,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 5
- "<5>"
- Name: Release guides
Shortcut:
- Win: false
@@ -818,7 +818,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 5
- "<5>"
- Name: Show/ hide smart guides
Shortcut:
- Win: false
@@ -925,7 +925,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 6
- "<6>"
- Name: Select the object above the current selection
Shortcut:
- Win: false
@@ -965,7 +965,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 2
- "<2>"
- Name: Unlock a selection
Shortcut:
- Win: false
@@ -973,7 +973,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Hide a selection
Shortcut:
- Win: false
@@ -981,7 +981,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 3
- "<3>"
- Name: Show all selections
Shortcut:
- Win: false
@@ -989,7 +989,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 3
- "<3>"
- Name: Move selection in user-defined increments
Shortcut:
- Win: false
@@ -1013,7 +1013,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 2
- "<2>"
- Name: Bring a selection forward
Shortcut:
- Win: false
@@ -1071,7 +1071,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 8
- "<8>"
- Name: Release a compound path
Shortcut:
- Win: false
@@ -1079,7 +1079,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 8
- "<8>"
- Name: Edit a pattern
Shortcut:
- Win: false
@@ -1261,7 +1261,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 4
- "<4>"
- Name: Move an object
Shortcut:
- Win: false
@@ -1285,7 +1285,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 7
- "<7>"
- Name: Release a clipping mask
Shortcut:
- Win: false
@@ -1293,7 +1293,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Toggle between fill and stroke
Shortcut:
- Win: false
@@ -1641,7 +1641,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 8
- "<8>"
- Name: Insert copyright symbol
Shortcut:
- Win: false
@@ -1665,7 +1665,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Insert section symbol
Shortcut:
- Win: false
@@ -1673,7 +1673,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 6
- "<6>"
- Name: Insert trademark symbol
Shortcut:
- Win: false
@@ -1681,7 +1681,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Insert registered trademark symbol
Shortcut:
- Win: false

View File

@@ -1036,7 +1036,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 5
- "<5>"
- Name: Redraw screen
Shortcut:
- Win: false
@@ -1060,7 +1060,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Switch to next/previous document window
Shortcut:
- Win: false
@@ -1155,7 +1155,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 6
- "<6>"
- Name: Toggle Character/Paragraph text attributes mode
Shortcut:
- Win: false
@@ -1163,7 +1163,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Display the pop-up menu that has focus
Shortcut:
- Win: false
@@ -1301,7 +1301,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 1
- "<1>"
- Name: Show Magenta plate
Shortcut:
- Win: false
@@ -1309,7 +1309,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 2
- "<2>"
- Name: Show Yellow plate
Shortcut:
- Win: false
@@ -1317,7 +1317,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 3
- "<3>"
- Name: Show Black plate
Shortcut:
- Win: false
@@ -1325,7 +1325,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 4
- "<4>"
- Name: Show 1st Spot plate
Shortcut:
- Win: false
@@ -1333,7 +1333,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 5
- "<5>"
- Name: Show 2nd Spot plate
Shortcut:
- Win: false
@@ -1341,7 +1341,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 6
- "<6>"
- Name: Show 3rd Spot plate
Shortcut:
- Win: false
@@ -1349,7 +1349,7 @@ Shortcuts:
Shift: true
Alt: true
Keys:
- 7
- "<7>"
- SectionName: Transform panel
Properties:
- Name: Apply value and copy object

View File

@@ -803,7 +803,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Switch to Hand tool (when not in text-edit mode)
Shortcut:
- Win: false
@@ -1309,7 +1309,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 1
- "<1>"
- Name: Tone Curve panel
Shortcut:
- Win: false
@@ -1317,7 +1317,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Detail panel
Shortcut:
- Win: false
@@ -1325,7 +1325,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 3
- "<3>"
- Name: HSL/Grayscale panel
Shortcut:
- Win: false
@@ -1333,7 +1333,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 4
- "<4>"
- Name: Split Toning panel
Shortcut:
- Win: false
@@ -1341,7 +1341,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 5
- "<5>"
- Name: Lens Corrections panel
Shortcut:
- Win: false
@@ -1349,7 +1349,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 6
- "<6>"
- Name: Camera Calibration panel
Shortcut:
- Win: false
@@ -1357,7 +1357,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Presets panel
Shortcut:
- Win: false
@@ -1365,7 +1365,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 9
- "<9>"
- Name: Open Snapshots panel
Shortcut:
- Win: false
@@ -1373,7 +1373,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 9
- "<9>"
- Name: Parametric Curve Targeted Adjustment tool
Shortcut:
- Win: false
@@ -1665,7 +1665,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 6
- "<6>"
- Name: (Filmstrip mode) Add yellow label
Shortcut:
- Win: false
@@ -1673,7 +1673,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 7
- "<7>"
- Name: (Filmstrip mode) Add green label
Shortcut:
- Win: false
@@ -1681,7 +1681,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 8
- "<8>"
- Name: (Filmstrip mode) Add blue label
Shortcut:
- Win: false
@@ -1689,7 +1689,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 9
- "<9>"
- Name: (Filmstrip mode) Add purple label
Shortcut:
- Win: false
@@ -1697,7 +1697,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 0
- "<0>"
- Name: Camera Raw preferences
Shortcut:
- Win: false
@@ -1936,7 +1936,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- Name: Cycle through blending modes
Shortcut:
- Win: false
@@ -2433,7 +2433,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Delete adjustment layer
Shortcut:
- Win: false

View File

@@ -407,7 +407,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Edge select mode
Shortcut:
- Win: false
@@ -415,7 +415,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 2
- "<2>"
- Name: Face select mode
Shortcut:
- Win: false
@@ -423,7 +423,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 3
- "<3>"
- Name: Extrude region
Shortcut:
- Win: false

View File

@@ -806,7 +806,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Set opacity to 50
Shortcut:
- Win: false
@@ -814,7 +814,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 5
- "<5>"
- Name: Set opacity to 100
Shortcut:
- Win: false
@@ -822,7 +822,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- SectionName: Arrange
Properties:
- Name: Bring forward

View File

@@ -489,7 +489,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- SectionName: Edit
Properties:
- Name: Undo

View File

@@ -63,7 +63,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Jump to rightmost tab
Shortcut:
- Win: false
@@ -71,7 +71,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 9
- "<9>"
- Name: Open home page in current tab
Shortcut:
- Win: false
@@ -424,7 +424,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- Name: Scroll down a screen
Shortcut:
- Win: false

View File

@@ -21,7 +21,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 1
- "<1>"
- Name: Show Intention Actions
Recommended: true
Shortcut:
@@ -778,7 +778,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 1
- "<1>"
- Name: Show Bookmarks window
Shortcut:
- Win: false
@@ -786,7 +786,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Show Find window
Shortcut:
- Win: false
@@ -794,7 +794,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 3
- "<3>"
- Name: Show Run window
Shortcut:
- Win: false
@@ -802,7 +802,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 4
- "<4>"
- Name: Show Debug window
Shortcut:
- Win: false
@@ -810,7 +810,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 5
- "<5>"
- Name: Show Problems window
Shortcut:
- Win: false
@@ -818,7 +818,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 6
- "<6>"
- Name: Show Structure window
Shortcut:
- Win: false
@@ -826,7 +826,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 7
- "<7>"
- Name: Show Services window
Shortcut:
- Win: false
@@ -834,7 +834,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 8
- "<8>"
- Name: Show Version Control window
Shortcut:
- Win: false
@@ -842,7 +842,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 9
- "<9>"
- Name: Show Commit window
Shortcut:
- Win: false
@@ -850,7 +850,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 0
- "<0>"
- Name: Show Terminal window
Shortcut:
- Win: false

View File

@@ -45,7 +45,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Switch to the last tab
Shortcut:
- Win: false
@@ -53,7 +53,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 9
- "<9>"
- Name: Close the current tab
Shortcut:
- Win: false
@@ -479,7 +479,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- Name: Stop loading page; close dialog or pop-up
Shortcut:
- Win: false

View File

@@ -492,7 +492,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Focus into Second Editor Group
Shortcut:
- Win: false
@@ -500,7 +500,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 2
- "<2>"
- Name: Focus into Third Editor Group
Shortcut:
- Win: false
@@ -508,7 +508,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 3
- "<3>"
- Name: Move Editor Left
Shortcut:
- Win: false

View File

@@ -202,7 +202,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 0
- "<0>"
- SectionName: Editing
Properties:
- Name: Copy
@@ -485,7 +485,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 1
- "<1>"
- Name: Go to last tab
Shortcut:
- Win: false
@@ -493,7 +493,7 @@ Shortcuts:
Shift: false
Alt: false
Keys:
- 9
- "<9>"
- Name: Move tab left
Shortcut:
- Win: false

View File

@@ -0,0 +1,463 @@
PackageName: Postman.Postman
Name: Postman
WindowFilter: "Postman.exe"
BackgroundProcess: false
Shortcuts:
- SectionName: Tabs
Properties:
- Name: Close tab
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- W
- Name: Force close tab
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- W
- Name: Switch to next tab
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- Tab
- Name: Switch to previous tab
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- Tab
- Name: Switch to tab at position (18)
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- '1 - 8'
- Name: Switch to last tab
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "<9>"
- Name: Reopen last closed tab
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- T
- Name: New runner tab
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- R
- Name: Search tabs
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- A
- SectionName: Sidebar
Properties:
- Name: Search sidebar
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- F
- Name: Next item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Down>"
- Name: Previous item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Up>"
- Name: Expand item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Right>"
- Name: Expand all
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: true
Keys:
- "<Right>"
- Name: Collapse item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Left>"
- Name: Collapse all
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: true
Keys:
- "<Left>"
- Name: Select item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Enter>"
- Name: Rename item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- E
- Name: Cut item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- X
- Name: Copy item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- C
- Name: Paste item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- V
- Name: Duplicate item
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- D
- Name: Delete item
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: false
Keys:
- "<Delete>"
- SectionName: Request
Properties:
- Name: Request URL
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- L
- Name: Save request
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- S
- Name: Save request as
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- S
- Name: Send request
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "<Enter>"
- Name: Send and download request
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- "<Enter>"
- SectionName: Interface
Properties:
- Name: Zoom in
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- Plus
- Name: Zoom out
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- Minus
- Name: Reset zoom
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "<0>"
- Name: Toggle two-pane view
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- V
- Name: Toggle left sidebar
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "\\"
- Name: Toggle right sidebar
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- "\\"
- Name: Toggle workbench
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- M
- Name: Swap sidebars
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- S
- Name: Reset layout
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- R
- Name: Environment selector
Shortcut:
- Win: false
Ctrl: false
Shift: false
Alt: true
Keys:
- E
- SectionName: Window and modals
Properties:
- Name: New…
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- N
- Name: New Postman window
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- N
- Name: New console window
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- C
- Name: Find
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- F
- Name: Import
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- O
- Name: Settings
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- ","
- Name: Open shortcut help
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "/"
- Name: Search
Recommended: true
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- K
- Name: Search in current workspace
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- K
- Name: Open Postbot
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: true
Keys:
- P
- Name: Open Vault
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- V
- Name: Open browser tab
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- B
- Name: Cancel conversation
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- C
- Name: Accept all
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- Y
- Name: Reject all
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "<Escape>"
- SectionName: Console
Properties:
- Name: Clear console
Shortcut:
- Win: false
Ctrl: true
Shift: true
Alt: false
Keys:
- K
- Name: Show/hide console
Shortcut:
- Win: false
Ctrl: true
Shift: false
Alt: false
Keys:
- "`"

View File

@@ -241,7 +241,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 1
- "<1>"
- Name: Browse DMs
Shortcut:
- Win: false
@@ -249,7 +249,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 2
- "<2>"
- Name: Open the Activity view
Shortcut:
- Win: false
@@ -265,7 +265,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 0
- "<0>"
- Name: Open the Threads view
Shortcut:
- Win: false
@@ -525,7 +525,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 9
- "<9>"
- Name: Inline code selected text
Shortcut:
- Win: false
@@ -549,7 +549,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 8
- "<8>"
- Name: Numbered list
Shortcut:
- Win: false
@@ -557,7 +557,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 7
- "<7>"
- Name: Apply markdown formatting
Shortcut:
- Win: false
@@ -583,7 +583,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 0
- "<0>"
- Name: Big heading
Shortcut:
- Win: false
@@ -591,7 +591,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 1
- "<1>"
- Name: Medium heading
Shortcut:
- Win: false
@@ -599,7 +599,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 2
- "<2>"
- Name: Small heading
Shortcut:
- Win: false
@@ -607,7 +607,7 @@ Shortcuts:
Shift: false
Alt: true
Keys:
- 3
- "<3>"
- Name: Checklist
Shortcut:
- Win: false
@@ -615,7 +615,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 0
- "<0>"
- Name: Bulleted list
Shortcut:
- Win: false
@@ -623,7 +623,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 8
- "<8>"
- Name: Numbered list
Shortcut:
- Win: false
@@ -631,7 +631,7 @@ Shortcuts:
Shift: true
Alt: false
Keys:
- 7
- "<7>"
- Name: Toggle heading and list styles
Shortcut:
- Win: false

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using ShortcutGuide.Models;
@@ -32,16 +33,27 @@ namespace ShortcutGuide.Helpers
list.Add(shortcutEntry);
}
// Persist on a best-effort basis. The in-memory pinned list is the source of truth
// for the rest of the session; failing to write should not crash the overlay
// (Pin/Unpin runs from a synchronous UI handler).
Save();
PinnedShortcutsChanged?.Invoke(null, appName);
}
public static void Save()
{
string serialized = JsonSerializer.Serialize(App.PinnedShortcuts);
string pinnedPath = SettingsUtils.Default.GetSettingsFilePath(ShortcutGuideSettings.ModuleName, "Pinned.json");
File.WriteAllText(pinnedPath, serialized);
try
{
string serialized = JsonSerializer.Serialize(App.PinnedShortcuts);
string pinnedPath = SettingsUtils.Default.GetSettingsFilePath(ShortcutGuideSettings.ModuleName, "Pinned.json");
File.WriteAllText(pinnedPath, serialized);
}
catch (Exception ex) when (ex is IOException
or UnauthorizedAccessException
or JsonException)
{
Logger.LogError("Failed to persist Shortcut Guide pinned shortcuts; keeping in-memory state.", ex);
}
}
}
}

View File

@@ -2,9 +2,12 @@
// 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.Text.Json;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
@@ -31,21 +34,39 @@ namespace ShortcutGuide
public App()
{
this.InitializeComponent();
// Register process-wide exception handlers so a stray exception (e.g. an IO failure
// during a fire-and-forget UI handler, or a background Task fault) gets logged
// instead of taking the overlay down with an unhandled access violation in coreclr.
// Without these the runtime tears the process down before our local catches can run.
this.UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
this.LoadData();
MainWindow = new MainWindow();
TaskBarWindow = new TaskbarWindow();
MainWindow.Activate();
MainWindow.Closed += (_, _) =>
try
{
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
MainWindow.SessionDurationMs,
MainWindow.CloseType));
TaskBarWindow.Close();
};
this.LoadData();
MainWindow = new MainWindow();
TaskBarWindow = new TaskbarWindow();
MainWindow.Activate();
MainWindow.Closed += (_, _) =>
{
PowerToysTelemetry.Log.WriteEvent(new ShortcutGuideSessionEvent(
MainWindow.SessionDurationMs,
MainWindow.CloseType));
TaskBarWindow.Close();
};
}
catch (Exception ex)
{
// Any failure in launch is fatal for this short-lived overlay; log and exit
// cleanly rather than letting WinUI surface a generic crash dialog.
Logger.LogError("Failed to launch Shortcut Guide.", ex);
Environment.Exit(1);
}
}
private void LoadData()
@@ -63,18 +84,53 @@ namespace ShortcutGuide
PinnedShortcuts = loaded;
}
}
catch (JsonException)
catch (Exception ex) when (ex is JsonException
or IOException
or UnauthorizedAccessException)
{
// Fall back to the empty default if the file is corrupt.
// Fall back to the empty default if the file is corrupt or unreadable.
Logger.LogWarning($"Failed to load pinned shortcuts from '{pinnedPath}'. Falling back to empty list. Reason: {ex.Message}");
}
}
ShortcutGuideSettings = SettingsRepository<ShortcutGuideSettings>.GetInstance(settingsUtils).SettingsConfig;
ShortcutGuideProperties = ShortcutGuideSettings.Properties;
try
{
#pragma warning disable CA1869 // Cache and reuse 'JsonSerializerOptions' instances
settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide");
settingsUtils.SaveSettings(JsonSerializer.Serialize(App.ShortcutGuideSettings, new JsonSerializerOptions { WriteIndented = true }), "Shortcut Guide");
#pragma warning restore CA1869 // Cache and reuse 'JsonSerializerOptions' instances
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Persisting the round-tripped settings is best-effort; the in-memory copy is still valid.
Logger.LogWarning($"Failed to persist Shortcut Guide settings on launch. Reason: {ex.Message}");
}
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// Exceptions raised on the UI thread land here. Mark handled so the runtime
// does not terminate the process; the overlay can usually continue.
Logger.LogError("Unhandled UI exception in Shortcut Guide.", e.Exception);
e.Handled = true;
}
private static void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs e)
{
// Background-thread exceptions reach here as a last resort; we cannot prevent
// termination when IsTerminating is true, but at least we leave a log trail.
if (e.ExceptionObject is Exception ex)
{
Logger.LogError($"Unhandled background exception in Shortcut Guide (IsTerminating={e.IsTerminating}).", ex);
}
}
private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
Logger.LogError("Unobserved Task exception in Shortcut Guide.", e.Exception);
e.SetObserved();
}
}
}

View File

@@ -237,37 +237,54 @@ namespace ShortcutGuide
private void SetWindowPosition()
{
if (!this._hasMovedToRightMonitor)
try
{
NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint);
AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) });
this._hasMovedToRightMonitor = true;
if (!this._hasMovedToRightMonitor)
{
NativeMethods.GetCursorPos(out NativeMethods.POINT lpPoint);
AppWindow.Move(new NativeMethods.POINT { Y = lpPoint.Y - ((int)Height / 2), X = lpPoint.X - ((int)Width / 2) });
this._hasMovedToRightMonitor = true;
}
var hwnd = WindowNative.GetWindowHandle(this);
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
// App.TaskBarWindow / its AppWindow can briefly be null during the reentrant
// Hide → Activate → BringToFront chain triggered from SelectionChanged. When the
// taskbar window is not currently observable, skip the overlap adjustment instead
// of crashing the overlay (issue #48448).
var taskbarWindow = App.TaskBarWindow?.AppWindow;
bool taskbarOnLeft = false;
bool taskbarOnRight = false;
if (taskbarWindow is not null)
{
taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
}
double newHeight = monitorRect.Height / dpi;
if (taskbarWindow is not null && (taskbarOnLeft || taskbarOnRight))
{
newHeight -= taskbarWindow.Size.Height;
}
MaxHeight = newHeight;
MinHeight = newHeight;
Height = newHeight;
int xPosition = windowPosition == ShortcutGuideWindowPosition.Right
? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi)
: (int)monitorRect.X;
this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height);
}
var hwnd = WindowNative.GetWindowHandle(this);
float dpi = DpiHelper.GetDPIScaleForWindow(hwnd);
Rect monitorRect = DisplayHelper.GetWorkAreaForDisplayWithWindow(hwnd);
var windowPosition = (ShortcutGuideWindowPosition)App.ShortcutGuideProperties.WindowPosition.Value;
var taskbarWindow = App.TaskBarWindow.AppWindow;
bool taskbarOnLeft = taskbarWindow.IsVisible && taskbarWindow.Position.X < AppWindow.Position.X + Width && windowPosition == ShortcutGuideWindowPosition.Left;
bool taskbarOnRight = taskbarWindow.IsVisible && taskbarWindow.Position.X + taskbarWindow.Size.Width > AppWindow.Position.X && windowPosition == ShortcutGuideWindowPosition.Right;
double newHeight = monitorRect.Height / dpi;
if (taskbarOnLeft || taskbarOnRight)
catch (Exception ex)
{
newHeight -= taskbarWindow.Size.Height;
Logger.LogError("Failed to set Shortcut Guide window position; keeping previous layout.", ex);
}
MaxHeight = newHeight;
MinHeight = newHeight;
Height = newHeight;
int xPosition = windowPosition == ShortcutGuideWindowPosition.Right
? (int)(monitorRect.X + monitorRect.Width) - (int)(Width * dpi)
: (int)monitorRect.X;
this.MoveAndResize(xPosition, (int)monitorRect.Y, Width, Height);
}
/// <summary>
@@ -282,25 +299,35 @@ namespace ShortcutGuide
return;
}
this._selectedAppName = selectedItem.Name;
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
App.TaskBarWindow.Hide();
if (this._shortcutFile is ShortcutFile file)
try
{
// Show the taskbar button window only when the selected app exposes the <TASKBAR1-9> section.
if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true))
{
this._taskBarWindowActivated = true;
App.TaskBarWindow.Activate();
}
this._selectedAppName = selectedItem.Name;
App.CurrentAppName = this._selectedAppName;
this._shortcutFile = ManifestInterpreter.GetShortcutsOfApplication(this._selectedAppName);
// Reposition before navigating so the taskbar window does not clip into the main window.
this.SetWindowPosition();
this.ContentFrame.Navigate(
typeof(ShortcutsPage),
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
App.TaskBarWindow?.Hide();
if (this._shortcutFile is ShortcutFile file)
{
// Show the taskbar button window only when the selected app exposes the <TASKBAR1-9> section.
if (file.Shortcuts is not null && file.Shortcuts.Any(c => c.SectionName?.StartsWith("<TASKBAR1-9>", StringComparison.Ordinal) == true))
{
this._taskBarWindowActivated = true;
App.TaskBarWindow?.Activate();
}
// Reposition before navigating so the taskbar window does not clip into the main window.
this.SetWindowPosition();
this.ContentFrame.Navigate(
typeof(ShortcutsPage),
new ShortcutPageNavParam { ShortcutFile = file, AppName = this._selectedAppName });
}
}
catch (Exception ex)
{
// Guard against exceptions during section navigation so the overlay does not close on the user.
// InitializeNavItemsAsync's catch interprets any exception bubbling out of the initial
// SelectedItem assignment as a fatal init failure and closes the window (issue #48448).
Logger.LogError($"Failed to handle Shortcut Guide section selection '{selectedItem.Name}'.", ex);
}
}

View File

@@ -30,54 +30,73 @@ namespace ShortcutGuide.ShortcutGuideXAML
public void UpdateTasklistButtons()
{
// This move ensures the window spawns on the same monitor as the main window
AppWindow.MoveInZOrderAtBottom();
AppWindow.Move(App.MainWindow.AppWindow.Position);
TasklistButton[] buttons = [];
// Wrap the entire body: this method runs from the ctor and from `Activated`,
// both of which can fire while MainWindow is closing or AppWindow is in a
// transient null state. An exception here used to crash the overlay because
// there was no caller-side try/catch (issue #48441).
try
{
buttons = TasklistPositions.GetButtons();
// This move ensures the window spawns on the same monitor as the main window.
// App.MainWindow / its AppWindow can briefly be null during the reentrant
// Hide → Activate → BringToFront chain triggered from SelectionChanged.
var mainAppWindow = App.MainWindow?.AppWindow;
if (mainAppWindow is null)
{
return;
}
AppWindow.MoveInZOrderAtBottom();
AppWindow.Move(mainAppWindow.Position);
TasklistButton[] buttons = [];
try
{
buttons = TasklistPositions.GetButtons();
}
catch (Exception ex)
{
Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex);
}
if (buttons.Length == 0)
{
AppWindow.Hide();
return;
}
float dpi = this.DPI;
double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value;
double windowHeight = 58;
double windowMargin = 8 * dpi;
double windowWidth = windowsLogoColumnWidth;
double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi);
double yPosition = this.WorkArea.Bottom - (windowHeight * dpi);
this.KeyHolder.Children.Clear();
foreach (TasklistButton b in buttons)
{
TaskbarIndicator indicator = new()
{
Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture),
Height = b.Height / dpi,
Width = b.Width / dpi,
};
windowWidth += indicator.Width;
this.KeyHolder.Children.Add(indicator);
double indicatorPos = (b.X - xPosition) / dpi;
Canvas.SetLeft(indicator, indicatorPos - windowsLogoColumnWidth);
}
this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight);
AppWindow.MoveInZOrderAtTop();
}
catch (Exception ex)
{
Logger.LogError("Failed to enumerate taskbar buttons via TasklistPositions.GetButtons.", ex);
Logger.LogError("Failed to update Shortcut Guide taskbar indicator window.", ex);
}
if (buttons.Length == 0)
{
AppWindow.Hide();
return;
}
float dpi = this.DPI;
double windowsLogoColumnWidth = this.WindowsLogoColumnWidth.Width.Value;
double windowHeight = 58;
double windowMargin = 8 * dpi;
double windowWidth = windowsLogoColumnWidth;
double xPosition = buttons[0].X - (windowsLogoColumnWidth * dpi);
double yPosition = this.WorkArea.Bottom - (windowHeight * dpi);
this.KeyHolder.Children.Clear();
foreach (TasklistButton b in buttons)
{
TaskbarIndicator indicator = new()
{
Label = b.Keynum >= 10 ? "0" : b.Keynum.ToString(CultureInfo.InvariantCulture),
Height = b.Height / dpi,
Width = b.Width / dpi,
};
windowWidth += indicator.Width;
this.KeyHolder.Children.Add(indicator);
double indicatorPos = (b.X - xPosition) / dpi;
Canvas.SetLeft(indicator, indicatorPos - windowsLogoColumnWidth);
}
this.MoveAndResize(xPosition - windowMargin, yPosition, windowWidth + (2 * windowMargin), windowHeight);
AppWindow.MoveInZOrderAtTop();
}
}
}

View File

@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ShortcutGuide.Converters;
using ShortcutGuide.Models;
namespace ShortcutGuide.UnitTests.ConvertersTests;
[TestClass]
public sealed class ShortcutDescriptionToKeysConverterTests
{
private static List<object> Convert(ShortcutDescription description)
=> new ShortcutDescriptionToKeysConverter().GetKeysList(description);
[TestMethod]
[DataRow("<0>")]
[DataRow("<1>")]
[DataRow("<8>")]
[DataRow("<9>")]
public void GetKeysList_LiteralDigitKey_IsPassedThroughVerbatim(string key)
{
// A literal digit key (e.g. Ctrl+9 "switch to last tab") is authored with the
// <N> convention so it is not parsed as a virtual-key code (VK 9 is Tab, VK 1 is
// the left mouse button, VK 0 is undefined). The converter forwards the token
// unchanged; KeyVisual strips the angle brackets when rendering.
var result = Convert(new ShortcutDescription(ctrl: true, shift: false, alt: false, win: false, keys: [key]));
CollectionAssert.AreEqual(new object[] { "Ctrl", key }, result);
}
[TestMethod]
public void GetKeysList_Modifiers_AreEmittedBeforeKeysInWinCtrlAltShiftOrder()
{
// Win -> 92, Ctrl -> "Ctrl", Alt -> "Alt", Shift -> 16, then the keys.
var result = Convert(new ShortcutDescription(ctrl: true, shift: true, alt: true, win: true, keys: ["A"]));
CollectionAssert.AreEqual(new object[] { 92, "Ctrl", "Alt", 16, "A" }, result);
}
[TestMethod]
public void GetKeysList_NonNumericKey_IsPassedThroughVerbatim()
{
// Non-numeric key strings (e.g. the "1 - 8" tab-range) render as-is.
var result = Convert(new ShortcutDescription(ctrl: true, shift: false, alt: false, win: false, keys: ["1 - 8"]));
CollectionAssert.AreEqual(new object[] { "Ctrl", "1 - 8" }, result);
}
[TestMethod]
public void GetKeysList_ArrowNameKey_MapsToVirtualKeyCode()
{
// Named arrow keys map to their VK codes (Up -> 38), independent of the digit handling.
var result = Convert(new ShortcutDescription(ctrl: false, shift: false, alt: false, win: false, keys: ["Up"]));
CollectionAssert.AreEqual(new object[] { 38 }, result);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="$(RepoRoot)src\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<SelfContained>true</SelfContained>
<RuntimeIdentifier Condition="'$(Platform)' == 'x64'">win-x64</RuntimeIdentifier>
<RuntimeIdentifier Condition="'$(Platform)' == 'ARM64'">win-arm64</RuntimeIdentifier>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\ShortcutGuide.UnitTests\</OutputPath>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ShortcutGuide.Ui\ShortcutGuide.Ui.csproj" />
</ItemGroup>
</Project>

View File

@@ -9,7 +9,6 @@ using ManagedCommon;
using PowerToys.Interop;
using PowerToys.ModuleContracts;
using WorkspacesCsharpLibrary.Data;
using WorkspacesCsharpLibrary.SettingsService;
namespace Workspaces.ModuleServices;
@@ -53,8 +52,6 @@ public sealed class WorkspaceService : ModuleServiceBase, IWorkspaceService
try
{
EnsureSettingsInitialized(SettingsBootstrapper.TriggerReason.WorkspaceLaunching);
var powertoysBaseDir = PowerToysPathResolver.GetPowerToysInstallPath();
if (string.IsNullOrEmpty(powertoysBaseDir))
{
@@ -87,8 +84,6 @@ public sealed class WorkspaceService : ModuleServiceBase, IWorkspaceService
{
try
{
EnsureSettingsInitialized();
var items = WorkspacesStorage.Load();
return Task.FromResult(OperationResults.Ok<IReadOnlyList<ProjectWrapper>>(items));
@@ -98,27 +93,4 @@ public sealed class WorkspaceService : ModuleServiceBase, IWorkspaceService
return Task.FromResult(OperationResults.Fail<IReadOnlyList<ProjectWrapper>>($"Failed to read workspaces: {ex.Message}"));
}
}
// Deferred settings initialization (Design-v6-Final.md §11). Composes the
// service-initialization and legacy-migration blocks behind one call so new
// trigger points only have to invoke SettingsBootstrapper.EnsureInitialized.
// On a per-machine install the service is already up, so provisioning is a
// no-op and only the migration backstop runs. On a per-user install with no
// service yet, this performs the one-time elevation to register + harden it.
private static void EnsureSettingsInitialized(
SettingsBootstrapper.TriggerReason reason = SettingsBootstrapper.TriggerReason.EditorOpened)
{
try
{
SettingsBootstrapper.EnsureInitialized(new BootstrapRequest
{
Reason = reason,
InstallFolder = PowerToysPathResolver.GetPowerToysInstallPath(),
});
}
catch (Exception)
{
// Best-effort; on failure reads fall back per WorkspacesStorage.
}
}
}

View File

@@ -9,77 +9,29 @@ using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using WorkspacesCsharpLibrary.SettingsService;
namespace WorkspacesCsharpLibrary.Data;
/// <summary>
/// Reader/writer for persisted workspaces. All access goes through the
/// PTSettingsSvc service (Design-v6-Final.md §10): the service stores opaque
/// bytes, this class owns the JSON shape, defensive parsing and the
/// no-service last-resort fallback to the legacy %LocalAppData% file.
/// Lightweight reader for persisted workspaces.
/// </summary>
public static class WorkspacesStorage
{
public static IReadOnlyList<ProjectWrapper> Load()
{
var rc = PTSettingsClient.GetBlob(out var blob);
switch (rc)
var filePath = GetDefaultFilePath();
if (!File.Exists(filePath))
{
case PTSettingsClient.Result.Ok:
return ParseDefensive(blob);
case PTSettingsClient.Result.NotFound:
// Service is up but this user has no blob yet (first run /
// pre-migration). Not an error.
return Array.Empty<ProjectWrapper>();
case PTSettingsClient.Result.Unavailable:
// No service installed (no-admin install / declined elevation).
// Last resort: read the legacy file directly (Design §10/§11).
return ParseDefensive(ReadLegacyBytes());
default:
// AuthRejected / Protocol / IoError → fail safe to empty.
return Array.Empty<ProjectWrapper>();
}
}
/// <summary>
/// Persists the workspaces through the service. Returns true on success.
/// Falls back to a direct legacy-file write only when no service exists.
/// </summary>
public static bool Save(IReadOnlyList<ProjectWrapper> workspaces)
{
byte[] bytes = Serialise(workspaces);
var rc = PTSettingsClient.PutBlob(bytes);
switch (rc)
{
case PTSettingsClient.Result.Ok:
return true;
case PTSettingsClient.Result.Unavailable:
return WriteLegacyBytes(bytes);
default:
return false;
}
}
private static IReadOnlyList<ProjectWrapper> ParseDefensive(byte[] bytes)
{
if (bytes == null || bytes.Length == 0)
{
return Array.Empty<ProjectWrapper>();
return [];
}
try
{
var data = JsonSerializer.Deserialize(bytes, WorkspacesStorageJsonContext.Default.WorkspacesFile);
var json = File.ReadAllText(filePath);
var data = JsonSerializer.Deserialize(json, WorkspacesStorageJsonContext.Default.WorkspacesFile);
if (data?.Workspaces == null)
{
return Array.Empty<ProjectWrapper>();
return [];
}
return data.Workspaces
@@ -98,77 +50,16 @@ public static class WorkspacesStorage
.ToList()
.AsReadOnly();
}
catch (JsonException)
{
return Array.Empty<ProjectWrapper>();
}
catch (NotSupportedException)
catch
{
return Array.Empty<ProjectWrapper>();
}
}
private static byte[] Serialise(IReadOnlyList<ProjectWrapper> workspaces)
public static string GetDefaultFilePath()
{
var file = new WorkspacesFile
{
Workspaces = (workspaces ?? new List<ProjectWrapper>())
.Select(ws => new WorkspaceProject
{
Id = ws.Id,
Name = ws.Name,
Applications = ws.Applications ?? new List<ApplicationWrapper>(),
MonitorConfiguration = ws.MonitorConfiguration ?? new List<MonitorConfigurationWrapper>(),
CreationTime = ws.CreationTime,
LastLaunchedTime = ws.LastLaunchedTime,
IsShortcutNeeded = ws.IsShortcutNeeded,
MoveExistingWindows = ws.MoveExistingWindows,
})
.ToList(),
};
return JsonSerializer.SerializeToUtf8Bytes(file, WorkspacesStorageJsonContext.Default.WorkspacesFile);
}
private static byte[] ReadLegacyBytes()
{
try
{
var legacy = SettingsPaths.LegacyWorkspacesFile();
return File.Exists(legacy) ? File.ReadAllBytes(legacy) : Array.Empty<byte>();
}
catch (IOException)
{
return Array.Empty<byte>();
}
catch (UnauthorizedAccessException)
{
return Array.Empty<byte>();
}
}
private static bool WriteLegacyBytes(byte[] bytes)
{
try
{
var legacy = SettingsPaths.LegacyWorkspacesFile();
var dir = Path.GetDirectoryName(legacy);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllBytes(legacy, bytes);
return true;
}
catch (IOException)
{
return false;
}
catch (UnauthorizedAccessException)
{
return false;
}
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(localAppData, "Microsoft", "PowerToys", "Workspaces", "workspaces.json");
}
internal sealed class WorkspacesFile

View File

@@ -1,26 +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.
#nullable enable
namespace WorkspacesCsharpLibrary.SettingsService;
/// <summary>
/// Inputs for <see cref="SettingsBootstrapper.EnsureInitialized"/>. Hosts build
/// this from their own context (install-path resolver, optional test seam).
/// </summary>
public sealed class BootstrapRequest
{
/// <summary>What triggered the bootstrap (an explicit request bypasses back-off).</summary>
public SettingsBootstrapper.TriggerReason Reason { get; init; }
/// <summary>
/// Resolved PowerToys install folder. When null/empty, service provisioning
/// is skipped and only migration (with its no-service fallback) runs.
/// </summary>
public string? InstallFolder { get; init; }
/// <summary>Optional elevation override forwarded to the provisioner (tests / headless).</summary>
public ServiceProvisioner.ElevationRunner? ElevationRunner { get; init; }
}

View File

@@ -1,186 +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.IO;
using System.IO.Pipes;
using System.Security.Principal;
namespace WorkspacesCsharpLibrary.SettingsService;
/// <summary>
/// Managed client for PTSettingsSvc. Mirrors the native client and the
/// service's wire protocol (see Protocol.h) so the Editor, runner and unit
/// tests can talk to the service without P/Invoke. The service treats the
/// payload as opaque bytes; all JSON / schema concerns live in the caller.
/// </summary>
public static class PTSettingsClient
{
/// <summary>Coarse result surfaced to callers (mirrors the service status bands).</summary>
public enum Result : byte
{
/// <summary>Request succeeded.</summary>
Ok = 0,
/// <summary>GetBlob: the blob does not exist yet (service is up).</summary>
NotFound,
/// <summary>Caller authentication / namespace check failed.</summary>
AuthRejected,
/// <summary>No service to talk to (not installed / not running).</summary>
Unavailable,
/// <summary>Framing / unexpected protocol error.</summary>
Protocol,
/// <summary>Underlying file IO failed in the service.</summary>
IoError,
}
// Mirror of PTSettingsSvc::kPipeName (server side strips the \\.\pipe\ prefix).
public const string PipeName = "PTSettingsSvc";
// Mirror of PTSettingsSvc::kMaxPayloadBytes (1 MiB).
private const int MaxPayloadBytes = 1 * 1024 * 1024;
private const int ConnectTimeoutMs = 3000;
// Opcodes (mirror of PTSettingsSvc::Opcode).
private const byte OpPing = 0x00;
private const byte OpGetBlob = 0x01;
private const byte OpPutBlob = 0x02;
/// <summary>Liveness probe. Authentication still runs server-side.</summary>
public static Result Ping()
{
return RoundTrip(OpPing, ReadOnlySpan<byte>.Empty, out _);
}
/// <summary>Reads this caller's namespace blob. Returns NotFound if none exists yet.</summary>
public static Result GetBlob(out byte[] blob)
{
var rc = RoundTrip(OpGetBlob, ReadOnlySpan<byte>.Empty, out var resp);
blob = rc == Result.Ok ? resp : Array.Empty<byte>();
return rc;
}
/// <summary>Atomically replaces this caller's namespace blob with the given bytes.</summary>
public static Result PutBlob(ReadOnlySpan<byte> blob)
{
return RoundTrip(OpPutBlob, blob, out _);
}
private static Result RoundTrip(byte opcode, ReadOnlySpan<byte> payload, out byte[] response)
{
response = Array.Empty<byte>();
if (payload.Length > MaxPayloadBytes)
{
return Result.Protocol;
}
NamedPipeClientStream pipe;
try
{
// TokenImpersonation lets the service impersonate us to read our
// SID (per-user data partitioning) and open our process for the
// image-path / signature checks.
pipe = new NamedPipeClientStream(
".",
PipeName,
PipeDirection.InOut,
PipeOptions.None,
TokenImpersonationLevel.Impersonation);
pipe.Connect(ConnectTimeoutMs);
}
catch (TimeoutException)
{
return Result.Unavailable;
}
catch (IOException)
{
return Result.Unavailable;
}
catch (UnauthorizedAccessException)
{
return Result.Unavailable;
}
using (pipe)
{
try
{
Span<byte> header = stackalloc byte[5];
header[0] = opcode;
BitConverter.TryWriteBytes(header[1..], (uint)payload.Length);
pipe.Write(header);
if (payload.Length > 0)
{
pipe.Write(payload);
}
pipe.Flush();
Span<byte> respHeader = stackalloc byte[5];
if (!ReadExact(pipe, respHeader))
{
return Result.Protocol;
}
byte status = respHeader[0];
uint respLen = BitConverter.ToUInt32(respHeader[1..]);
if (respLen > MaxPayloadBytes)
{
return Result.Protocol;
}
if (respLen > 0)
{
response = new byte[respLen];
if (!ReadExact(pipe, response))
{
response = Array.Empty<byte>();
return Result.Protocol;
}
}
return MapStatus(status);
}
catch (IOException)
{
return Result.Protocol;
}
}
}
private static bool ReadExact(Stream stream, Span<byte> dest)
{
int offset = 0;
while (offset < dest.Length)
{
int got = stream.Read(dest[offset..]);
if (got <= 0)
{
return false;
}
offset += got;
}
return true;
}
private static Result MapStatus(byte status)
{
// Mirror of PTSettingsSvc::Status, collapsed to the coarse Result.
return status switch
{
0x00 => Result.Ok,
0x20 => Result.NotFound,
0x10 or 0x11 or 0x12 => Result.AuthRejected,
0x21 => Result.IoError,
_ => Result.Protocol,
};
}
}

View File

@@ -1,48 +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.
#nullable enable
namespace WorkspacesCsharpLibrary.SettingsService;
/// <summary>
/// Inputs for <see cref="ServiceProvisioner.EnsureProvisioned"/>. Paths are
/// supplied by the caller (resolved from the install folder) so the provisioner
/// stays free of host/registry dependencies and is fully testable.
/// </summary>
public sealed class ProvisionOptions
{
/// <summary>Full path to the settings-service executable to register.</summary>
public string? ServiceBinaryPath { get; init; }
/// <summary>Full path to the signed service MSIX to deploy (deferred install).</summary>
public string? ServiceMsixPath { get; init; }
/// <summary>SID of the user to harden; defaults to the current user when null/empty.</summary>
public string? UserSid { get; init; }
/// <summary>
/// When true, bypass the "already attempted" back-off and prompt again.
/// Use for explicit user actions (e.g. an "enable protection" toggle).
/// </summary>
public bool Force { get; init; }
/// <summary>
/// Optional override for how the elevated step is launched. Defaults to
/// <see cref="ServiceProvisioner.RunElevatedPowerShell"/> (a real UAC prompt).
/// Tests and headless hosts can inject a direct runner.
/// </summary>
public ServiceProvisioner.ElevationRunner? ElevationRunner { get; init; }
/// <summary>Builds options from a resolved PowerToys install folder.</summary>
public static ProvisionOptions FromInstallFolder(string installFolder, bool force = false)
{
return new ProvisionOptions
{
ServiceBinaryPath = SettingsPaths.ServiceBinaryPath(installFolder),
ServiceMsixPath = SettingsPaths.ServiceMsixPath(installFolder),
Force = force,
};
}
}

View File

@@ -1,256 +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.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Security.Principal;
#nullable enable
namespace WorkspacesCsharpLibrary.SettingsService;
/// <summary>
/// Service-initialization block (Design-v6-Final.md §11 "Lazy per-user install").
///
/// The per-machine MSI registers PTSettingsSvc eagerly at install time. A
/// per-user install ships the service payload unregistered; this block performs
/// the one-time elevation that registers the machine-wide service and hardens
/// the current user's protected store the first time protection is actually
/// needed. It is deliberately self-contained so the same logic can be invoked
/// from any trigger point (editor open, first save, workspace launch, an
/// explicit Settings toggle) — see <see cref="SettingsBootstrapper"/>.
///
/// The elevation step is injectable (<see cref="ElevationRunner"/>) so callers
/// and tests can substitute the UAC prompt with a direct run.
/// </summary>
public static class ServiceProvisioner
{
/// <summary>Result of an attempt to provision the service for the current user.</summary>
public enum Outcome
{
/// <summary>The service was already reachable; nothing to do.</summary>
ServiceAvailable,
/// <summary>Elevation ran and the service is now reachable.</summary>
Provisioned,
/// <summary>Elevation ran but the service still isn't reachable.</summary>
AttemptedNotConfirmed,
/// <summary>A prior attempt was already made; not re-prompting (unless forced).</summary>
AlreadyAttempted,
/// <summary>The user declined the elevation (UAC cancelled).</summary>
UserDeclined,
/// <summary>The service payload (exe / script) was not found in the install.</summary>
PayloadMissing,
/// <summary>The elevation could not be launched at all.</summary>
ElevationFailed,
}
/// <summary>Outcome of launching the elevated provisioning helper.</summary>
public enum ElevationResult
{
/// <summary>The elevated helper ran to completion.</summary>
Completed,
/// <summary>The user cancelled the UAC prompt.</summary>
Declined,
/// <summary>The helper could not be launched.</summary>
Failed,
}
/// <summary>
/// Launches the elevated provisioning helper. Implementations must block
/// until the helper exits and report whether it completed, was declined, or
/// failed to launch. The default is <see cref="RunElevatedPowerShell"/>.
/// </summary>
public delegate ElevationResult ElevationRunner(string fileName, string arguments);
/// <summary>True when the service answers (installed and running).</summary>
public static bool IsServiceAvailable()
{
// Fast pre-check: if the named pipe doesn't exist, the service isn't
// running, so skip PTSettingsClient.Ping() whose connect waits out a
// multi-second timeout for a missing pipe. This keeps the common
// "no service yet" path (per-user, pre-provision) cheap (~ms) instead
// of blocking the caller for the full connect timeout.
if (!PipeExists())
{
return false;
}
return PTSettingsClient.Ping() != PTSettingsClient.Result.Unavailable;
}
private static bool PipeExists()
{
try
{
foreach (var pipe in Directory.EnumerateFiles(@"\\.\pipe\"))
{
if (string.Equals(Path.GetFileName(pipe), PTSettingsClient.PipeName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
catch (Exception)
{
// If enumeration fails for any reason, fall back to the (slower but
// authoritative) connect probe rather than wrongly reporting absent.
return true;
}
return false;
}
/// <summary>
/// Ensures the service is provisioned for the current user, performing the
/// one-time elevation if needed. Idempotent and sentinel-guarded so it is
/// safe to call from multiple trigger points.
/// </summary>
public static Outcome EnsureProvisioned(ProvisionOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (IsServiceAvailable())
{
return Outcome.ServiceAvailable;
}
// Back off if we've already prompted this user, unless the caller forces
// it (e.g. an explicit "enable protection" action in Settings).
if (!options.Force && File.Exists(SettingsPaths.ProvisionAttemptSentinel()))
{
return Outcome.AlreadyAttempted;
}
var serviceMsix = options.ServiceMsixPath;
if (string.IsNullOrEmpty(serviceMsix) || !File.Exists(serviceMsix))
{
// No package to install from (e.g. a no-admin xcopy deployment).
// Don't write the sentinel: a later install that adds the payload
// should still be allowed to try.
return Outcome.PayloadMissing;
}
var userSid = string.IsNullOrEmpty(options.UserSid)
? WindowsIdentity.GetCurrent().User?.Value
: options.UserSid;
if (string.IsNullOrEmpty(userSid))
{
return Outcome.ElevationFailed;
}
// Record the attempt up front so a crash mid-elevation doesn't make us
// re-prompt on the next trigger.
TryWriteAttemptSentinel();
var runner = options.ElevationRunner ?? RunElevatedPowerShell;
var arguments = BuildInstallArguments(serviceMsix);
var elevation = runner("powershell.exe", arguments);
switch (elevation)
{
case ElevationResult.Declined:
return Outcome.UserDeclined;
case ElevationResult.Failed:
return Outcome.ElevationFailed;
case ElevationResult.Completed:
default:
return IsServiceAvailable() ? Outcome.Provisioned : Outcome.AttemptedNotConfirmed;
}
}
/// <summary>
/// Builds the elevated install command. Deploys the SIGNED service MSIX via
/// <c>Add-AppxPackage</c> — an inline command (in our signed binary, NOT a
/// user-writable script) whose only payload is the signed .msix; the OS
/// verifies its signature on deploy, so this cannot run attacker code. The
/// packaged windows.service extension auto-registers PTSettingsSvc; DACL and
/// migration are then done by the LocalSystem service (Design §12.1) — no
/// extra elevation. Replaces the retired user-writable Harden-PtSettings ps1.
/// </summary>
public static string BuildInstallArguments(string serviceMsix)
{
// -ForceApplicationShutdown is REQUIRED for the upgrade case: a packaged
// windows.service holds its binaries while running, so replacing them on
// an in-place update fails with 0x80073D02 ("resources ... currently in
// use") unless the running service is force-stopped first. The flag stops
// the old service so the new version's files can be laid down; the service
// then auto-restarts pointing at the new exe (verified 2026-06-30,
// Design §12.6). -ForceUpdateFromAnyVersion allows same/again deploys.
return "-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "
+ "\"Add-AppxPackage -Path '" + serviceMsix + "' -ForceUpdateFromAnyVersion -ForceApplicationShutdown\"";
}
/// <summary>
/// Default elevation runner: launches PowerShell elevated (UAC) and waits.
/// Maps a cancelled UAC prompt to <see cref="ElevationResult.Declined"/>.
/// </summary>
public static ElevationResult RunElevatedPowerShell(string fileName, string arguments)
{
try
{
var psi = new ProcessStartInfo(fileName, arguments)
{
UseShellExecute = true,
Verb = "runas",
WindowStyle = ProcessWindowStyle.Hidden,
};
using var proc = Process.Start(psi);
if (proc == null)
{
return ElevationResult.Failed;
}
proc.WaitForExit();
return ElevationResult.Completed;
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
// ERROR_CANCELLED — the user dismissed the UAC prompt.
return ElevationResult.Declined;
}
catch (Win32Exception)
{
return ElevationResult.Failed;
}
catch (InvalidOperationException)
{
return ElevationResult.Failed;
}
}
private static void TryWriteAttemptSentinel()
{
try
{
var sentinel = SettingsPaths.ProvisionAttemptSentinel();
var dir = Path.GetDirectoryName(sentinel);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllText(sentinel, DateTime.UtcNow.ToString("o"));
}
catch (IOException)
{
// Best-effort: a missing sentinel only means we may re-prompt once more.
}
catch (UnauthorizedAccessException)
{
}
}
}

View File

@@ -1,111 +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.Threading;
#nullable enable
namespace WorkspacesCsharpLibrary.SettingsService;
/// <summary>
/// Orchestrates the two settings blocks — service initialization
/// (<see cref="ServiceProvisioner"/>) and settings-file migration
/// (<see cref="WorkspacesMigration"/>) — behind a single entry point that can be
/// invoked from any number of trigger points (editor open, first save, workspace
/// launch, an explicit Settings toggle). Keeping the orchestration here means
/// new trigger points only have to call <see cref="EnsureInitialized"/>; they
/// don't need to know the ordering or guards.
/// </summary>
public static class SettingsBootstrapper
{
/// <summary>Where the bootstrap was invoked from (diagnostics / policy).</summary>
public enum TriggerReason
{
/// <summary>The Workspaces editor was opened / its list loaded.</summary>
EditorOpened,
/// <summary>A workspace is about to be saved.</summary>
WorkspaceSaving,
/// <summary>A workspace is about to be launched.</summary>
WorkspaceLaunching,
/// <summary>The user explicitly asked to enable protection.</summary>
ExplicitUserRequest,
}
/// <summary>Combined result of a bootstrap pass.</summary>
public readonly record struct Result(
ServiceProvisioner.Outcome Provision,
WorkspacesMigration.Outcome Migration);
// Auto (non-forced) bootstrap runs at most once per process to keep the hot
// path (every editor open) cheap; an explicit user request always runs.
private static int _autoBootstrapped;
/// <summary>
/// Ensures the service is provisioned (if a payload is available) and that
/// this user's legacy data has been migrated. Safe to call repeatedly and
/// from multiple trigger points.
/// </summary>
/// <param name="request">Trigger, install folder and provisioning knobs.</param>
public static Result EnsureInitialized(BootstrapRequest request)
{
ArgumentNullException.ThrowIfNull(request);
var force = request.Reason == TriggerReason.ExplicitUserRequest;
if (!force && Interlocked.Exchange(ref _autoBootstrapped, 1) != 0)
{
// Already ran the automatic pass this process; nothing cheap left to do.
return new Result(ServiceProvisioner.Outcome.AlreadyAttempted, WorkspacesMigration.Outcome.AlreadyMigrated);
}
// Block 1: service initialization. Only attempt when we have an install
// folder to locate the payload; otherwise skip straight to migration,
// which has its own no-service fallback.
var provision = ServiceProvisioner.Outcome.PayloadMissing;
if (!string.IsNullOrEmpty(request.InstallFolder))
{
try
{
var options = ProvisionOptions.FromInstallFolder(request.InstallFolder!, force);
if (request.ElevationRunner != null)
{
options = new ProvisionOptions
{
ServiceBinaryPath = options.ServiceBinaryPath,
ServiceMsixPath = options.ServiceMsixPath,
UserSid = options.UserSid,
Force = force,
ElevationRunner = request.ElevationRunner,
};
}
provision = ServiceProvisioner.EnsureProvisioned(options);
}
catch (Exception)
{
// Provisioning is best-effort; fall through to migration so the
// editor still works via the no-service fallback.
provision = ServiceProvisioner.Outcome.ElevationFailed;
}
}
// Block 2: settings-file migration. Idempotent; when the service is up
// this seeds the protected blob, otherwise it no-ops cleanly.
var migration = WorkspacesMigration.Outcome.SkippedServiceUnavailable;
try
{
migration = WorkspacesMigration.Run();
}
catch (Exception)
{
// Best-effort backstop; reads fall back per WorkspacesStorage.
}
return new Result(provision, migration);
}
}

View File

@@ -1,119 +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.IO;
using System.Security.Principal;
namespace WorkspacesCsharpLibrary.SettingsService;
/// <summary>
/// Resolves the new (v6) and legacy paths used for the Workspaces data.
/// The new location lives under %ProgramData% in the service-managed
/// SettingsSvc tree, partitioned by namespace and per-user SID; only the
/// PTSettingsSvc service may write into it, but the owning user (and
/// Administrators) can read it directly. The legacy location is the
/// pre-v6 %LocalAppData% file, used only by one-shot migration and the
/// no-service last-resort fallback.
/// </summary>
public static class SettingsPaths
{
// Namespace id the Workspaces module is bound to in the service's
// CallerBinding table (mirror of the native "Workspaces" namespace).
private const string NamespaceId = "Workspaces";
// Canonical file name kept inside the namespace folder (mirror of the
// native CallerBinding fileName). Keeps the original, human-readable name.
private const string WorkspacesFileName = "workspaces.json";
// %ProgramData%\Microsoft\PowerToys\Settings (the service-managed store root)
private const string SettingsStoreSubpath = @"Microsoft\PowerToys\Settings";
// Pre-v6 per-user data folder under %LocalAppData%.
private const string LegacySubpath = @"Microsoft\PowerToys\Workspaces";
// Subfolder of the install root that carries the settings-service payload
// (the service exe and the per-user hardening script). The per-machine MSI
// registers the service from here; the per-user install ships the same
// payload unregistered so deferred initialization can register it lazily.
private const string ServicePayloadSubdir = "WorkspacesSettingsService";
/// <summary>File name of the settings-service executable.</summary>
public const string ServiceBinaryName = "PowerToys.PTSettingsSvc.exe";
/// <summary>File name of the signed service MSIX package (deferred install).</summary>
public const string ServiceMsixName = "PTSettingsSvc.msix";
/// <summary>%ProgramData%\Microsoft\PowerToys\Settings (the store root).</summary>
public static string ServiceStoreRoot()
{
var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
return Path.Combine(programData, SettingsStoreSubpath);
}
/// <summary>%ProgramData%\Microsoft\PowerToys\Settings\&lt;current-user-sid&gt; (per-user node).</summary>
public static string CurrentUserFolder()
{
var sid = WindowsIdentity.GetCurrent().User?.Value
?? throw new InvalidOperationException("No current user SID");
return Path.Combine(ServiceStoreRoot(), sid);
}
/// <summary>%ProgramData%\Microsoft\PowerToys\Settings\&lt;sid&gt;\Workspaces (namespace folder).</summary>
public static string CurrentUserNamespaceFolder()
{
return Path.Combine(CurrentUserFolder(), NamespaceId);
}
/// <summary>The per-user settings file the service reads/writes (direct-read allowed).</summary>
public static string CurrentUserFile()
{
return Path.Combine(CurrentUserNamespaceFolder(), WorkspacesFileName);
}
/// <summary>The pre-v6 location. Used by one-shot migration and the no-service fallback.</summary>
public static string LegacyWorkspacesFile()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(localAppData, LegacySubpath, "workspaces.json");
}
/// <summary>Sentinel dropped by the runner the first time a user is migrated.</summary>
public static string MigrationSentinel()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(localAppData, LegacySubpath, ".migrated-to-svc");
}
/// <summary>
/// Sentinel recording that deferred service provisioning has already been
/// attempted for this user, so repeated trigger points don't re-prompt for
/// elevation. Lives under %LocalAppData% (user-writable): it only governs
/// UX back-off, never security.
/// </summary>
public static string ProvisionAttemptSentinel()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(localAppData, LegacySubpath, ".svc-provision-attempted");
}
/// <summary>Folder under the install root that carries the settings-service payload.</summary>
public static string ServicePayloadDir(string installFolder)
{
ArgumentException.ThrowIfNullOrEmpty(installFolder);
return Path.Combine(installFolder, ServicePayloadSubdir);
}
/// <summary>Full path to the settings-service executable inside an install folder.</summary>
public static string ServiceBinaryPath(string installFolder)
{
return Path.Combine(ServicePayloadDir(installFolder), ServiceBinaryName);
}
/// <summary>Full path to the signed service MSIX package inside an install folder.</summary>
public static string ServiceMsixPath(string installFolder)
{
return Path.Combine(ServicePayloadDir(installFolder), ServiceMsixName);
}
}

View File

@@ -1,115 +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.IO;
namespace WorkspacesCsharpLibrary.SettingsService;
/// <summary>
/// One-shot legacy migration, called by the runner on startup (idempotent).
/// The service has no "migrate" concept (Design-v6-Final.md §10): migration is
/// simply "read the legacy %LocalAppData% file once and PutBlob it through the
/// service". A sentinel under %LocalAppData% short-circuits subsequent calls.
/// </summary>
public static class WorkspacesMigration
{
public enum Outcome
{
AlreadyMigrated,
NothingToMigrate,
Migrated,
SkippedServiceUnavailable,
SkippedLegacyUnreadable,
SkippedServerRejected,
}
public static Outcome Run()
{
var sentinel = SettingsPaths.MigrationSentinel();
if (File.Exists(sentinel))
{
return Outcome.AlreadyMigrated;
}
// If the service already holds a blob for this user, another runner
// invocation migrated it; drop the sentinel and stop.
var probe = PTSettingsClient.GetBlob(out var existing);
if (probe == PTSettingsClient.Result.Ok && existing.Length > 0)
{
TryWriteSentinel(sentinel);
return Outcome.AlreadyMigrated;
}
if (probe == PTSettingsClient.Result.Unavailable)
{
return Outcome.SkippedServiceUnavailable;
}
// probe is NotFound (no blob yet) or a transient error — proceed only
// when we positively know there is nothing yet.
if (probe != PTSettingsClient.Result.NotFound)
{
return Outcome.SkippedServerRejected;
}
var legacy = SettingsPaths.LegacyWorkspacesFile();
if (!File.Exists(legacy))
{
TryWriteSentinel(sentinel);
return Outcome.NothingToMigrate;
}
byte[] bytes;
try
{
bytes = File.ReadAllBytes(legacy);
}
catch (IOException)
{
return Outcome.SkippedLegacyUnreadable;
}
catch (System.UnauthorizedAccessException)
{
return Outcome.SkippedLegacyUnreadable;
}
var put = PTSettingsClient.PutBlob(bytes);
switch (put)
{
case PTSettingsClient.Result.Ok:
// Keep the legacy file as a backup for one release; the service
// blob is the authority going forward.
TryWriteSentinel(sentinel);
return Outcome.Migrated;
case PTSettingsClient.Result.Unavailable:
return Outcome.SkippedServiceUnavailable;
default:
return Outcome.SkippedServerRejected;
}
}
private static void TryWriteSentinel(string sentinel)
{
try
{
var dir = Path.GetDirectoryName(sentinel);
if (!string.IsNullOrEmpty(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllText(sentinel, System.DateTime.UtcNow.ToString("o"));
}
catch (IOException)
{
// Best-effort: if we can't write the sentinel we simply re-probe
// next time, which is cheap and idempotent.
}
catch (System.UnauthorizedAccessException)
{
}
}
}

View File

@@ -19,23 +19,9 @@ public class FolderUtils
return Path.GetTempPath();
}
// User-writable working folder for the Editor's transient files (icons,
// temp-project handoff) AND the legacy / no-service fallback store.
//
// v6 note: the *protected* settings store does NOT live here — it is the
// service-managed blob under %ProgramData% (see SettingsPaths / §9). The
// Editor reads/writes the real settings through PTSettingsClient
// (GetBlob / PutBlob); this %LocalAppData% path is only the working dir and
// the no-service fallback, both of which must stay user-writable.
// Note: the same path should be used in SnapshotTool and Launcher
public static string DataFolder()
{
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
}
// The pre-v6 location. Same as DataFolder() now; kept as a distinct name
// for the one-shot migration source and the no-service fallback.
public static string LegacyDataFolder()
{
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\Microsoft\\PowerToys\\Workspaces";
}
}

View File

@@ -6,10 +6,8 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using ManagedCommon;
using WorkspacesCsharpLibrary.Data;
using WorkspacesCsharpLibrary.SettingsService;
using WorkspacesCsharpLibrary.Utils;
using WorkspacesEditor.Models;
using WorkspacesEditor.ViewModels;
@@ -26,45 +24,14 @@ namespace WorkspacesEditor.Utils
{
try
{
// Deferred per-user service init + legacy migration (Design §11 / §14.1).
// On a per-machine install the service is already up (no-op); on a
// per-user install with no service yet, this performs the one-time
// elevation to register + harden it, then migrates the legacy file.
TryBootstrapSettings();
WorkspacesData parser = new();
WorkspacesData.WorkspacesListWrapper workspaces;
// v6: read the settings through the service (GetBlob). Fall back to
// the legacy %LocalAppData% file only when no service is installed
// (no-admin / declined-UAC), per §10.
var rc = PTSettingsClient.GetBlob(out var blob);
switch (rc)
if (!File.Exists(parser.File))
{
case PTSettingsClient.Result.Ok:
workspaces = parser.Deserialize(Encoding.UTF8.GetString(blob));
break;
case PTSettingsClient.Result.NotFound:
// Service is up but this user has no blob yet (first run).
return new ParsingResult(true);
case PTSettingsClient.Result.Unavailable:
if (!File.Exists(parser.File))
{
Logger.LogWarning($"Workspaces storage file not found: {parser.File}");
return new ParsingResult(true);
}
workspaces = parser.Read(parser.File);
break;
default:
// AuthRejected / Protocol / IoError → fail safe to empty.
Logger.LogWarning($"GetBlob returned {rc}; treating workspaces as empty.");
return new ParsingResult(true);
Logger.LogWarning($"Workspaces storage file not found: {parser.File}");
return new ParsingResult(true);
}
WorkspacesData.WorkspacesListWrapper workspaces = parser.Read(parser.File);
if (workspaces.Workspaces == null)
{
return new ParsingResult(true);
@@ -85,23 +52,6 @@ namespace WorkspacesEditor.Utils
}
}
private static void TryBootstrapSettings()
{
try
{
SettingsBootstrapper.EnsureInitialized(new BootstrapRequest
{
Reason = SettingsBootstrapper.TriggerReason.EditorOpened,
InstallFolder = PowerToysPathResolver.GetPowerToysInstallPath(),
});
}
catch (Exception e)
{
// Best-effort: on failure reads/writes fall back to the legacy file.
Logger.LogWarning($"Settings bootstrap failed (continuing with fallback): {e.Message}");
}
}
public Project ParseTempProject()
{
try
@@ -201,35 +151,8 @@ namespace WorkspacesEditor.Utils
try
{
string json = serializer.Serialize(workspacesWrapper);
if (useTempFile)
{
// Transient snapshot→editor handoff stays a direct user-writable
// file (not the protected store).
IOUtils ioUtils = new();
ioUtils.WriteFile(TempProjectData.File, json);
return;
}
// v6: persist the settings through the service (PutBlob). Fall back
// to the legacy %LocalAppData% file only when no service is installed
// (no-admin / declined-UAC), per §10.
var rc = PTSettingsClient.PutBlob(Encoding.UTF8.GetBytes(json));
switch (rc)
{
case PTSettingsClient.Result.Ok:
break;
case PTSettingsClient.Result.Unavailable:
IOUtils fallback = new();
fallback.WriteFile(serializer.File, json);
break;
default:
Logger.LogError($"Failed to save workspaces through the settings service: {rc}");
break;
}
IOUtils ioUtils = new();
ioUtils.WriteFile(useTempFile ? TempProjectData.File : serializer.File, serializer.Serialize(workspacesWrapper));
}
catch (Exception e)
{

View File

@@ -9,7 +9,6 @@
#include <AppLauncher.h>
#include <WorkspacesLib/AppUtils.h>
#include <WorkspacesLib/JsonUtils.h>
Launcher::Launcher(const WorkspacesData::WorkspacesProject& project,
std::vector<WorkspacesData::WorkspacesProject>& workspaces,
@@ -69,7 +68,7 @@ Launcher::~Launcher()
break;
}
}
JsonUtils::WriteWorkspacesToService(m_workspaces);
json::to_file(WorkspacesData::WorkspacesFile(), WorkspacesData::WorkspacesListJSON::ToJson(m_workspaces));
}
// telemetry

View File

@@ -126,7 +126,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
if (projectToLaunch.id.empty())
{
auto file = WorkspacesData::WorkspacesFile();
auto res = JsonUtils::ReadWorkspacesFromService();
auto res = JsonUtils::ReadWorkspaces(file);
if (res.isOk())
{
workspaces = res.getValue();
@@ -201,7 +201,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
}
}
JsonUtils::WriteWorkspacesToService(workspaces);
json::to_file(WorkspacesData::WorkspacesFile(), WorkspacesData::WorkspacesListJSON::ToJson(workspaces));
}
// launch

View File

@@ -5,8 +5,6 @@
#include <common/logger/logger.h>
#include "../WorkspacesSettingsClient/PTSettingsClient.h"
namespace JsonUtils
{
Result<WorkspacesData::WorkspacesProject, WorkspacesFileError> ReadSingleWorkspace(const std::wstring& fileName)
@@ -91,76 +89,6 @@ namespace JsonUtils
return true;
}
Result<std::vector<WorkspacesData::WorkspacesProject>, WorkspacesFileError> ReadWorkspacesFromService()
{
std::vector<uint8_t> bytes;
auto rc = PTSettingsClient::GetBlob(bytes);
switch (rc)
{
case PTSettingsClient::Result::Ok:
{
try
{
// The blob is the same UTF-8 JSON the Editor writes.
std::string utf8(bytes.begin(), bytes.end());
auto obj = json::JsonValue::Parse(winrt::to_hstring(utf8)).GetObjectW();
auto parsed = WorkspacesData::WorkspacesListJSON::FromJson(obj);
if (parsed.has_value())
{
return Ok(parsed.value());
}
Logger::critical("Incorrect Workspaces blob from service");
return Error(WorkspacesFileError::IncorrectFileError);
}
catch (std::exception ex)
{
Logger::critical("Exception parsing Workspaces blob: {}", ex.what());
return Error(WorkspacesFileError::FileReadingError);
}
}
case PTSettingsClient::Result::NotFound:
// Service is up but this user has no blob yet (first run).
return Ok(std::vector<WorkspacesData::WorkspacesProject>{});
case PTSettingsClient::Result::ServiceUnavailable:
// No service (no-admin / declined-UAC): legacy file fallback.
return ReadWorkspaces(WorkspacesData::WorkspacesFile());
default:
Logger::error("GetBlob failed ({}); treating workspaces as empty.", static_cast<int>(rc));
return Ok(std::vector<WorkspacesData::WorkspacesProject>{});
}
}
bool WriteWorkspacesToService(const std::vector<WorkspacesData::WorkspacesProject>& projects)
{
try
{
std::wstring str{ WorkspacesData::WorkspacesListJSON::ToJson(projects).Stringify().c_str() };
std::string utf8 = winrt::to_string(winrt::hstring(str));
std::vector<uint8_t> bytes(utf8.begin(), utf8.end());
auto rc = PTSettingsClient::PutBlob(bytes);
if (rc == PTSettingsClient::Result::Ok)
{
return true;
}
if (rc == PTSettingsClient::Result::ServiceUnavailable)
{
// No service: legacy file fallback (no-admin / declined-UAC).
return Write(WorkspacesData::WorkspacesFile(), projects);
}
Logger::error("PutBlob failed ({}) writing workspaces.", static_cast<int>(rc));
return false;
}
catch (std::exception ex)
{
Logger::error("Exception writing workspaces via service: {}", ex.what());
return false;
}
}
bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project)
{
try

View File

@@ -14,14 +14,6 @@ namespace JsonUtils
Result<WorkspacesData::WorkspacesProject, WorkspacesFileError> ReadSingleWorkspace(const std::wstring& fileName);
Result<std::vector<WorkspacesData::WorkspacesProject>, WorkspacesFileError> ReadWorkspaces(const std::wstring& fileName);
// v6: read/write the workspaces list through the PTSettingsSvc service
// (PTSettingsClient GetBlob/PutBlob) so the protected %ProgramData% store is
// the single source of truth. Both fall back to direct file IO on
// WorkspacesData::WorkspacesFile() only when the service is unavailable
// (no-admin / declined-UAC), per Design-v6-Final.md §10.
Result<std::vector<WorkspacesData::WorkspacesProject>, WorkspacesFileError> ReadWorkspacesFromService();
bool WriteWorkspacesToService(const std::vector<WorkspacesData::WorkspacesProject>& projects);
bool Write(const std::wstring& fileName, const std::vector<WorkspacesData::WorkspacesProject>& projects);
bool Write(const std::wstring& fileName, const WorkspacesData::WorkspacesProject& project);
}

View File

@@ -4,59 +4,23 @@
#include <workspaces-common/GuidUtils.h>
#include <windows.h>
#include <sddl.h>
#include <shlobj.h>
#include <vector>
#pragma comment(lib, "Advapi32.lib")
#pragma comment(lib, "Shell32.lib")
namespace NonLocalizable
{
const inline wchar_t ModuleKey[] = L"Workspaces";
}
namespace
{
// v6: the protected settings store lives under %ProgramData% and is reached
// only through the PTSettingsSvc named pipe (PTSettingsClient GetBlob/PutBlob)
// — see JsonUtils::ReadWorkspacesFromService / WriteWorkspacesToService.
//
// This %LocalAppData% folder is the *user-writable* working location: the
// pre-v6 / no-service fallback file and the transient snapshot->editor temp
// handoff. It matches the managed editor (FolderUtils.DataFolder), so the
// snapshot tool (writer) and editor (reader) agree on the temp path.
std::wstring GetUserWritableWorkspacesFolder()
{
PWSTR localAppData = nullptr;
std::wstring root;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppData)))
{
root = localAppData;
CoTaskMemFree(localAppData);
}
else
{
return L"";
}
root += L"\\Microsoft\\PowerToys\\Workspaces";
return root;
}
}
namespace WorkspacesData
{
std::wstring WorkspacesFile()
{
// No-service fallback location (also the legacy / migration source).
return GetUserWritableWorkspacesFolder() + L"\\workspaces.json";
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
return settingsFolderPath + L"\\workspaces.json";
}
std::wstring TempWorkspacesFile()
{
// Transient snapshot->editor handoff; user-writable, matches the editor.
return GetUserWritableWorkspacesFolder() + L"\\temp-workspaces.json";
std::wstring settingsFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
return settingsFolderPath + L"\\temp-workspaces.json";
}
RECT WorkspacesProject::Application::Position::toRect() const noexcept

View File

@@ -74,9 +74,6 @@
<ProjectReference Include="$(RepoRoot)src\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\WorkspacesSettingsClient\WorkspacesSettingsClient.vcxproj">
<Project>{d24e2c12-9911-4e51-b102-39e7b62b22f1}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />

View File

@@ -1,168 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#include "PTSettingsClient.h"
#include "../WorkspacesSettingsService/protocol/Protocol.h"
#include <windows.h>
#include <vector>
#include <cstring>
namespace PTSettingsClient
{
namespace
{
using PTSettingsSvc::kPipeName;
using PTSettingsSvc::kMaxPayloadBytes;
using PTSettingsSvc::Opcode;
using PTSettingsSvc::Status;
struct PipeHandle
{
HANDLE h = INVALID_HANDLE_VALUE;
~PipeHandle()
{
if (h != INVALID_HANDLE_VALUE) CloseHandle(h);
}
};
bool Connect(PipeHandle& out)
{
for (int attempt = 0; attempt < 3; ++attempt)
{
HANDLE h = CreateFileW(kPipeName,
GENERIC_READ | GENERIC_WRITE,
0,
nullptr,
OPEN_EXISTING,
// Allow the server to impersonate us
// so it can read our SID; anything
// weaker yields an Anonymous token
// and the server's auth check fails.
SECURITY_SQOS_PRESENT | SECURITY_IMPERSONATION,
nullptr);
if (h != INVALID_HANDLE_VALUE)
{
out.h = h;
return true;
}
DWORD err = GetLastError();
if (err != ERROR_PIPE_BUSY && err != ERROR_FILE_NOT_FOUND)
{
return false;
}
WaitNamedPipeW(kPipeName, 2000);
}
return false;
}
bool WriteAll(HANDLE h, const void* buf, DWORD len)
{
const BYTE* p = static_cast<const BYTE*>(buf);
while (len > 0)
{
DWORD wrote = 0;
if (!WriteFile(h, p, len, &wrote, nullptr) || wrote == 0) return false;
p += wrote;
len -= wrote;
}
return true;
}
bool ReadAll(HANDLE h, void* buf, DWORD len)
{
BYTE* p = static_cast<BYTE*>(buf);
while (len > 0)
{
DWORD got = 0;
if (!ReadFile(h, p, len, &got, nullptr) || got == 0) return false;
p += got;
len -= got;
}
return true;
}
Result MapStatus(Status s)
{
switch (s)
{
case Status::Ok: return Result::Ok;
case Status::AuthFailToken:
case Status::AuthFailCaller: return Result::AuthRejected;
case Status::NamespaceUnknown: return Result::NamespaceUnknown;
case Status::BadRequest:
case Status::UnknownOpcode: return Result::ProtocolError;
case Status::PayloadTooLarge: return Result::PayloadTooLarge;
case Status::NotFound: return Result::NotFound;
case Status::IoError: return Result::IoError;
}
return Result::UnknownStatus;
}
Result RoundTrip(Opcode op, const void* payload, uint32_t payloadLen,
std::vector<uint8_t>& outResp)
{
outResp.clear();
if (payloadLen > kMaxPayloadBytes)
{
return Result::PayloadTooLarge;
}
PipeHandle pipe;
if (!Connect(pipe))
{
return Result::ServiceUnavailable;
}
uint8_t opByte = static_cast<uint8_t>(op);
if (!WriteAll(pipe.h, &opByte, sizeof(opByte)) ||
!WriteAll(pipe.h, &payloadLen, sizeof(payloadLen)) ||
(payloadLen > 0 && !WriteAll(pipe.h, payload, payloadLen)))
{
return Result::ProtocolError;
}
uint8_t statusByte = 0;
uint32_t respLen = 0;
if (!ReadAll(pipe.h, &statusByte, sizeof(statusByte)) ||
!ReadAll(pipe.h, &respLen, sizeof(respLen)))
{
return Result::ProtocolError;
}
if (respLen > kMaxPayloadBytes)
{
return Result::ProtocolError;
}
if (respLen > 0)
{
outResp.resize(respLen);
if (!ReadAll(pipe.h, outResp.data(), respLen))
{
outResp.clear();
return Result::ProtocolError;
}
}
return MapStatus(static_cast<Status>(statusByte));
}
}
Result Ping()
{
std::vector<uint8_t> resp;
return RoundTrip(Opcode::Ping, nullptr, 0, resp);
}
Result GetBlob(std::vector<uint8_t>& outBytes)
{
return RoundTrip(Opcode::GetBlob, nullptr, 0, outBytes);
}
Result PutBlob(const std::vector<uint8_t>& bytes)
{
std::vector<uint8_t> resp;
return RoundTrip(Opcode::PutBlob,
bytes.data(),
static_cast<uint32_t>(bytes.size()),
resp);
}
}

View File

@@ -1,49 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
//
// Thin C++ client for PTSettingsSvc. Linked into PowerToys.WorkspacesEditor /
// WorkspacesSnapshotTool / runner / etc. The client is payload-agnostic —
// it shuttles opaque bytes to and from the service. Whatever the bytes mean
// is the caller's responsibility (JSON shape, schema version, sensitive-
// field stripping, migration logic — see Design-v6-Final.md §10).
//
// Modules using settings (e.g. Workspaces) wrap this in their own
// type-safe layer (Workspaces serialises its `Workspaces` object → UTF-8
// JSON bytes → PutBlob; reverse on read).
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace PTSettingsClient
{
enum class Result : uint8_t
{
Ok = 0,
ServiceUnavailable, // Pipe couldn't be opened (service stopped
// or wrong machine).
AuthRejected, // Service refused the caller — usually
// means binary isn't where the MSI put it,
// basename not allow-listed, or the
// install folder DACL isn't hardened.
NamespaceUnknown, // Caller authenticated but isn't in the
// binding table. Build-time misconfig.
NotFound, // GetBlob: blob does not exist yet.
ProtocolError, // Truncated / malformed wire frames.
PayloadTooLarge, // Local or remote rejected oversize payload.
IoError, // Service-side disk failure.
UnknownStatus, // Server returned a status code we don't recognise.
};
Result Ping();
// Reads the caller's namespace blob. Returns NotFound (with `outBytes`
// empty) when no blob has ever been written for this user+namespace.
Result GetBlob(std::vector<uint8_t>& outBytes);
// Replaces the caller's namespace blob with `bytes`. Service does
// the atomic write + DACL re-assertion.
Result PutBlob(const std::vector<uint8_t>& bytes);
}

View File

@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{D24E2C12-9911-4E51-B102-39E7B62B22F1}</ProjectGuid>
<RootNamespace>WorkspacesSettingsClient</RootNamespace>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
<ProjectName>WorkspacesSettingsClient</ProjectName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup>
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>./;../WorkspacesSettingsService;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="PTSettingsClient.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="PTSettingsClient.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -1,82 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#include "Bindings.h"
#include <cwctype>
namespace PTSettingsSvc
{
namespace
{
// The one place in the service where module-specific knowledge lives.
// Each row: { exe basename, namespace id, file name }.
//
// The on-disk file keeps its original, human-readable name (e.g.
// workspaces.json) rather than an opaque "blob.bin": the service still
// treats the bytes as opaque (it never parses them), but a real name
// aids diagnostics and lets native direct-readers (the Launcher hot
// path, §9) open the same file by the name they already use.
//
// Workspaces ships five executables; all operate on the same namespace
// ("Workspaces") / file and so share one store. The runner
// (PowerToys.exe) is bound to the same namespace so it can perform the
// one-shot legacy migration during startup.
//
// To add a new module:
// 1. Add a row for each of its executables here (with its file name).
// 2. Point that module's read/write code at PTSettingsClient.
// No service code changes required.
constexpr CallerBinding kBindings[] = {
{ L"PowerToys.WorkspacesEditor.exe", L"Workspaces", L"workspaces.json" },
{ L"PowerToys.WorkspacesLauncher.exe", L"Workspaces", L"workspaces.json" },
{ L"PowerToys.WorkspacesSnapshotTool.exe", L"Workspaces", L"workspaces.json" },
{ L"PowerToys.WorkspacesWindowArranger.exe", L"Workspaces", L"workspaces.json" },
{ L"PowerToys.WorkspacesLauncherUI.exe", L"Workspaces", L"workspaces.json" },
// Runner can act on behalf of any module that needs runner-owned
// one-shot tasks (e.g. legacy migration). v6.0 ships with one
// such module so the runner gets exactly one row.
{ L"PowerToys.exe", L"Workspaces", L"workspaces.json" },
};
bool ICaseEquals(const wchar_t* a, const wchar_t* b)
{
while (*a && *b)
{
if (std::towlower(*a) != std::towlower(*b)) return false;
++a; ++b;
}
return *a == 0 && *b == 0;
}
}
const CallerBinding* FindBindingByExeBasename(const std::wstring& basename)
{
for (const auto& row : kBindings)
{
if (ICaseEquals(basename.c_str(), row.exeBasename))
{
return &row;
}
}
return nullptr;
}
bool IsValidNamespaceId(const wchar_t* id)
{
if (!id || !*id) return false;
size_t len = 0;
for (const wchar_t* p = id; *p; ++p, ++len)
{
if (len >= 64) return false;
wchar_t c = *p;
bool ok = (c >= L'A' && c <= L'Z') ||
(c >= L'a' && c <= L'z') ||
(c >= L'0' && c <= L'9') ||
c == L'_' || c == L'-' || c == L'.';
if (!ok) return false;
}
return len > 0;
}
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
//
// Caller-to-namespace binding table for PTSettingsSvc.
//
// The service is intentionally namespace-agnostic at the storage layer —
// every PutBlob / GetBlob touches
// `<storeRoot>\<userSid>\<namespaceId>\<fileName>`.
// The only place the service knows anything module-specific is this
// table: which executable basenames are allowed to talk to it, and which
// namespace each one operates on.
//
// Adding a new PowerToys module to the protection scheme is a one-line
// change here (plus pointing that module's read/write code at PTSettingsClient).
#pragma once
#include <string>
namespace PTSettingsSvc
{
struct CallerBinding
{
const wchar_t* exeBasename; // case-insensitive compare
const wchar_t* namespaceId; // subfolder under <storeRoot>\<sid>
const wchar_t* fileName; // canonical file name kept inside that namespace folder
};
// Pointer into a static, immutable table. Lifetime is the lifetime of
// the service process. Do not free. Returns nullptr if the basename
// isn't allow-listed.
const CallerBinding* FindBindingByExeBasename(const std::wstring& basename);
// Returns true if `id` looks like a syntactically valid namespace id —
// ASCII alphanumeric / underscore / hyphen / dot, no path separators,
// length 1..64. Defensive check used before turning the id into a
// directory name.
bool IsValidNamespaceId(const wchar_t* id);
}

View File

@@ -1,256 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#include "CallerAuth.h"
#include "Bindings.h"
#include "Paths.h"
#include "CallerVerify.h"
#include <windows.h>
#include <sddl.h>
#include <pathcch.h>
#include <vector>
#include <algorithm>
#pragma comment(lib, "Advapi32.lib")
#pragma comment(lib, "Pathcch.lib")
namespace PTSettingsSvc
{
namespace
{
HRESULT RejectionForToken(HANDLE token, std::wstring& outSidString)
{
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
if (size == 0)
{
return E_FAIL;
}
std::vector<BYTE> buf(size);
if (!GetTokenInformation(token, TokenUser, buf.data(), size, &size))
{
return HRESULT_FROM_WIN32(GetLastError());
}
PSID sid = reinterpret_cast<TOKEN_USER*>(buf.data())->User.Sid;
// Reject well-known synthetic principals — we want a real
// interactive user so the data folder is scoped to a human.
const WELL_KNOWN_SID_TYPE rejected[] = {
WinLocalSystemSid,
WinLocalServiceSid,
WinNetworkServiceSid,
WinAnonymousSid,
WinNullSid,
};
for (auto wk : rejected)
{
if (IsWellKnownSid(sid, wk))
{
return E_ACCESSDENIED;
}
}
outSidString = SidToString(sid);
if (outSidString.empty())
{
return E_FAIL;
}
return S_OK;
}
std::wstring CanonicalizePath(const std::wstring& path)
{
// Open with backup-semantics so we can canonicalize even
// executables that the loader has already mapped.
HANDLE h = CreateFileW(path.c_str(),
READ_CONTROL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
nullptr);
if (h == INVALID_HANDLE_VALUE)
{
return path;
}
wchar_t buf[1024] = {};
DWORD len = GetFinalPathNameByHandleW(h, buf, ARRAYSIZE(buf), FILE_NAME_NORMALIZED);
CloseHandle(h);
if (len == 0 || len >= ARRAYSIZE(buf))
{
return path;
}
std::wstring result(buf);
if (result.compare(0, 4, L"\\\\?\\") == 0)
{
result.erase(0, 4);
}
return result;
}
std::wstring BaseName(const std::wstring& path)
{
auto pos = path.find_last_of(L"\\/");
return pos == std::wstring::npos ? path : path.substr(pos + 1);
}
}
HRESULT AuthenticateCaller(HANDLE pipeHandle, CallerIdentity& outIdentity)
{
outIdentity = {};
// 1) Capture client pid up front (cheap, doesn't need impersonation).
ULONG pid = 0;
if (!GetNamedPipeClientProcessId(pipeHandle, &pid))
{
return HRESULT_FROM_WIN32(GetLastError());
}
outIdentity.processId = pid;
// 2) Impersonate the client. We need the caller's token to (a) read
// its SID and (b) open a handle to its own process. The service
// runs as NT SERVICE\<vacct>, which is NOT a member of
// Authenticated Users and so cannot satisfy the default process
// DACL when calling OpenProcess across user boundaries. Doing
// the OpenProcess while impersonating means the DACL check is
// against the user's own token, which naturally grants access
// to its own processes.
if (!ImpersonateNamedPipeClient(pipeHandle))
{
return HRESULT_FROM_WIN32(GetLastError());
}
HANDLE clientToken = nullptr;
BOOL gotToken = OpenThreadToken(GetCurrentThread(),
TOKEN_QUERY,
TRUE,
&clientToken);
DWORD tokenErr = gotToken ? ERROR_SUCCESS : GetLastError();
if (!gotToken)
{
RevertToSelf();
return HRESULT_FROM_WIN32(tokenErr);
}
HRESULT hr = RejectionForToken(clientToken, outIdentity.userSidString);
CloseHandle(clientToken);
if (FAILED(hr))
{
RevertToSelf();
return hr;
}
// 3) While still impersonating: open the client process and read its
// image path. Hold the handle for the rest of validation so the
// PID can't be reused under us.
HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
DWORD openErr = hProc ? ERROR_SUCCESS : GetLastError();
wchar_t exePath[MAX_PATH * 2] = {};
DWORD cch = ARRAYSIZE(exePath);
BOOL gotImage = FALSE;
DWORD imageErr = ERROR_SUCCESS;
if (hProc)
{
gotImage = QueryFullProcessImageNameW(hProc, 0, exePath, &cch);
imageErr = gotImage ? ERROR_SUCCESS : GetLastError();
}
// The caller binary often lives under %LocalAppData% (per-user install),
// which is ACL'd to the user only. The service account cannot read it,
// so canonicalization and the signature/version checks MUST run while we
// are still impersonating the client (which can read its own image).
std::wstring canonical;
bool sigMicrosoft = false;
unsigned long long callerVersion = 0;
if (gotImage)
{
canonical = CanonicalizePath(exePath);
sigMicrosoft = VerifyMicrosoftSignature(canonical);
callerVersion = GetBinaryVersion(canonical);
}
// Revert before we touch any service-side resources (file IO etc).
RevertToSelf();
if (!hProc)
{
return HRESULT_FROM_WIN32(openErr);
}
CloseHandle(hProc);
if (!gotImage)
{
return HRESULT_FROM_WIN32(imageErr);
}
outIdentity.imagePath = canonical;
// 4) Caller-image trust anchor (UNIFIED — Design §7/§12.7, updated 2026-06-30).
// EVERY caller, per-machine and per-user alike, must be Microsoft-
// signed AND its version must satisfy the floor + max-delta policy
// against the service's own version (IsCallerVersionAcceptable).
//
// Why a version POLICY, not exact equality:
// * The machine-wide service is a singleton (one version). Exact
// `caller == service` broke multi-user / multi-version: the
// latest install would reject every other-version caller (§12.7).
// * The real goal is anti-DOWNGRADE — block old vulnerable signed
// binaries — which a minimum-version FLOOR achieves, while a
// bounded max-delta keeps callers reasonably current.
// * The signature is verified by this LocalSystem service against
// the MACHINE trust store (CallerVerify.cpp), so it is NOT
// forgeable by a non-admin user-store root (defeats the §13
// per-user TrustedPeople objection that argued path > signature).
// * Binary immutability is already guaranteed by deployment
// (WindowsApps for the service, %ProgramFiles% for per-machine
// callers), so it need not be re-proven during authentication.
//
// sigMicrosoft and callerVersion were captured above under
// impersonation so a user-profile image is readable.
const unsigned long long serviceVersion = GetServiceOwnVersion();
bool sigOk = sigMicrosoft;
#ifdef _DEBUG
// DEV-ONLY, conditional compilation: this block exists ONLY in Debug
// builds and is physically absent from Release, so there is no bypass to
// abuse in shipped binaries. Local/smoke-test builds are not
// Microsoft-signed, so a Debug build accepts an unsigned caller — but
// the version policy below STILL applies, so the anchor's logic is
// exercised. Production is always Release + ESRP-signed, where a real
// Microsoft signature is mandatory.
sigOk = true;
#endif
const bool accepted =
sigOk &&
IsCallerVersionAcceptable(callerVersion, serviceVersion);
if (!accepted)
{
return E_ACCESSDENIED;
}
// 5) Caller binding lookup (basename allow-list + namespace selection).
std::wstring basename = BaseName(canonical);
const CallerBinding* binding = FindBindingByExeBasename(basename);
if (!binding)
{
return E_ACCESSDENIED;
}
// Defensive: the table should always carry a well-formed namespace id;
// verify before we hand it to the storage layer to use as a directory
// name. Failure here is a build-time misconfiguration of Bindings.cpp.
if (!IsValidNamespaceId(binding->namespaceId))
{
return HRESULT_FROM_WIN32(ERROR_NOT_FOUND);
}
outIdentity.binding = binding;
return S_OK;
}
}

View File

@@ -1,50 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#pragma once
#include <windows.h>
#include <string>
namespace PTSettingsSvc
{
struct CallerBinding; // Bindings.h
struct CallerIdentity
{
std::wstring userSidString; // S-1-5-21-... (per-user data partition key)
std::wstring imagePath; // Canonicalised, reparse-points resolved
DWORD processId{};
const CallerBinding* binding = nullptr; // never freed (static table)
};
// Authenticates the client connected to the named-pipe handle.
//
// Successful authentication means ALL of the following hold:
// * Caller token is a real interactive user (not SYSTEM / SERVICE /
// ANONYMOUS), so we have a SID to scope the per-user data folder.
// * Caller image is trusted by EITHER anchor (Design-v6-Final.md §7):
// - PATH anchor: image resolves under %ProgramFiles%\PowerToys and
// that folder's DACL is admin-only writable (per-machine), OR
// - BINARY-IDENTITY anchor: image is Microsoft-signed AND its version
// equals the service's own version (per-user, user-writable folder).
// * Caller image basename is in the CallerBinding allow-list — and the
// matched binding is returned in outIdentity.binding so the dispatch
// layer knows which namespace this caller may operate on.
//
// The path anchor is preferred where available (smaller privileged surface,
// immutability); the signature+version anchor is the fallback used only
// when the path cannot be trusted. See §7 and §15 #5.
//
// The function ImpersonateNamedPipeClient()s internally and reverts
// before returning, regardless of success.
//
// Returns:
// S_OK — all checks passed
// E_ACCESSDENIED — auth-rejected (path, DACL, or basename)
// HRESULT_FROM_WIN32(ERROR_NOT_FOUND) — basename allow-listed but
// binding lookup returned nullptr
// any other HRESULT — Win32 failure (token read,
// OpenProcess, etc.)
HRESULT AuthenticateCaller(HANDLE pipeHandle, CallerIdentity& outIdentity);
}

View File

@@ -1,212 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#include "CallerVerify.h"
#include <windows.h>
#include <wintrust.h>
#include <softpub.h>
#include <wincrypt.h>
#include <vector>
#pragma comment(lib, "wintrust.lib")
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "version.lib")
namespace PTSettingsSvc
{
namespace
{
// WinVerifyTrust with no UI; confirms the embedded signature is valid
// and chains to a trusted root. Runs in the service's own security
// context, so it consults the machine trust stores, not the caller's.
bool EmbeddedSignatureChainsToTrustedRoot(const std::wstring& path)
{
WINTRUST_FILE_INFO fileInfo = {};
fileInfo.cbStruct = sizeof(fileInfo);
fileInfo.pcwszFilePath = path.c_str();
GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
WINTRUST_DATA wd = {};
wd.cbStruct = sizeof(wd);
wd.dwUIChoice = WTD_UI_NONE;
// Prototype: skip network revocation on the hot path. Production
// should use WTD_REVOKE_WHOLECHAIN with a cached/offline policy.
wd.fdwRevocationChecks = WTD_REVOKE_NONE;
wd.dwUnionChoice = WTD_CHOICE_FILE;
wd.pFile = &fileInfo;
wd.dwStateAction = WTD_STATEACTION_VERIFY;
wd.dwProvFlags = WTD_SAFER_FLAG;
HWND noWindow = static_cast<HWND>(INVALID_HANDLE_VALUE);
LONG status = WinVerifyTrust(noWindow, &action, &wd);
wd.dwStateAction = WTD_STATEACTION_CLOSE;
WinVerifyTrust(noWindow, &action, &wd);
return status == ERROR_SUCCESS;
}
// Extracts the signer leaf certificate's simple display name and checks
// it is "Microsoft Corporation". Production should pin the exact cert
// (public key / thumbprint) rather than the subject string.
bool SignerSubjectIsMicrosoft(const std::wstring& path)
{
HCERTSTORE store = nullptr;
HCRYPTMSG msg = nullptr;
DWORD encoding = 0;
DWORD contentType = 0;
DWORD formatType = 0;
if (!CryptQueryObject(CERT_QUERY_OBJECT_FILE,
path.c_str(),
CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
CERT_QUERY_FORMAT_FLAG_BINARY,
0,
&encoding,
&contentType,
&formatType,
&store,
&msg,
nullptr))
{
return false;
}
bool isMicrosoft = false;
DWORD signerInfoSize = 0;
if (CryptMsgGetParam(msg, CMSG_SIGNER_INFO_PARAM, 0, nullptr, &signerInfoSize) &&
signerInfoSize > 0)
{
std::vector<BYTE> signerInfoBuf(signerInfoSize);
if (CryptMsgGetParam(msg, CMSG_SIGNER_INFO_PARAM, 0, signerInfoBuf.data(), &signerInfoSize))
{
auto signerInfo = reinterpret_cast<CMSG_SIGNER_INFO*>(signerInfoBuf.data());
CERT_INFO certInfo = {};
certInfo.Issuer = signerInfo->Issuer;
certInfo.SerialNumber = signerInfo->SerialNumber;
PCCERT_CONTEXT cert = CertFindCertificateInStore(
store,
X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
0,
CERT_FIND_SUBJECT_CERT,
&certInfo,
nullptr);
if (cert)
{
wchar_t name[256] = {};
DWORD n = CertGetNameStringW(cert,
CERT_NAME_SIMPLE_DISPLAY_TYPE,
0,
nullptr,
name,
ARRAYSIZE(name));
if (n > 1)
{
isMicrosoft = (wcsstr(name, L"Microsoft Corporation") != nullptr);
}
CertFreeCertificateContext(cert);
}
}
}
if (msg)
{
CryptMsgClose(msg);
}
if (store)
{
CertCloseStore(store, 0);
}
return isMicrosoft;
}
}
bool VerifyMicrosoftSignature(const std::wstring& path)
{
if (path.empty())
{
return false;
}
return EmbeddedSignatureChainsToTrustedRoot(path) && SignerSubjectIsMicrosoft(path);
}
unsigned long long GetBinaryVersion(const std::wstring& path)
{
if (path.empty())
{
return 0;
}
DWORD ignored = 0;
DWORD size = GetFileVersionInfoSizeW(path.c_str(), &ignored);
if (size == 0)
{
return 0;
}
std::vector<BYTE> buf(size);
if (!GetFileVersionInfoW(path.c_str(), 0, size, buf.data()))
{
return 0;
}
VS_FIXEDFILEINFO* ffi = nullptr;
UINT ffiLen = 0;
if (!VerQueryValueW(buf.data(), L"\\", reinterpret_cast<LPVOID*>(&ffi), &ffiLen) ||
ffi == nullptr || ffiLen == 0)
{
return 0;
}
return (static_cast<unsigned long long>(ffi->dwFileVersionMS) << 32) |
static_cast<unsigned long long>(ffi->dwFileVersionLS);
}
unsigned long long GetServiceOwnVersion()
{
wchar_t self[MAX_PATH * 2] = {};
DWORD n = GetModuleFileNameW(nullptr, self, ARRAYSIZE(self));
if (n == 0 || n >= ARRAYSIZE(self))
{
return 0;
}
return GetBinaryVersion(self);
}
bool IsCallerVersionAcceptable(unsigned long long callerVersion,
unsigned long long serviceVersion)
{
if (callerVersion == 0 || serviceVersion == 0)
{
return false;
}
// 1) Absolute floor — the anti-downgrade boundary.
if (callerVersion < kMinSupportedCallerVersion)
{
return false;
}
// 2) Bounded staleness on the MINOR-release field (bits 32..47). Compare
// the absolute distance so a caller may trail OR (transiently, mid-
// upgrade) lead the service by at most kMaxMinorVersionDelta releases.
const unsigned long long callerMinor = (callerVersion >> 32) & 0xFFFFull;
const unsigned long long serviceMinor = (serviceVersion >> 32) & 0xFFFFull;
const unsigned long long minorDelta =
(serviceMinor > callerMinor) ? (serviceMinor - callerMinor)
: (callerMinor - serviceMinor);
if (minorDelta > kMaxMinorVersionDelta)
{
return false;
}
return true;
}
}

View File

@@ -1,78 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#pragma once
#include <string>
namespace PTSettingsSvc
{
// Binary-identity anchor used when the install-path anchor cannot be
// trusted (per-user installs in a user-writable folder — Design-v6-Final.md
// §7 fallback branch / §15 #5 option d).
//
// Accepting a caller on this branch requires BOTH:
// * VerifyMicrosoftSignature(exe) — the on-disk image carries a valid
// Authenticode signature that chains to a trusted machine root AND is
// signed by "Microsoft Corporation". The check runs in the service's
// own context, so a user poisoning their HKCU cert stores cannot affect
// it (contrast the §13 package-identity attack).
// * GetBinaryVersion(exe) == GetServiceOwnVersion() — the caller is the
// same release as the service. Because the signature protects the
// version resource, a re-stamped version breaks the signature, and an
// old (downgrade) signed binary has an older version. Version
// comparison ALONE is insecure — VERSIONINFO is attacker-writable
// metadata — which is why it must be paired with the signature.
// True iff the file at `path` has a valid embedded Authenticode signature
// (chains to a trusted root) AND the signer leaf subject is Microsoft.
bool VerifyMicrosoftSignature(const std::wstring& path);
// 64-bit file version (dwFileVersionMS<<32 | dwFileVersionLS) from the
// VS_FIXEDFILEINFO of `path`. 0 if the file has no version resource.
unsigned long long GetBinaryVersion(const std::wstring& path);
// Version of the running service executable (this module). 0 if the
// service binary carries no version resource (production builds must).
unsigned long long GetServiceOwnVersion();
// Packs a (major, minor, build, revision) tuple into the same 64-bit layout
// GetBinaryVersion returns: major<<48 | minor<<32 | build<<16 | revision.
constexpr unsigned long long MakeVersion(unsigned short major,
unsigned short minor,
unsigned short build,
unsigned short revision)
{
return (static_cast<unsigned long long>(major) << 48) |
(static_cast<unsigned long long>(minor) << 32) |
(static_cast<unsigned long long>(build) << 16) |
static_cast<unsigned long long>(revision);
}
// --- Version-acceptance policy (Design §12.7, decided 2026-06-30) ----------
// Replaces the exact `caller == service` rule, which broke multi-user /
// multi-version (a machine-wide singleton service can be only one version,
// so the latest install would reject every other-version caller). A caller
// is version-acceptable iff BOTH bounds hold:
// 1. ABSOLUTE FLOOR: callerVersion >= kMinSupportedCallerVersion. This is
// the real anti-downgrade control — set it to exclude any version known
// to be vulnerable. Bump it when a bad old version must be cut off.
// 2. BOUNDED STALENESS (max delta): the caller's MINOR-release number is
// within kMaxMinorVersionDelta of the service's, so a caller can be at
// most N monthly releases away from the running service.
// The signature check (VerifyMicrosoftSignature) is still required and is
// what makes the version fields trustworthy.
// Oldest caller MINOR release still accepted. PowerToys versions are
// 0.<minor>.<build>; the minor is the monthly release train. Set to the
// first v6 shipping minor at release; placeholder baseline below.
constexpr unsigned long long kMinSupportedCallerVersion = MakeVersion(0, 100, 0, 0);
// Max number of MINOR releases a caller may trail (or lead) the service.
constexpr unsigned int kMaxMinorVersionDelta = 3;
// True iff `callerVersion` satisfies the floor + max-delta policy against the
// running `serviceVersion`. Both are packed (GetBinaryVersion layout).
bool IsCallerVersionAcceptable(unsigned long long callerVersion,
unsigned long long serviceVersion);
}

View File

@@ -1,323 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#include "FileGuard.h"
#include <windows.h>
#include <sddl.h>
#include <aclapi.h>
#include <pathcch.h>
#include <memory>
#include <vector>
#pragma comment(lib, "Advapi32.lib")
#pragma comment(lib, "Pathcch.lib")
namespace PTSettingsSvc
{
namespace
{
struct LocalFreeDeleter
{
void operator()(void* p) const noexcept { if (p) LocalFree(p); }
};
HRESULT GetServiceSid(PSID& outSid)
{
// The service runs as LocalSystem (S-1-5-18) under MSIX, whose
// windows.service extension only allows LocalSystem/LocalService/
// NetworkService start accounts (no virtual NT SERVICE\<name>
// account — Design §12.1). Grant the writer ACE to SYSTEM.
BYTE buf[SECURITY_MAX_SID_SIZE];
DWORD cb = sizeof(buf);
if (!CreateWellKnownSid(WinLocalSystemSid, nullptr, buf, &cb))
{
return HRESULT_FROM_WIN32(GetLastError());
}
outSid = static_cast<PSID>(LocalAlloc(LMEM_FIXED, cb));
if (!outSid)
{
return E_OUTOFMEMORY;
}
CopySid(cb, outSid, buf);
return S_OK;
}
HRESULT ApplyProtectiveDacl(const std::wstring& target,
const std::wstring& userSidString)
{
PSID serviceSid = nullptr;
HRESULT hr = GetServiceSid(serviceSid);
if (FAILED(hr))
{
return hr;
}
std::unique_ptr<void, LocalFreeDeleter> serviceSidGuard(serviceSid);
PSID userSid = nullptr;
if (!ConvertStringSidToSidW(userSidString.c_str(), &userSid))
{
return HRESULT_FROM_WIN32(GetLastError());
}
std::unique_ptr<void, LocalFreeDeleter> userSidGuard(userSid);
PSID adminSid = nullptr;
if (!ConvertStringSidToSidW(L"S-1-5-32-544", &adminSid)) // BUILTIN\Administrators
{
return HRESULT_FROM_WIN32(GetLastError());
}
std::unique_ptr<void, LocalFreeDeleter> adminSidGuard(adminSid);
// Per Design-v6-Final.md §9 the per-user folder DACL is:
// svc:F, admin:F, <specific user>:RX
// Everyone else implicitly denied because we PROTECT the DACL
// below (no inheritance from <storeRoot>\, so the blanket
// AuthUsers:RX granted at the store root does NOT carry through
// here — that's how user A can't read user B's data). Applied at
// the per-user <sid> node, it inherits down to the namespace folder
// and the file.
EXPLICIT_ACCESS_W ea[3] = {};
ea[0].grfAccessPermissions = GENERIC_ALL;
ea[0].grfAccessMode = SET_ACCESS;
ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[0].Trustee.TrusteeType = TRUSTEE_IS_USER;
ea[0].Trustee.ptstrName = static_cast<LPWSTR>(serviceSid);
ea[1].grfAccessPermissions = GENERIC_ALL;
ea[1].grfAccessMode = SET_ACCESS;
ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[1].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ea[1].Trustee.ptstrName = static_cast<LPWSTR>(adminSid);
ea[2].grfAccessPermissions = GENERIC_READ | GENERIC_EXECUTE;
ea[2].grfAccessMode = SET_ACCESS;
ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[2].Trustee.TrusteeType = TRUSTEE_IS_USER;
ea[2].Trustee.ptstrName = static_cast<LPWSTR>(userSid);
PACL acl = nullptr;
DWORD rc = SetEntriesInAclW(ARRAYSIZE(ea), ea, nullptr, &acl);
if (rc != ERROR_SUCCESS)
{
return HRESULT_FROM_WIN32(rc);
}
std::unique_ptr<void, LocalFreeDeleter> aclGuard(acl);
// PROTECTED_DACL_SECURITY_INFORMATION blocks inheritance from
// <root>\<namespace>\. SetNamedSecurityInfoW takes a non-const
// LPWSTR by historical signature; copy into a local mutable buffer.
std::vector<wchar_t> mutableName(target.begin(), target.end());
mutableName.push_back(L'\0');
rc = SetNamedSecurityInfoW(mutableName.data(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
nullptr, nullptr, acl, nullptr);
return rc == ERROR_SUCCESS ? S_OK : HRESULT_FROM_WIN32(rc);
}
}
HRESULT EnsureStoreRoot(const std::wstring& root)
{
if (!CreateDirectoryW(root.c_str(), nullptr))
{
DWORD err = GetLastError();
if (err != ERROR_ALREADY_EXISTS)
{
return HRESULT_FROM_WIN32(err);
}
}
PSID adminSid = nullptr;
if (!ConvertStringSidToSidW(L"S-1-5-32-544", &adminSid))
{
return HRESULT_FROM_WIN32(GetLastError());
}
std::unique_ptr<void, LocalFreeDeleter> adminGuard(adminSid);
PSID systemSid = nullptr;
if (!ConvertStringSidToSidW(L"S-1-5-18", &systemSid))
{
return HRESULT_FROM_WIN32(GetLastError());
}
std::unique_ptr<void, LocalFreeDeleter> systemGuard(systemSid);
PSID authUsersSid = nullptr;
if (!ConvertStringSidToSidW(L"S-1-5-11", &authUsersSid)) // Authenticated Users
{
return HRESULT_FROM_WIN32(GetLastError());
}
std::unique_ptr<void, LocalFreeDeleter> authUsersGuard(authUsersSid);
// Root: SYSTEM/Admins Full, Authenticated Users RX (traverse only). Not
// protected — each <sid> node below protects itself; the blanket RX here
// lets every user reach their own node but the protected child DACL
// stops A reading B.
EXPLICIT_ACCESS_W ea[3] = {};
ea[0].grfAccessPermissions = GENERIC_ALL;
ea[0].grfAccessMode = SET_ACCESS;
ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[0].Trustee.TrusteeType = TRUSTEE_IS_USER;
ea[0].Trustee.ptstrName = static_cast<LPWSTR>(systemSid);
ea[1].grfAccessPermissions = GENERIC_ALL;
ea[1].grfAccessMode = SET_ACCESS;
ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[1].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ea[1].Trustee.ptstrName = static_cast<LPWSTR>(adminSid);
ea[2].grfAccessPermissions = GENERIC_READ | GENERIC_EXECUTE;
ea[2].grfAccessMode = SET_ACCESS;
ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[2].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ea[2].Trustee.ptstrName = static_cast<LPWSTR>(authUsersSid);
PACL acl = nullptr;
DWORD rc = SetEntriesInAclW(ARRAYSIZE(ea), ea, nullptr, &acl);
if (rc != ERROR_SUCCESS)
{
return HRESULT_FROM_WIN32(rc);
}
std::unique_ptr<void, LocalFreeDeleter> aclGuard(acl);
std::vector<wchar_t> mutableName(root.begin(), root.end());
mutableName.push_back(L'\0');
rc = SetNamedSecurityInfoW(mutableName.data(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
nullptr, nullptr, acl, nullptr);
return rc == ERROR_SUCCESS ? S_OK : HRESULT_FROM_WIN32(rc);
}
HRESULT EnsureUserFolder(const std::wstring& folder,
const std::wstring& userSidString)
{
if (!CreateDirectoryW(folder.c_str(), nullptr))
{
DWORD err = GetLastError();
if (err != ERROR_ALREADY_EXISTS)
{
return HRESULT_FROM_WIN32(err);
}
}
return ApplyProtectiveDacl(folder, userSidString);
}
HRESULT WriteFileAtomically(const std::wstring& targetFile,
const std::vector<BYTE>& bytes)
{
std::wstring tmp = targetFile + L".tmp";
HANDLE h = CreateFileW(tmp.c_str(),
GENERIC_WRITE,
0,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (h == INVALID_HANDLE_VALUE)
{
return HRESULT_FROM_WIN32(GetLastError());
}
DWORD written = 0;
BOOL ok = WriteFile(h,
bytes.data(),
static_cast<DWORD>(bytes.size()),
&written,
nullptr);
DWORD writeErr = ok ? ERROR_SUCCESS : GetLastError();
FlushFileBuffers(h);
CloseHandle(h);
if (!ok || written != bytes.size())
{
DeleteFileW(tmp.c_str());
return HRESULT_FROM_WIN32(writeErr ? writeErr : ERROR_WRITE_FAULT);
}
if (!ReplaceFileW(targetFile.c_str(),
tmp.c_str(),
nullptr,
REPLACEFILE_WRITE_THROUGH | REPLACEFILE_IGNORE_MERGE_ERRORS,
nullptr,
nullptr))
{
DWORD err = GetLastError();
if (err == ERROR_FILE_NOT_FOUND)
{
// No existing file — MoveFile is sufficient.
if (!MoveFileExW(tmp.c_str(),
targetFile.c_str(),
MOVEFILE_WRITE_THROUGH))
{
DWORD mvErr = GetLastError();
DeleteFileW(tmp.c_str());
return HRESULT_FROM_WIN32(mvErr);
}
}
else
{
DeleteFileW(tmp.c_str());
return HRESULT_FROM_WIN32(err);
}
}
return S_OK;
}
HRESULT ReadFileFully(const std::wstring& path,
uint32_t maxBytes,
std::vector<BYTE>& outBytes)
{
outBytes.clear();
HANDLE h = CreateFileW(path.c_str(),
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr);
if (h == INVALID_HANDLE_VALUE)
{
return HRESULT_FROM_WIN32(GetLastError());
}
LARGE_INTEGER size{};
if (!GetFileSizeEx(h, &size))
{
DWORD err = GetLastError();
CloseHandle(h);
return HRESULT_FROM_WIN32(err);
}
if (size.QuadPart > static_cast<LONGLONG>(maxBytes))
{
CloseHandle(h);
return HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE);
}
outBytes.resize(static_cast<size_t>(size.QuadPart));
DWORD read = 0;
BOOL ok = ReadFile(h,
outBytes.data(),
static_cast<DWORD>(outBytes.size()),
&read,
nullptr);
DWORD err = ok ? ERROR_SUCCESS : GetLastError();
CloseHandle(h);
if (!ok || read != outBytes.size())
{
outBytes.clear();
return HRESULT_FROM_WIN32(err ? err : ERROR_READ_FAULT);
}
return S_OK;
}
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#pragma once
#include <windows.h>
#include <string>
#include <vector>
namespace PTSettingsSvc
{
// Creates the store root (<ProgramData>\Microsoft\PowerToys\Settings) if it
// doesn't exist and applies the root DACL: SYSTEM/Admins Full, Authenticated
// Users RX (traverse so each user reaches their own <sid> node). Idempotent;
// the per-user MSIX install has no installer step so the LocalSystem service
// creates the root lazily on first PutBlob (Design §12.1).
HRESULT EnsureStoreRoot(const std::wstring& root);
// Creates `folder` if it doesn't exist and applies the DACL that locks
// the directory to:
// * the service account — Full Control
// * BUILTIN\Administrators — Read & Execute (audit/backup)
// * the user whose SID is passed in — Read & Execute (Launcher needs to read)
// * Everyone else — denied (DACL is protected, no inherit)
HRESULT EnsureUserFolder(const std::wstring& folder,
const std::wstring& userSidString);
// Atomically replaces `targetFile` with `bytes`. Internally writes to
// a sibling .tmp and uses ReplaceFileW so a crash during write never
// leaves the file in a half-written state. Re-asserts the directory's
// protective DACL after the write in case something has tampered with it.
HRESULT WriteFileAtomically(const std::wstring& targetFile,
const std::vector<BYTE>& bytes);
// Reads an entire file into memory. Caps at maxBytes; returns
// HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE) if exceeded.
HRESULT ReadFileFully(const std::wstring& path,
uint32_t maxBytes,
std::vector<BYTE>& outBytes);
}

View File

@@ -1,230 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#include "Paths.h"
#include <windows.h>
#include <sddl.h>
#include <shlobj.h>
#include <pathcch.h>
#include <aclapi.h>
#include <memory>
#pragma comment(lib, "Shell32.lib")
#pragma comment(lib, "Pathcch.lib")
#pragma comment(lib, "Advapi32.lib")
namespace PTSettingsSvc
{
namespace
{
std::wstring GetProgramDataFolder()
{
PWSTR path = nullptr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_ProgramData, 0, nullptr, &path)))
{
std::wstring result(path);
CoTaskMemFree(path);
return result;
}
return L"C:\\ProgramData";
}
}
std::wstring GetSettingsRoot()
{
return GetProgramDataFolder() + L"\\Microsoft\\PowerToys\\Settings";
}
std::wstring GetUserFolder(const std::wstring& userSidString)
{
return GetSettingsRoot() + L"\\" + userSidString;
}
std::wstring GetUserNamespaceFolder(const std::wstring& userSidString,
const std::wstring& namespaceId)
{
return GetUserFolder(userSidString) + L"\\" + namespaceId;
}
std::wstring GetUserFilePath(const std::wstring& userSidString,
const std::wstring& namespaceId,
const std::wstring& fileName)
{
return GetUserNamespaceFolder(userSidString, namespaceId) + L"\\" + fileName;
}
std::wstring GetPowerToysInstallFolder()
{
// The MSI writes InstallFolder under HKLM\SOFTWARE\Classes\PowerToys
// for per-machine installs. This is the authoritative location the
// service uses to validate the caller image path.
HKEY hKey = nullptr;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Classes\\PowerToys",
0,
KEY_READ | KEY_WOW64_64KEY,
&hKey) != ERROR_SUCCESS)
{
return {};
}
wchar_t buf[MAX_PATH] = {};
DWORD cb = sizeof(buf);
DWORD type = 0;
LSTATUS rc = RegQueryValueExW(hKey,
L"InstallFolder",
nullptr,
&type,
reinterpret_cast<LPBYTE>(buf),
&cb);
RegCloseKey(hKey);
if (rc != ERROR_SUCCESS || type != REG_SZ)
{
return {};
}
std::wstring result(buf);
// Strip trailing backslash.
while (!result.empty() && result.back() == L'\\')
{
result.pop_back();
}
return result;
}
std::wstring SidToString(void* psid)
{
LPWSTR str = nullptr;
if (!ConvertSidToStringSidW(static_cast<PSID>(psid), &str))
{
return {};
}
std::wstring result(str);
LocalFree(str);
return result;
}
namespace
{
bool IsAdminClassPrincipal(PSID sid)
{
// Build a small set of well-known principals that are allowed to
// write to an install folder we still consider hardened.
const WELL_KNOWN_SID_TYPE wellKnown[] = {
WinLocalSystemSid,
WinBuiltinAdministratorsSid,
};
for (auto wk : wellKnown)
{
BYTE buf[SECURITY_MAX_SID_SIZE];
DWORD cb = sizeof(buf);
if (CreateWellKnownSid(wk, nullptr, buf, &cb) &&
EqualSid(sid, reinterpret_cast<PSID>(buf)))
{
return true;
}
}
// NT SERVICE\TrustedInstaller — no WELL_KNOWN_SID_TYPE constant,
// but the SID is stable.
PSID tiSid = nullptr;
if (ConvertStringSidToSidW(
L"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464",
&tiSid))
{
bool match = EqualSid(sid, tiSid) != 0;
LocalFree(tiSid);
if (match) return true;
}
return false;
}
}
bool IsFolderAdminOnlyWritable(const std::wstring& folder)
{
if (folder.empty())
{
return false;
}
// Rights that let an attacker influence what's inside the folder
// (drop a fake exe, swap an existing one, change the DACL itself).
constexpr DWORD kDangerousRights =
FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY |
FILE_WRITE_DATA | FILE_APPEND_DATA |
FILE_DELETE_CHILD | DELETE |
WRITE_DAC | WRITE_OWNER |
GENERIC_WRITE | GENERIC_ALL;
PACL dacl = nullptr;
PSECURITY_DESCRIPTOR sd = nullptr;
DWORD rc = GetNamedSecurityInfoW(
folder.c_str(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION,
nullptr,
nullptr,
&dacl,
nullptr,
&sd);
if (rc != ERROR_SUCCESS)
{
return false;
}
// NULL DACL means "allow everyone everything" — definitely not safe.
if (!dacl)
{
if (sd) LocalFree(sd);
return false;
}
bool safe = true;
for (WORD i = 0; safe && i < dacl->AceCount; ++i)
{
PACE_HEADER hdr = nullptr;
if (!GetAce(dacl, i, reinterpret_cast<LPVOID*>(&hdr)))
{
continue;
}
// Only positive ACEs matter. ACCESS_DENIED only narrows
// permissions further.
if (hdr->AceType != ACCESS_ALLOWED_ACE_TYPE &&
hdr->AceType != ACCESS_ALLOWED_OBJECT_ACE_TYPE)
{
continue;
}
ACCESS_ALLOWED_ACE* ace = reinterpret_cast<ACCESS_ALLOWED_ACE*>(hdr);
if ((ace->Mask & kDangerousRights) == 0)
{
continue;
}
PSID sid = reinterpret_cast<PSID>(&ace->SidStart);
// CREATOR OWNER / CREATOR GROUP only apply when something is
// created; they don't grant the current trustee anything by
// themselves, so they're benign here.
BYTE creatorOwner[SECURITY_MAX_SID_SIZE];
DWORD cb = sizeof(creatorOwner);
if (CreateWellKnownSid(WinCreatorOwnerSid, nullptr, creatorOwner, &cb) &&
EqualSid(sid, reinterpret_cast<PSID>(creatorOwner)))
{
continue;
}
if (!IsAdminClassPrincipal(sid))
{
safe = false;
}
}
LocalFree(sd);
return safe;
}
}

View File

@@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#pragma once
#include <string>
namespace PTSettingsSvc
{
// %ProgramData%\Microsoft\PowerToys\Settings
std::wstring GetSettingsRoot();
// %ProgramData%\Microsoft\PowerToys\Settings\<sid>
// Per-user node: this is where the protected, user-isolating DACL is
// applied; everything below inherits it.
std::wstring GetUserFolder(const std::wstring& userSidString);
// %ProgramData%\Microsoft\PowerToys\Settings\<sid>\<namespaceId>
std::wstring GetUserNamespaceFolder(const std::wstring& userSidString,
const std::wstring& namespaceId);
// %ProgramData%\Microsoft\PowerToys\Settings\<sid>\<namespaceId>\<fileName>
std::wstring GetUserFilePath(const std::wstring& userSidString,
const std::wstring& namespaceId,
const std::wstring& fileName);
// Path to the PowerToys install folder (from HKLM\SOFTWARE\Classes\PowerToys
// or the registry key the bootstrapper writes). Empty string on failure.
std::wstring GetPowerToysInstallFolder();
// Returns true iff `folder` exists AND its DACL grants write/create/delete
// only to admin-class principals (BUILTIN\Administrators,
// NT AUTHORITY\SYSTEM, NT SERVICE\TrustedInstaller). Used by the auth
// pipeline to reject install paths that landed in a user-writable
// location (custom MSI directory under a Users-writable parent, per-user
// MSI under %LocalAppData%, etc.) — in those cases same-user malware
// could plant a fake allow-listed exe there and pass the path+name check.
bool IsFolderAdminOnlyWritable(const std::wstring& folder);
// Convert a binary SID to its string form (S-1-5-21-...). Empty on failure.
std::wstring SidToString(void* psid);
}

View File

@@ -1,294 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#include "PipeServer.h"
#include "Bindings.h"
#include "CallerAuth.h"
#include "FileGuard.h"
#include "Paths.h"
#include "protocol/Protocol.h"
#include <windows.h>
#include <sddl.h>
#include <aclapi.h>
#include <vector>
#include <string>
#include <cstring>
#pragma comment(lib, "Advapi32.lib")
namespace PTSettingsSvc
{
namespace
{
// Pipe SD: Authenticated Users may connect; SYSTEM and BUILTIN\Administrators
// get full control for diagnostics; everyone else is implicitly denied
// because the DACL doesn't grant them anything. The protocol layer
// does the real access control (caller image + allow-list).
constexpr const wchar_t* kPipeSddl =
L"D:"
L"(A;;GRGW;;;AU)" // Authenticated Users : connect/read/write
L"(A;;GA;;;SY)" // SYSTEM : full
L"(A;;GA;;;BA)"; // BUILTIN\Administrators : full
bool ReadExact(HANDLE pipe, void* buf, DWORD len)
{
BYTE* p = static_cast<BYTE*>(buf);
DWORD remaining = len;
while (remaining > 0)
{
DWORD got = 0;
if (!ReadFile(pipe, p, remaining, &got, nullptr) || got == 0)
{
return false;
}
p += got;
remaining -= got;
}
return true;
}
bool WriteExact(HANDLE pipe, const void* buf, DWORD len)
{
const BYTE* p = static_cast<const BYTE*>(buf);
DWORD remaining = len;
while (remaining > 0)
{
DWORD wrote = 0;
if (!WriteFile(pipe, p, remaining, &wrote, nullptr) || wrote == 0)
{
return false;
}
p += wrote;
remaining -= wrote;
}
return true;
}
void SendResponse(HANDLE pipe, Status status,
const std::vector<BYTE>& payload = {})
{
uint8_t st = static_cast<uint8_t>(status);
uint32_t len = static_cast<uint32_t>(payload.size());
WriteExact(pipe, &st, sizeof(st));
WriteExact(pipe, &len, sizeof(len));
if (len > 0)
{
WriteExact(pipe, payload.data(), len);
}
}
void SendStatus(HANDLE pipe, Status status)
{
SendResponse(pipe, status);
}
void HandleGetBlob(HANDLE pipe, const CallerIdentity& id)
{
std::wstring target = GetUserFilePath(id.userSidString,
id.binding->namespaceId,
id.binding->fileName);
std::vector<BYTE> bytes;
HRESULT hr = ReadFileFully(target, kMaxPayloadBytes, bytes);
if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ||
hr == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND))
{
// Brand new user / namespace — explicit NotFound so the
// caller can distinguish "blob is empty" from "blob doesn't
// exist yet" (matters for migration).
SendStatus(pipe, Status::NotFound);
return;
}
if (FAILED(hr))
{
SendStatus(pipe, hr == HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE)
? Status::PayloadTooLarge
: Status::IoError);
return;
}
SendResponse(pipe, Status::Ok, bytes);
}
void HandlePutBlob(HANDLE pipe, const CallerIdentity& id,
const std::vector<BYTE>& payload)
{
// No structural / schema check on the payload. The service is
// payload-agnostic; the caller is responsible for whatever
// shape it wants on disk. See Design-v6-Final.md §4.
// Ensure the store root exists with the traverse DACL (no installer
// creates it in the per-user MSIX case; LocalSystem does it lazily).
HRESULT hr = EnsureStoreRoot(GetSettingsRoot());
if (FAILED(hr))
{
SendStatus(pipe, Status::IoError);
return;
}
// Ensure the per-user node <storeRoot>\<sid> exists and carries the
// PROTECTED, user-isolating DACL (svc:F, admin:F, this-user:RX).
// It is applied once here and inherited by the namespace folder and
// the file below — that single tightening is what stops user A from
// reading user B's data (Design §9).
hr = EnsureUserFolder(GetUserFolder(id.userSidString),
id.userSidString);
if (FAILED(hr))
{
SendStatus(pipe, Status::IoError);
return;
}
// Ensure the <sid>\<namespace> folder. It inherits the protected
// DACL from the per-user node, so no tightening is needed here.
std::wstring nsFolder = GetUserNamespaceFolder(id.userSidString,
id.binding->namespaceId);
if (!CreateDirectoryW(nsFolder.c_str(), nullptr))
{
DWORD err = GetLastError();
if (err != ERROR_ALREADY_EXISTS)
{
SendStatus(pipe, Status::IoError);
return;
}
}
hr = WriteFileAtomically(
GetUserFilePath(id.userSidString,
id.binding->namespaceId,
id.binding->fileName),
payload);
SendStatus(pipe, FAILED(hr) ? Status::IoError : Status::Ok);
}
void HandleConnection(HANDLE pipe)
{
CallerIdentity id;
HRESULT hr = AuthenticateCaller(pipe, id);
if (FAILED(hr))
{
Status s = (hr == E_ACCESSDENIED)
? Status::AuthFailCaller
: (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
? Status::NamespaceUnknown
: Status::AuthFailToken;
SendStatus(pipe, s);
return;
}
// ── Read request frame ─────────────────────────────────
uint8_t op = 0;
uint32_t plen = 0;
if (!ReadExact(pipe, &op, sizeof(op)) ||
!ReadExact(pipe, &plen, sizeof(plen)))
{
SendStatus(pipe, Status::BadRequest);
return;
}
if (plen > kMaxPayloadBytes)
{
SendStatus(pipe, Status::PayloadTooLarge);
return;
}
std::vector<BYTE> payload(plen);
if (plen > 0 && !ReadExact(pipe, payload.data(), plen))
{
SendStatus(pipe, Status::BadRequest);
return;
}
// ── Dispatch ───────────────────────────────────────────
switch (static_cast<Opcode>(op))
{
case Opcode::Ping:
SendStatus(pipe, Status::Ok);
break;
case Opcode::GetBlob:
HandleGetBlob(pipe, id);
break;
case Opcode::PutBlob:
HandlePutBlob(pipe, id, payload);
break;
default:
SendStatus(pipe, Status::UnknownOpcode);
break;
}
}
HANDLE CreateProtectedPipe()
{
PSECURITY_DESCRIPTOR sd = nullptr;
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
kPipeSddl, SDDL_REVISION_1, &sd, nullptr))
{
return INVALID_HANDLE_VALUE;
}
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = sd;
sa.bInheritHandle = FALSE;
HANDLE pipe = CreateNamedPipeW(
kPipeName,
PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT |
PIPE_REJECT_REMOTE_CLIENTS,
/*nMaxInstances*/ PIPE_UNLIMITED_INSTANCES,
/*nOutBufferSize*/ 64 * 1024,
/*nInBufferSize*/ 64 * 1024,
/*nDefaultTimeOut*/ 5000,
&sa);
LocalFree(sd);
return pipe;
}
}
DWORD RunPipeServer(HANDLE stopEvent)
{
for (;;)
{
if (WaitForSingleObject(stopEvent, 0) == WAIT_OBJECT_0)
{
return ERROR_SUCCESS;
}
HANDLE pipe = CreateProtectedPipe();
if (pipe == INVALID_HANDLE_VALUE)
{
return GetLastError();
}
// ConnectNamedPipe blocks until a client opens the pipe. The
// service control handler signals stopEvent AND closes the pipe
// handle (via DisconnectNamedPipe from the stop handler) to
// unblock us during shutdown — we observe that path via
// ERROR_BROKEN_PIPE / ERROR_INVALID_HANDLE.
BOOL connected = ConnectNamedPipe(pipe, nullptr);
DWORD err = connected ? ERROR_SUCCESS : GetLastError();
if (!connected && err == ERROR_PIPE_CONNECTED)
{
connected = TRUE;
}
if (WaitForSingleObject(stopEvent, 0) == WAIT_OBJECT_0)
{
CloseHandle(pipe);
return ERROR_SUCCESS;
}
if (connected)
{
HandleConnection(pipe);
FlushFileBuffers(pipe);
DisconnectNamedPipe(pipe);
}
CloseHandle(pipe);
}
}
}

View File

@@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
#pragma once
#include <windows.h>
#include <atomic>
namespace PTSettingsSvc
{
// Runs the named-pipe loop until `stopEvent` is signalled.
// Returns 0 on a clean stop, non-zero on a fatal error.
DWORD RunPipeServer(HANDLE stopEvent);
}

View File

@@ -1,148 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
//
// PTSettingsSvc — PowerToys Settings Service.
//
// Design context — see Design-v6-Final.md in the Workspaces-EoP-Fix folder.
//
// The service runs as a virtual service account (NT SERVICE\PTSettingsSvc),
// owns the DACL on %ProgramData%\Microsoft\PowerToys\SettingsSvc\<ns>\<sid>\
// and is the only writer to the blob.bin file inside it. Callers (Editor,
// SnapshotTool, runner, etc. — see Bindings.cpp) connect over a named pipe
// to GetBlob / PutBlob.
#include <windows.h>
#include <tchar.h>
#include <atomic>
#include "PipeServer.h"
#include "protocol/Protocol.h"
namespace
{
SERVICE_STATUS g_status{};
SERVICE_STATUS_HANDLE g_statusHandle = nullptr;
HANDLE g_stopEvent = nullptr;
HANDLE g_workerThread = nullptr;
void ReportStatus(DWORD state, DWORD waitHintMs = 0, DWORD exitCode = 0)
{
static DWORD checkPoint = 1;
g_status.dwCurrentState = state;
g_status.dwWin32ExitCode = exitCode;
g_status.dwWaitHint = waitHintMs;
g_status.dwControlsAccepted =
(state == SERVICE_START_PENDING) ? 0 : (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN);
g_status.dwCheckPoint = (state == SERVICE_RUNNING || state == SERVICE_STOPPED)
? 0
: checkPoint++;
if (g_statusHandle)
{
SetServiceStatus(g_statusHandle, &g_status);
}
}
DWORD WINAPI WorkerThread(LPVOID)
{
return PTSettingsSvc::RunPipeServer(g_stopEvent);
}
VOID WINAPI ServiceCtrlHandler(DWORD ctrl)
{
switch (ctrl)
{
case SERVICE_CONTROL_STOP:
case SERVICE_CONTROL_SHUTDOWN:
ReportStatus(SERVICE_STOP_PENDING, 5000);
if (g_stopEvent)
{
SetEvent(g_stopEvent);
}
break;
default:
break;
}
}
VOID WINAPI ServiceMain(DWORD, LPTSTR*)
{
g_statusHandle = RegisterServiceCtrlHandlerW(
PTSettingsSvc::kServiceName, ServiceCtrlHandler);
if (!g_statusHandle)
{
return;
}
g_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
ReportStatus(SERVICE_START_PENDING, 3000);
g_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!g_stopEvent)
{
ReportStatus(SERVICE_STOPPED, 0, GetLastError());
return;
}
g_workerThread = CreateThread(nullptr, 0, WorkerThread, nullptr, 0, nullptr);
if (!g_workerThread)
{
DWORD err = GetLastError();
CloseHandle(g_stopEvent);
g_stopEvent = nullptr;
ReportStatus(SERVICE_STOPPED, 0, err);
return;
}
ReportStatus(SERVICE_RUNNING);
WaitForSingleObject(g_workerThread, INFINITE);
DWORD workerRc = 0;
GetExitCodeThread(g_workerThread, &workerRc);
CloseHandle(g_workerThread);
g_workerThread = nullptr;
CloseHandle(g_stopEvent);
g_stopEvent = nullptr;
ReportStatus(SERVICE_STOPPED, 0, workerRc);
}
}
int wmain(int argc, wchar_t* argv[])
{
// `--console` runs the pipe server in the foreground for local debugging
// and prototype testing without going through SCM. Production launch
// always goes through StartServiceCtrlDispatcher.
bool console = false;
for (int i = 1; i < argc; ++i)
{
if (wcscmp(argv[i], L"--console") == 0)
{
console = true;
}
}
if (console)
{
g_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
SetConsoleCtrlHandler([](DWORD) -> BOOL {
if (g_stopEvent) { SetEvent(g_stopEvent); }
return TRUE;
}, TRUE);
DWORD rc = PTSettingsSvc::RunPipeServer(g_stopEvent);
CloseHandle(g_stopEvent);
return static_cast<int>(rc);
}
wchar_t name[] = L"PTSettingsSvc";
SERVICE_TABLE_ENTRYW table[] = {
{ name, ServiceMain },
{ nullptr, nullptr },
};
if (!StartServiceCtrlDispatcherW(table))
{
return static_cast<int>(GetLastError());
}
return 0;
}

View File

@@ -1,46 +0,0 @@
// Win32 version resource for the PowerToys Workspaces Settings Service.
//
// The FileVersion / ProductVersion are sourced from the central PowerToys
// version (common/version/version.h -> Generated Files/version_gen.h), so the
// service exe carries the same product version as the rest of PowerToys.
//
// This version is load-bearing for the per-user hardening path: the service
// reads its own VS_FIXEDFILEINFO (CallerVerify.cpp) and compares it against the
// caller's file version as the signature+version trust anchor. A native exe has
// no managed "assembly version"; the Win32 FileVersion is the canonical value.
#include <windows.h>
#include "..\..\..\common\version\version.h"
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", "PowerToys Workspaces Settings Service"
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", "WorkspacesSettingsService"
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", "PowerToys.PTSettingsSvc.exe"
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

View File

@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{8B6A7C32-5C8D-4AD1-9F60-7E1B3D17A220}</ProjectGuid>
<RootNamespace>WorkspacesSettingsService</RootNamespace>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
<ProjectName>WorkspacesSettingsService</ProjectName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup>
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
<TargetName>PowerToys.PTSettingsSvc</TargetName>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalIncludeDirectories>./;./protocol;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>Advapi32.lib;Shell32.lib;Pathcch.lib;Ole32.lib;Wintrust.lib;Crypt32.lib;Version.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="WorkspacesSettingsService.cpp" />
<ClCompile Include="PipeServer.cpp" />
<ClCompile Include="CallerAuth.cpp" />
<ClCompile Include="CallerVerify.cpp" />
<ClCompile Include="Bindings.cpp" />
<ClCompile Include="FileGuard.cpp" />
<ClCompile Include="Paths.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="PipeServer.h" />
<ClInclude Include="CallerAuth.h" />
<ClInclude Include="CallerVerify.h" />
<ClInclude Include="Bindings.h" />
<ClInclude Include="FileGuard.h" />
<ClInclude Include="Paths.h" />
<ClInclude Include="protocol\Protocol.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="WorkspacesSettingsService.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<!--
NOTE: the signed MSIX (PTSettingsSvc.msix) is NOT packed here. It must be
built from the SIGNED service binary and then signed itself, which happens
in the installer pipeline (steps-build-installer-vnext.yml) AFTER core ESRP
signing and BEFORE the MSI build. Packing it at compile time would capture
the unsigned exe and ship an unsigned package (Design §12.1). For local dev
builds, run devtools\build-msix.ps1 manually.
-->
</Project>

View File

@@ -1,3 +0,0 @@
# Compiled helpers produced by the validation scripts are build artifacts and
# must never be committed.
*.exe

View File

@@ -1,24 +0,0 @@
# WorkspacesSettingsService — dev validation tooling
These scripts stand up and exercise `PTSettingsSvc` locally to validate the v6
tamper-resistant settings design. They are **developer tooling, not product
code** and are not part of the shipping build or the installer.
| File | Purpose |
| --- | --- |
| `setup-ptsettingssvc.ps1` | Registers the service, creates the PROTECTED `%ProgramData%` store, and a fake admin-locked install folder. Run elevated. |
| `verify-prototype.ps1` | Runs the 9-step end-to-end security suite (liveness, caller allow-list, path-prefix, DACL hardness, round-trip, NotFound, per-user DACL, non-user owner, non-elevated write/delete rejection). Does not need elevation. |
| `SaferModify.cs` | Helper compiled on demand by step 9 to obtain a Medium-IL (non-elevated) SAFER token and attempt a tamper write/delete. |
## Usage
```powershell
# 1. Build the service + smoke test (Debug|x64) first.
# 2. Elevated:
pwsh -File .\setup-ptsettingssvc.ps1
# 3. Non-elevated:
pwsh -File .\verify-prototype.ps1
```
`RepoRoot` is derived automatically from the script location; pass `-RepoRoot`
to override. Requires PowerShell 7+ (the suite uses the ternary operator).

View File

@@ -1,27 +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.Runtime.InteropServices;
using System.IO;
class P {
[DllImport("advapi32",SetLastError=true)] static extern bool SaferCreateLevel(int s,int l,int o,out IntPtr h,IntPtr r);
[DllImport("advapi32",SetLastError=true)] static extern bool SaferComputeTokenFromLevel(IntPtr h,IntPtr it,out IntPtr ot,int f,IntPtr r);
[DllImport("advapi32",SetLastError=true)] static extern bool SaferCloseLevel(IntPtr h);
[DllImport("advapi32",SetLastError=true)] static extern bool ImpersonateLoggedOnUser(IntPtr t);
[DllImport("advapi32",SetLastError=true)] static extern bool RevertToSelf();
static int Main(string[] a){
string f=a[0]; IntPtr lvl,tok;
SaferCreateLevel(2,0x20000,1,out lvl,IntPtr.Zero);
SaferComputeTokenFromLevel(lvl,IntPtr.Zero,out tok,0,IntPtr.Zero);
SaferCloseLevel(lvl);
ImpersonateLoggedOnUser(tok);
Console.WriteLine("[as] "+System.Security.Principal.WindowsIdentity.GetCurrent().Name+" (non-elevated SAFER token)");
try { File.WriteAllText(f,"PWNED"); Console.WriteLine("WRITE : SUCCEEDED <-- lock broken"); }
catch(Exception e){ Console.WriteLine("WRITE : rejected -> "+e.GetType().Name); }
try { File.Delete(f); Console.WriteLine("DELETE: SUCCEEDED <-- lock broken"); }
catch(Exception e){ Console.WriteLine("DELETE: rejected -> "+e.GetType().Name); }
RevertToSelf(); return 0;
}
}

View File

@@ -1,64 +0,0 @@
<#
.SYNOPSIS
Build the PowerToys Settings Service MSIX from a built service exe.
Local-dev helper; production packaging happens in the signed build pipeline.
.DESCRIPTION
Stages AppxManifest + logo + the built PowerToys.PTSettingsSvc.exe into a
layout, packs it with makeappx, and (optionally) signs it with a dev cert.
Mirrors the validated prototype (Design-v6-Final.md §12.1).
#>
[CmdletBinding()]
param(
[string]$Config = 'Release',
[string]$ExePath = "$PSScriptRoot\..\x64\$Config\WorkspacesSettingsService\PowerToys.PTSettingsSvc.exe",
[string]$OutMsix = "$PSScriptRoot\..\package\PTSettingsSvc.msix",
[string]$Version = '',
[string]$Arch = 'x64',
[string]$PfxPath = '',
[string]$PfxPass = ''
)
$ErrorActionPreference = 'Stop'
$pkgSrc = Join-Path $PSScriptRoot '..\package'
$staging = Join-Path $env:TEMP 'ptsettingssvc-msix'
$sdkBin = (Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter makeappx.exe |
Where-Object { $_.FullName -match 'x64' } | Select-Object -Last 1).DirectoryName
if (-not (Test-Path $ExePath)) { throw "Service exe not found: $ExePath (build the vcxproj first)." }
# 1x1 transparent logo if none present.
$logo = Join-Path $pkgSrc 'logo.png'
if (-not (Test-Path $logo)) {
[IO.File]::WriteAllBytes($logo,[Convert]::FromBase64String('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='))
}
Remove-Item $staging -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force $staging | Out-Null
Copy-Item (Join-Path $pkgSrc 'AppxManifest.xml') $staging
Copy-Item $logo $staging
Copy-Item $ExePath $staging
# Stamp the package version (must be 4-part) to keep it in lockstep with the build.
# Use case-sensitive -creplace so the lowercase `version` in the XML declaration
# is left untouched (only the Identity's `Version` attribute is replaced).
if ($Version) {
$v = if (($Version -split '\.').Count -eq 3) { "$Version.0" } else { $Version }
$mf = Join-Path $staging 'AppxManifest.xml'
(Get-Content $mf -Raw) -creplace 'Version="[0-9.]+"', "Version=`"$v`"" | Set-Content $mf -Encoding utf8
}
# Stamp the package architecture (MSIX uses lowercase x64/arm64).
$arch = $Arch.ToLowerInvariant()
$mf = Join-Path $staging 'AppxManifest.xml'
(Get-Content $mf -Raw) -creplace 'ProcessorArchitecture="[a-zA-Z0-9]+"', "ProcessorArchitecture=`"$arch`"" | Set-Content $mf -Encoding utf8
& "$sdkBin\makeappx.exe" pack /d $staging /p $OutMsix /o | Out-Null
if ($LASTEXITCODE -ne 0) { throw "makeappx failed ($LASTEXITCODE)." }
Write-Output "packed: $OutMsix"
if ($PfxPath) {
& "$sdkBin\signtool.exe" sign /fd SHA256 /f $PfxPath /p $PfxPass $OutMsix | Out-Null
if ($LASTEXITCODE -ne 0) { throw "signtool failed ($LASTEXITCODE)." }
Write-Output "signed: $OutMsix"
}

View File

@@ -1,184 +0,0 @@
# setup-ptsettingssvc.ps1
#
# Stands up PTSettingsSvc for local v6 prototype validation:
# * Registers the service under NT SERVICE\PTSettingsSvc
# * Creates the PROTECTED data root at %ProgramData%\Microsoft\PowerToys\SettingsSvc
# * Creates a fake "install folder" under %TEMP%, locks its DACL to admin-only,
# copies the smoke-test exe in renamed to an allow-listed basename
# (PowerToys.WorkspacesEditor.exe)
# * Sets HKLM\SOFTWARE\Classes\PowerToys\InstallFolder so the service finds
# the fake install folder via the same code path the production MSI uses
#
# Must be run elevated.
[CmdletBinding()]
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..\..')).Path,
[string]$SvcName = 'PTSettingsSvc',
[string]$DisplayName= 'PowerToys Settings Service',
[string]$Description= 'Provides tamper-resistant storage for PowerToys module settings. Stopping this service prevents affected modules (e.g. Workspaces) from saving configuration changes.',
[string]$FakeInstall= (Join-Path $env:TEMP 'PTFakeInstall')
)
$ErrorActionPreference = 'Stop'
$svcExe = Join-Path $RepoRoot 'x64\Debug\WorkspacesSettingsService\PowerToys.PTSettingsSvc.exe'
$smokeExe = Join-Path $RepoRoot 'x64\Debug\WorkspacesSvcSmokeTest\PowerToys.PTSettingsSvcSmokeTest.exe'
$dataRoot = 'C:\ProgramData\Microsoft\PowerToys\Settings'
$renamedCaller = Join-Path $FakeInstall 'PowerToys.WorkspacesEditor.exe'
$badCaller = Join-Path $FakeInstall 'PowerToys.PTSettingsSvcSmokeTest.exe'
if (-not (Test-Path $svcExe)) { throw "Service exe not found: $svcExe`nBuild WorkspacesSettingsService.vcxproj first." }
if (-not (Test-Path $smokeExe)) { throw "Smoke-test exe not found: $smokeExe`nBuild WorkspacesSvcSmokeTest.vcxproj first." }
Write-Host "=== Setting up PTSettingsSvc for local validation ===" -ForegroundColor Cyan
Write-Host "Running as: $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"
# -------------------------------------------------------------------
# 1) Stop & remove any prior install so we can iterate cleanly.
# -------------------------------------------------------------------
$existing = Get-Service -Name $SvcName -ErrorAction SilentlyContinue
if ($existing)
{
Write-Host "`n[1/5] Found existing service $SvcName - removing ..."
if ($existing.Status -ne 'Stopped')
{
sc.exe stop $SvcName | Out-Null
Start-Sleep -Seconds 2
}
sc.exe delete $SvcName | Out-Null
Start-Sleep -Seconds 1
}
else
{
Write-Host "`n[1/5] No prior install of $SvcName - clean slate."
}
# Also clean any legacy PTWorkspacesSvc from earlier prototype builds.
$legacy = Get-Service -Name 'PTWorkspacesSvc' -ErrorAction SilentlyContinue
if ($legacy)
{
Write-Host " Removing legacy PTWorkspacesSvc from earlier prototype ..."
if ($legacy.Status -ne 'Stopped') { sc.exe stop 'PTWorkspacesSvc' | Out-Null; Start-Sleep 2 }
sc.exe delete 'PTWorkspacesSvc' | Out-Null
}
# -------------------------------------------------------------------
# 2) Create the service under the virtual account.
# -------------------------------------------------------------------
Write-Host "`n[2/5] Creating service $SvcName under NT SERVICE\$SvcName ..."
$out = sc.exe create $SvcName binPath= "`"$svcExe`"" start= demand `
obj= "NT SERVICE\$SvcName" DisplayName= "$DisplayName" 2>&1
Write-Host $out
if ($LASTEXITCODE -ne 0) { throw "sc.exe create failed (exit $LASTEXITCODE)" }
sc.exe description $SvcName "$Description" | Out-Null
sc.exe failure $SvcName reset= 86400 actions= restart/60000/restart/60000/``/``/0 | Out-Null
# -------------------------------------------------------------------
# 3) Create the data root with PROTECTED admin-only DACL.
# -------------------------------------------------------------------
Write-Host "`n[3/5] Setting up data root $dataRoot ..."
if (Test-Path $dataRoot)
{
Write-Host " Folder exists - resetting ACL."
}
else
{
New-Item -ItemType Directory -Force $dataRoot | Out-Null
}
$acl = New-Object System.Security.AccessControl.DirectorySecurity
$acl.SetAccessRuleProtection($true, $false) # PROTECTED, drop inherited ACEs
$svcPrincipal = New-Object System.Security.Principal.NTAccount("NT SERVICE\$SvcName")
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
$svcPrincipal, 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
'BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
$acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
'NT AUTHORITY\Authenticated Users', 'ReadAndExecute', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
$acl.SetOwner((New-Object System.Security.Principal.NTAccount('BUILTIN\Administrators')))
Set-Acl -Path $dataRoot -AclObject $acl
Write-Host " DACL:"
(Get-Acl $dataRoot).Access | ForEach-Object {
Write-Host (" {0,-45} {1,-20} {2}" -f $_.IdentityReference, $_.FileSystemRights, $_.AccessControlType)
}
# -------------------------------------------------------------------
# 4) Set up fake install folder so the smoke test can pass auth.
# -------------------------------------------------------------------
Write-Host "`n[4/5] Setting up fake install folder $FakeInstall ..."
if (Test-Path $FakeInstall) { Remove-Item $FakeInstall -Recurse -Force }
New-Item -ItemType Directory -Force $FakeInstall | Out-Null
# Admin-only DACL. Without this the service's IsFolderAdminOnlyWritable
# check (see Paths.cpp) rejects the install folder and every caller fails
# AuthFailCaller, regardless of binary name.
$ial = New-Object System.Security.AccessControl.DirectorySecurity
$ial.SetAccessRuleProtection($true, $false)
$ial.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
'NT AUTHORITY\SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
$ial.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
'BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
# Virtual service account needs RX so it can read the folder's own DACL
# from inside IsFolderAdminOnlyWritable. Production WiX will grant this
# explicitly to NT SERVICE\PTSettingsSvc; for the smoke test we grant it
# to the whole NT SERVICE bucket which is equivalent for the lookup.
$ial.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
'NT SERVICE\ALL SERVICES', 'ReadAndExecute', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
# Non-admin user needs RX too so the smoke test exe can actually launch
# from this folder under our current login.
$ial.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule(
'BUILTIN\Users', 'ReadAndExecute', 'ContainerInherit,ObjectInherit', 'None', 'Allow')))
$ial.SetOwner((New-Object System.Security.Principal.NTAccount('BUILTIN\Administrators')))
Set-Acl -Path $FakeInstall -AclObject $ial
# Copy smoke test twice: one renamed to an allow-listed basename (positive
# case), one keeping its real name (negative case: AuthRejected).
Copy-Item $smokeExe $renamedCaller -Force
Copy-Item $smokeExe $badCaller -Force
Write-Host " Copied:"
Write-Host " $renamedCaller (allow-listed basename, should pass auth)"
Write-Host " $badCaller (real name, should be rejected)"
# Point the service at the fake install folder via the same HKLM key the
# production MSI writes. Without this the service reads InstallFolder=""
# and rejects every caller.
$hklmKey = 'HKLM:\SOFTWARE\Classes\PowerToys'
if (-not (Test-Path $hklmKey)) { New-Item -Path $hklmKey -Force | Out-Null }
Set-ItemProperty -Path $hklmKey -Name 'InstallFolder' -Value $FakeInstall -Type String
Write-Host " HKLM\SOFTWARE\Classes\PowerToys\InstallFolder = $FakeInstall"
# -------------------------------------------------------------------
# 5) Start the service.
# -------------------------------------------------------------------
Write-Host "`n[5/5] Starting service ..."
sc.exe start $SvcName | Out-Null
Start-Sleep -Seconds 2
$svc = Get-Service -Name $SvcName
Write-Host " Status: $($svc.Status)"
if ($svc.Status -eq 'Running')
{
$proc = Get-CimInstance Win32_Process -Filter "Name = 'PowerToys.PTSettingsSvc.exe'" -ErrorAction SilentlyContinue
if ($proc)
{
$owner = Invoke-CimMethod -InputObject $proc -MethodName GetOwner
Write-Host " Running as: $($owner.Domain)\$($owner.User) (PID $($proc.ProcessId))"
}
}
else
{
Write-Warning "Service is not Running. sc.exe query output:"
sc.exe query $SvcName
}
Write-Host "`n=== Setup complete ===" -ForegroundColor Green
Write-Host "Pipe: \\.\pipe\$SvcName"
Write-Host "DataRoot: $dataRoot"
Write-Host "InstallFld: $FakeInstall"
Write-Host ""
Write-Host "Next: run verify-prototype.ps1 (does not need elevation)."

View File

@@ -1,284 +0,0 @@
# verify-prototype.ps1
#
# Exercises the PTSettingsSvc prototype end-to-end.
# Run AFTER setup-ptsettingssvc.ps1. Does NOT need elevation.
#
# Coverage:
# 1. Liveness (Ping)
# 2. Caller-allow-list — bad-basename caller is rejected
# 3. Path-prefix — caller outside install folder rejected
# 4. Install-folder DACL hardness — temporarily relax DACL, expect rejection
# 5. Round-trip — PutBlob a payload, GetBlob it back
# 6. GetBlob NotFound — fresh user/namespace returns NotFound
# 7. Per-user folder DACL — only this user can read; admin can; others cannot
# 8. Owner is a non-user principal — store nodes owned by SYSTEM/Admin/service, never the user
# 9. Non-elevated write+delete rejected — Medium-IL user token cannot tamper or delete the blob
[CmdletBinding()]
param(
[string]$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..\..')).Path,
[string]$FakeInstall = (Join-Path $env:TEMP 'PTFakeInstall'),
[string]$DataRoot = 'C:\ProgramData\Microsoft\PowerToys\Settings'
)
$ErrorActionPreference = 'Continue'
$smokeExe = Join-Path $RepoRoot 'x64\Debug\WorkspacesSvcSmokeTest\PowerToys.PTSettingsSvcSmokeTest.exe'
$renamedCaller = Join-Path $FakeInstall 'PowerToys.WorkspacesEditor.exe'
$badCaller = Join-Path $FakeInstall 'PowerToys.PTSettingsSvcSmokeTest.exe'
$tmpPayload = Join-Path $env:TEMP 'pt-prototype-payload.bin'
$tmpReadBack = Join-Path $env:TEMP 'pt-prototype-readback.bin'
$pass = 0; $fail = 0
function Step([string]$name, [scriptblock]$body)
{
Write-Host ""
Write-Host "── $name ──" -ForegroundColor Cyan
try
{
$ok = & $body
if ($ok) { Write-Host " PASS" -ForegroundColor Green; $script:pass++ }
else { Write-Host " FAIL" -ForegroundColor Red; $script:fail++ }
}
catch
{
Write-Host " FAIL (exception): $_" -ForegroundColor Red
$script:fail++
}
}
function Run-Caller([string]$caller, [string[]]$callerArgs)
{
$out = & $caller @callerArgs 2>&1
[pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = ($out -join "`n") }
}
# Sanity: artefacts exist.
if (-not (Test-Path $smokeExe)) { throw "Smoke test not built: $smokeExe" }
if (-not (Test-Path $renamedCaller)) { throw "$renamedCaller missing - run setup-ptsettingssvc.ps1 first" }
if (-not (Test-Path $badCaller)) { throw "$badCaller missing - run setup-ptsettingssvc.ps1 first" }
Write-Host "==============================================" -ForegroundColor Yellow
Write-Host " PTSettingsSvc prototype verification" -ForegroundColor Yellow
Write-Host "==============================================" -ForegroundColor Yellow
Write-Host " User : $env:USERDOMAIN\$env:USERNAME"
Write-Host " Pipe : \\.\pipe\PTSettingsSvc"
Write-Host " DataRoot : $DataRoot"
Write-Host " InstallFld: $FakeInstall"
# 1) Liveness ----------------------------------------------------------
Step "1. Ping (allow-listed caller, happy path)" {
$r = Run-Caller $renamedCaller @('ping')
Write-Host " output: $($r.Output)"
return ($r.ExitCode -eq 0 -and $r.Output -match 'Ping -> Ok')
}
# 2) Caller allow-list -------------------------------------------------
Step "2. Bad basename -> AuthRejected" {
$r = Run-Caller $badCaller @('ping')
Write-Host " output: $($r.Output)"
return ($r.ExitCode -ne 0 -and $r.Output -match 'AuthRejected')
}
# 3) Path-prefix -------------------------------------------------------
Step "3. Caller outside install folder -> AuthRejected" {
# Run the smoke test directly from its build folder — that path is
# NOT under InstallFolder so the path-prefix check should reject it
# (even though its basename also isn't allow-listed).
$r = Run-Caller $smokeExe @('ping')
Write-Host " output: $($r.Output)"
return ($r.ExitCode -ne 0 -and $r.Output -match 'AuthRejected')
}
# 4) Install-folder DACL hardness check -------------------------------
Step "4. User-write ACE on install folder -> AuthRejected" {
# This step needs elevation because we have to add an ACL ourselves.
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$isAdmin = (New-Object Security.Principal.WindowsPrincipal $identity).IsInRole(
[Security.Principal.WindowsBuiltinRole]::Administrator)
if (-not $isAdmin)
{
Write-Host " SKIPPED (needs elevation; re-run this script from an admin shell to exercise)"
return $true
}
# Snapshot original DACL.
$original = Get-Acl $FakeInstall
try
{
$acl = Get-Acl $FakeInstall
$ace = New-Object System.Security.AccessControl.FileSystemAccessRule(
"$env:USERDOMAIN\$env:USERNAME", 'Modify',
'ContainerInherit,ObjectInherit', 'None', 'Allow')
$acl.AddAccessRule($ace)
Set-Acl $FakeInstall $acl
$r = Run-Caller $renamedCaller @('ping')
Write-Host " output (with user-write ACE present): $($r.Output)"
$rejected = ($r.ExitCode -ne 0 -and $r.Output -match 'AuthRejected')
# Restore.
Set-Acl $FakeInstall $original
$r2 = Run-Caller $renamedCaller @('ping')
Write-Host " output (DACL restored): $($r2.Output)"
$restoredOk = ($r2.ExitCode -eq 0 -and $r2.Output -match 'Ping -> Ok')
return ($rejected -and $restoredOk)
}
catch
{
Set-Acl $FakeInstall $original
throw
}
}
# 5) Round-trip --------------------------------------------------------
Step "5. PutBlob then GetBlob round-trip" {
$payload = '{"$schemaVersion":1,"workspaces":[{"id":"abc","name":"test-' + (Get-Date -Format o) + '"}]}'
[System.IO.File]::WriteAllText($tmpPayload, $payload, [System.Text.UTF8Encoding]::new($false))
$put = Run-Caller $renamedCaller @('put', $tmpPayload)
Write-Host " put: $($put.Output)"
if ($put.ExitCode -ne 0 -or $put.Output -notmatch 'Ok') { return $false }
if (Test-Path $tmpReadBack) { Remove-Item $tmpReadBack -Force }
$get = Run-Caller $renamedCaller @('get', $tmpReadBack)
Write-Host " get: $($get.Output)"
if ($get.ExitCode -ne 0) { return $false }
$readBack = [System.IO.File]::ReadAllText($tmpReadBack)
return ($readBack -eq $payload)
}
# 6) GetBlob NotFound on fresh namespace -------------------------------
Step "6. GetBlob NotFound semantics (delete blob, expect NotFound)" {
$blobPath = Join-Path (Join-Path (Join-Path $DataRoot ([Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) 'Workspaces') 'workspaces.json'
if (Test-Path $blobPath)
{
# Need elevation to delete - service owns the dir.
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$isAdmin = (New-Object Security.Principal.WindowsPrincipal $identity).IsInRole(
[Security.Principal.WindowsBuiltinRole]::Administrator)
if (-not $isAdmin)
{
Write-Host " SKIPPED (needs elevation to clear the blob; the blob exists from step 5)"
return $true
}
Remove-Item $blobPath -Force
}
$get = Run-Caller $renamedCaller @('get')
Write-Host " get: $($get.Output)"
return ($get.Output -match 'NotFound')
}
# 7) Per-user folder DACL ---------------------------------------------
Step "7. Per-user folder DACL (svc:F, admin:F, current-user:RX, others denied)" {
# First PutBlob so the user folder exists.
$payload = 'hello'
[System.IO.File]::WriteAllText($tmpPayload, $payload, [System.Text.UTF8Encoding]::new($false))
Run-Caller $renamedCaller @('put', $tmpPayload) | Out-Null
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$userDir = Join-Path $DataRoot $userSid
if (-not (Test-Path $userDir))
{
Write-Host " user folder not created: $userDir"
return $false
}
$acl = Get-Acl $userDir
Write-Host " DACL of $userDir :"
$acl.Access | ForEach-Object {
Write-Host (" {0,-45} {1,-20} {2}" -f $_.IdentityReference, $_.FileSystemRights, $_.AccessControlType)
}
$svcOk = $acl.Access | Where-Object {
$_.IdentityReference.Value -like '*PTSettingsSvc*' -and
$_.AccessControlType -eq 'Allow' -and
$_.FileSystemRights -match 'FullControl'
} | Select-Object -First 1
$admOk = $acl.Access | Where-Object {
$_.IdentityReference.Value -like '*Administrators*' -and
$_.AccessControlType -eq 'Allow' -and
$_.FileSystemRights -match 'FullControl'
} | Select-Object -First 1
$userOk = $acl.Access | Where-Object {
($_.IdentityReference.Value -eq "$env:USERDOMAIN\$env:USERNAME" -or
$_.IdentityReference.Value -like "*$userSid*") -and
$_.AccessControlType -eq 'Allow' -and
($_.FileSystemRights -match 'Read' -or $_.FileSystemRights -match 'Execute')
} | Select-Object -First 1
$noWild = -not ($acl.Access | Where-Object {
$_.IdentityReference.Value -like '*Authenticated Users*' -or
$_.IdentityReference.Value -like '*Everyone*'
})
$protectedOk = -not $acl.AreAccessRulesProtected -eq $false
Write-Host " svc:F=$([bool]$svcOk) admin:F=$([bool]$admOk) user:R*=$([bool]$userOk) no-blanket-AuthUsers=$noWild PROTECTED=$($acl.AreAccessRulesProtected)"
return ([bool]$svcOk -and [bool]$admOk -and [bool]$userOk -and $noWild -and $acl.AreAccessRulesProtected)
}
# 8) Owner is a non-user trusted principal ----------------------------
Step "8. Owner of store nodes is a non-user principal (SYSTEM/Admin/service)" {
# Ensure the user folder + blob exist.
[System.IO.File]::WriteAllText($tmpPayload, 'hello', [System.Text.UTF8Encoding]::new($false))
Run-Caller $renamedCaller @('put', $tmpPayload) | Out-Null
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$userDir = Join-Path $DataRoot $userSid
$blob = Join-Path (Join-Path $userDir 'Workspaces') 'workspaces.json'
$me = "$env:USERDOMAIN\$env:USERNAME"
$trusted = @('NT AUTHORITY\SYSTEM', 'BUILTIN\Administrators', 'NT SERVICE\PTSettingsSvc')
$allOk = $true
foreach ($p in @($DataRoot, $userDir, $blob))
{
if (-not (Test-Path $p)) { continue }
$owner = (Get-Acl $p).Owner
$ok = ($owner -ne $me) -and ($trusted -contains $owner)
Write-Host (" {0,-70} owner={1} {2}" -f (Split-Path $p -Leaf), $owner, ($ok ? 'OK' : 'BAD'))
if (-not $ok) { $allOk = $false }
}
return $allOk
}
# 9) Non-elevated write + delete are both rejected --------------------
Step "9. Medium-IL user token cannot write or delete the blob" {
$safer = Join-Path $PSScriptRoot 'SaferModify.exe'
$saferSrc = Join-Path $PSScriptRoot 'SaferModify.cs'
if (-not (Test-Path $safer) -and (Test-Path $saferSrc))
{
# Build the helper from source so the suite is self-contained.
$csc = Get-ChildItem 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe' -ErrorAction SilentlyContinue | Select-Object -First 1
if ($csc) { & $csc.FullName /nologo /out:$safer $saferSrc 2>&1 | Out-Null }
}
if (-not (Test-Path $safer))
{
Write-Host " SKIPPED (SaferModify.exe/.cs not present in $PSScriptRoot)"
return $true
}
# Ensure the blob exists to target.
[System.IO.File]::WriteAllText($tmpPayload, 'hello', [System.Text.UTF8Encoding]::new($false))
Run-Caller $renamedCaller @('put', $tmpPayload) | Out-Null
$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$blob = Join-Path (Join-Path (Join-Path $DataRoot $userSid) 'Workspaces') 'workspaces.json'
$out = (& $safer $blob 2>&1) -join "`n"
Write-Host ($out -split "`n" | ForEach-Object { " $_" }) -Separator "`n"
$writeRej = $out -match 'WRITE\s*:\s*rejected'
$deleteRej = $out -match 'DELETE\s*:\s*rejected'
$intact = Test-Path $blob
return ($writeRej -and $deleteRej -and $intact)
}
Write-Host ""
Write-Host "==============================================" -ForegroundColor Yellow
Write-Host " Result: $pass passed, $fail failed" -ForegroundColor (@('Green','Red')[[int]($fail -gt 0)])
Write-Host "==============================================" -ForegroundColor Yellow
if ($fail -gt 0) { exit 1 } else { exit 0 }

View File

@@ -1,5 +0,0 @@
*.pfx
*.cer
*.msix
logo.png
*.ps1

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
MSIX package for the PowerToys Settings Service (Design-v6-Final.md §12.1).
The service binary lives in the immutable, signed WindowsApps store so a
non-admin same-user attacker cannot replace it. The windows.service
extension auto-registers PTSettingsSvc; it runs as LocalSystem (the only
start account MSIX allows that can own/protect the per-SID store DACL).
Data is written to real %ProgramData% by the service, not virtualized.
Publisher must match the signing certificate subject. For production this
is the Microsoft cert; for local validation the dev cert subject is used.
-->
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6">
<Identity Name="Microsoft.PowerToys.SettingsService"
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
Version="0.0.1.0"
ProcessorArchitecture="x64" />
<Properties>
<DisplayName>PowerToys Settings Service</DisplayName>
<PublisherDisplayName>Microsoft Corporation</PublisherDisplayName>
<Logo>logo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
</Resources>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<rescap:Capability Name="packagedServices" />
<rescap:Capability Name="localSystemServices" />
</Capabilities>
<Applications>
<Application Id="PTSettingsSvc"
Executable="PowerToys.PTSettingsSvc.exe"
EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements DisplayName="PowerToys Settings Service"
Description="Sole writer of the protected PowerToys settings store (EoP defense)."
BackgroundColor="transparent" Square150x150Logo="logo.png" Square44x44Logo="logo.png"
AppListEntry="none" />
<Extensions>
<desktop6:Extension Category="windows.service"
Executable="PowerToys.PTSettingsSvc.exe"
EntryPoint="Windows.FullTrustApplication">
<desktop6:Service Name="PTSettingsSvc"
StartupType="auto"
StartAccount="localSystem" />
</desktop6:Extension>
</Extensions>
</Application>
</Applications>
</Package>

View File

@@ -1,66 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
//
// Shared wire protocol between PTSettingsSvc and its clients.
//
// Wire format (little-endian, no padding):
//
// REQUEST := opcode(uint8) | length(uint32) | payload[length]
// RESPONSE := status(uint8) | length(uint32) | payload[length]
//
// One request per connection. After the response is written the server
// disconnects. Keep this surface as small as possible — every additional
// opcode is a new attack surface on a privileged endpoint.
//
// The service treats `payload` as opaque bytes. It does not parse them,
// does not validate their shape, does not interpret a "schema version"
// inside them. Module-specific concerns (JSON shape, schema versioning,
// migration from legacy on-disk layouts, sensitive-field stripping) all
// live in the caller — see Design-v6-Final.md §4 and §10.
#pragma once
#include <cstdint>
namespace PTSettingsSvc
{
// Wire constants ---------------------------------------------------------
constexpr const wchar_t* kPipeName = L"\\\\.\\pipe\\PTSettingsSvc";
constexpr const wchar_t* kServiceName = L"PTSettingsSvc";
// Payload size guard rails. A typical settings blob sits in the low
// tens of KB. 1 MiB is generous and bounds memory the service has to
// allocate per request.
constexpr uint32_t kMaxPayloadBytes = 1u * 1024u * 1024u;
enum class Opcode : uint8_t
{
Ping = 0x00, // No payload. Authn still runs. Used by liveness checks.
GetBlob = 0x01, // No payload. Returns the caller's namespace blob bytes.
PutBlob = 0x02, // payload = full blob bytes. Atomic replace.
};
enum class Status : uint8_t
{
Ok = 0x00,
// Framing / dispatch errors.
BadRequest = 0x01,
UnknownOpcode = 0x02,
PayloadTooLarge = 0x03,
// Authentication outcomes.
AuthFailToken = 0x10, // Caller token is synthetic (SYSTEM / SERVICE / etc.)
// or the SID couldn't be read.
AuthFailCaller = 0x11, // Caller exe failed path / DACL-hardness /
// basename allow-list.
NamespaceUnknown = 0x12, // Caller authenticated but is not in the
// binding table (should never happen for
// well-formed clients).
// Storage outcomes.
NotFound = 0x20, // GetBlob: blob does not exist yet.
IoError = 0x21, // Underlying file IO failed.
};
}

View File

@@ -1,135 +0,0 @@
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
//
// Console smoke test for PTSettingsSvc.
//
// Usage:
// PowerToys.PTSettingsSvcSmokeTest.exe ping
// PowerToys.PTSettingsSvcSmokeTest.exe get [<output-file>]
// PowerToys.PTSettingsSvcSmokeTest.exe put <input-file>
//
// Pair with `PowerToys.PTSettingsSvc.exe --console` in another terminal
// when iterating without installing & registering the service.
//
// NB: this exe is NOT in the caller-binding allow-list, so the service
// will return AuthRejected unless one of the following holds:
// * you copy/rename this exe to one of the allow-listed basenames
// (e.g. PowerToys.WorkspacesEditor.exe) under the PT install folder
// pointed to by HKLM\SOFTWARE\Classes\PowerToys\InstallFolder
// (or by the PT_DEV_INSTALL_FOLDER env var in dev builds), AND
// * that folder's DACL is admin-only writable (per Design-v6-Final.md §8).
//
// The verify-prototype.ps1 script automates both prerequisites.
#include "../../WorkspacesSettingsClient/PTSettingsClient.h"
#include <windows.h>
#include <cstdio>
#include <string>
#include <fstream>
#include <vector>
namespace
{
std::vector<uint8_t> ReadAllBytes(const char* path)
{
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return {};
std::streamsize size = f.tellg();
if (size <= 0)
{
return {};
}
std::vector<uint8_t> buf(static_cast<size_t>(size));
f.seekg(0, std::ios::beg);
f.read(reinterpret_cast<char*>(buf.data()), size);
return buf;
}
bool WriteAllBytes(const char* path, const std::vector<uint8_t>& bytes)
{
std::ofstream f(path, std::ios::binary | std::ios::trunc);
if (!f) return false;
if (!bytes.empty())
{
f.write(reinterpret_cast<const char*>(bytes.data()),
static_cast<std::streamsize>(bytes.size()));
}
return static_cast<bool>(f);
}
const char* Name(PTSettingsClient::Result r)
{
switch (r)
{
case PTSettingsClient::Result::Ok: return "Ok";
case PTSettingsClient::Result::ServiceUnavailable: return "ServiceUnavailable";
case PTSettingsClient::Result::AuthRejected: return "AuthRejected";
case PTSettingsClient::Result::NamespaceUnknown: return "NamespaceUnknown";
case PTSettingsClient::Result::NotFound: return "NotFound";
case PTSettingsClient::Result::ProtocolError: return "ProtocolError";
case PTSettingsClient::Result::PayloadTooLarge: return "PayloadTooLarge";
case PTSettingsClient::Result::IoError: return "IoError";
case PTSettingsClient::Result::UnknownStatus: return "UnknownStatus";
}
return "?";
}
}
int main(int argc, char* argv[])
{
if (argc < 2)
{
std::printf("usage: %s ping | get [<output-file>] | put <input-file>\n", argv[0]);
return 2;
}
std::string cmd = argv[1];
if (cmd == "ping")
{
auto rc = PTSettingsClient::Ping();
std::printf("Ping -> %s\n", Name(rc));
return rc == PTSettingsClient::Result::Ok ? 0 : 1;
}
if (cmd == "get")
{
std::vector<uint8_t> bytes;
auto rc = PTSettingsClient::GetBlob(bytes);
std::printf("GetBlob -> %s, %zu bytes\n", Name(rc), bytes.size());
if (rc == PTSettingsClient::Result::Ok)
{
if (argc >= 3)
{
bool ok = WriteAllBytes(argv[2], bytes);
std::printf(" wrote %zu bytes to %s%s\n",
bytes.size(), argv[2], ok ? "" : " (FAILED)");
if (!ok) return 1;
}
else if (!bytes.empty())
{
std::fwrite(bytes.data(), 1, bytes.size(), stdout);
std::printf("\n");
}
}
return rc == PTSettingsClient::Result::Ok ||
rc == PTSettingsClient::Result::NotFound ? 0 : 1;
}
if (cmd == "put" && argc >= 3)
{
auto bytes = ReadAllBytes(argv[2]);
if (bytes.empty())
{
std::fprintf(stderr, "input file empty or unreadable: %s\n", argv[2]);
return 2;
}
auto rc = PTSettingsClient::PutBlob(bytes);
std::printf("PutBlob (%zu bytes) -> %s\n", bytes.size(), Name(rc));
return rc == PTSettingsClient::Result::Ok ? 0 : 1;
}
std::fprintf(stderr, "unknown / incomplete command: %s\n", argv[1]);
return 2;
}

View File

@@ -1,79 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{8B6A7C32-5C8D-4AD1-9F60-7E1B3D17A221}</ProjectGuid>
<RootNamespace>WorkspacesSvcSmokeTest</RootNamespace>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
<ProjectName>WorkspacesSvcSmokeTest</ProjectName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup>
<OutDir>$(RepoRoot)$(Platform)\$(Configuration)\$(MSBuildProjectName)\</OutDir>
<TargetName>PowerToys.PTSettingsSvcSmokeTest</TargetName>
<!-- Manual CLI driver, not an automated VSTest container. Opt out of the
RunVSTest SDK so the CI "/t:Test" pass does not try to execute it. -->
<RunVSTest>false</RunVSTest>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="SmokeTest.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WorkspacesSettingsClient\WorkspacesSettingsClient.vcxproj">
<Project>{D24E2C12-9911-4E51-B102-39E7B62B22F1}</Project>
</ProjectReference>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -64,7 +64,7 @@ int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, LPSTR cmdline, int cm
if (projectToLaunch.id.empty())
{
auto file = WorkspacesData::WorkspacesFile();
auto res = JsonUtils::ReadWorkspacesFromService();
auto res = JsonUtils::ReadWorkspaces(file);
if (res.isOk())
{
workspaces = res.getValue();

View File

@@ -1089,13 +1089,10 @@ VideoRecordingSession::VideoRecordingSession(
// Store frame interval for timeout-based frame production when webcam is active.
m_frameIntervalTicks = ( frameRate > 0 ) ? ( 10'000'000LL / frameRate ) : 333'333LL;
if (captureAudio || captureSystemAudio)
{
// Always set up audio profile for loopback capture (stereo AAC)
auto audio = m_encodingProfile.Audio();
audio = winrt::AudioEncodingProperties::CreateAac(48000, 2, 192000);
m_encodingProfile.Audio(audio);
}
// NOTE: Audio encoding profile (m_encodingProfile.Audio) is set in
// StartAsync() after the audio graph is fully initialized, not here.
// Calling GetEncodingProperties() before InitializeAsync completes
// would crash because m_audioOutputNode is still null.
// Describe our input: uncompressed BGRA8 buffers
auto properties = winrt::VideoEncodingProperties::CreateUncompressed(
@@ -1176,7 +1173,16 @@ winrt::IAsyncAction VideoRecordingSession::StartAsync()
RecDiag( L"StartAsync: co_await InitializeAsync...\n" );
co_await m_audioGenerator->InitializeAsync();
RecDiag( L"StartAsync: audio initialized\n" );
m_streamSource = winrt::MediaStreamSource(m_videoDescriptor, winrt::AudioStreamDescriptor(m_audioGenerator->GetEncodingProperties()));
// Set up the audio encoding profile now that the audio graph is
// fully initialized. GetEncodingProperties() requires
// m_audioOutputNode to be valid, which is only guaranteed after
// InitializeAsync completes.
auto audioProps = m_audioGenerator->GetEncodingProperties();
m_encodingProfile.Audio(winrt::AudioEncodingProperties::CreateAac(
audioProps.SampleRate(), audioProps.ChannelCount(), 192000));
m_streamSource = winrt::MediaStreamSource(m_videoDescriptor, winrt::AudioStreamDescriptor(audioProps));
}
else {

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -20,6 +19,9 @@ public record AppStateModel
init => _recentCommands = value;
}
// HERE BE DRAGONS: Using an ImmutableList<T> for a setting may explode in
// AOT builds. Make sure to test IN AOT setting this setting to null, [],
// and and array with values.
private ImmutableList<string>? _runHistory = ImmutableList<string>.Empty;
public ImmutableList<string> RunHistory

View File

@@ -427,12 +427,39 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
var title = _commandItemViewModel?.Title ?? string.Empty;
var subtitle = _commandItemViewModel?.Subtitle ?? string.Empty;
var icon = _commandItemViewModel?.Icon;
var dockSide = _settingsService.Settings.DockSettings.Side;
IReadOnlyList<MonitorInfo>? monitors = _monitorService?.GetMonitors();
var dockSettings = _settingsService.Settings.DockSettings;
var dockSide = dockSettings.Side;
IReadOnlyList<MonitorInfo>? monitors = GetDockEnabledMonitors(_monitorService, dockSettings);
ShowPinToDockDialogMessage message = new(_providerId, _commandId, title, subtitle, icon, dockSide, monitors);
WeakReferenceMessenger.Default.Send(message);
}
// Only list monitors where the dock is currently enabled, so users can't
// pin a command to a display that has no dock visible.
private static IReadOnlyList<MonitorInfo>? GetDockEnabledMonitors(IMonitorService? monitorService, DockSettings dockSettings)
{
var monitors = monitorService?.GetMonitors();
if (monitors is null)
{
return null;
}
var configs = dockSettings.MonitorConfigs;
// When there are no per-monitor configs (legacy / first-run), the dock
// is only shown on the primary monitor.
if (configs.Count == 0)
{
return monitors.Where(m => m.IsPrimary).ToList();
}
return monitors
.Where(m => configs.Any(c =>
string.Equals(c.MonitorDeviceId, m.StableId, System.StringComparison.OrdinalIgnoreCase) &&
c.Enabled))
.ToList();
}
private void UnpinFromDock()
{
PinToDockMessage message = new(_providerId, _commandId, false);

View File

@@ -24,9 +24,21 @@ internal sealed class RunHistoryService : IRunHistoryService
if (_appStateService.State.RunHistory.IsEmpty)
{
var history = Microsoft.Terminal.UI.RunHistory.CreateRunHistory();
// Copy the WinRT-projected IVector<string> into a plain List<string>
// before building the ImmutableList. ImmutableList.CreateRange tries to
// cast the source to IReadOnlyCollection<string>, which requires a WinRT
// helper type that isn't available in AOT builds and throws
// NotSupportedException.
var historyList = new List<string>(history.Count);
for (var i = 0; i < history.Count; i++)
{
historyList.Add(history[i]);
}
_appStateService.UpdateState(state => state with
{
RunHistory = history.ToImmutableList(),
RunHistory = historyList.ToImmutableList(),
});
}

View File

@@ -134,6 +134,25 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
MoreCommands = _networkPage.Commands,
};
if (isBandPage)
{
_networkUpItem = new ListItem(_networkPage)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
Icon = Icons.NetworkUpIcon,
MoreCommands = _networkPage.Commands,
};
_networkDownItem = new ListItem(_networkPage)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
Icon = Icons.NetworkDownIcon,
MoreCommands = _networkPage.Commands,
};
}
_networkPage.Updated += (s, e) =>
{
_networkItem.Title = _networkPage.GetItemTitle(isBandPage);
@@ -253,22 +272,6 @@ internal sealed partial class PerformanceWidgetsPage : OnLoadStaticListPage, IDi
}
else
{
_networkUpItem = new ListItem(_networkPage!)
{
Title = $"{_networkUpSpeed}",
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
Icon = Icons.NetworkUpIcon,
MoreCommands = _networkPage!.Commands,
};
_networkDownItem = new ListItem(_networkPage!)
{
Title = $"{_networkDownSpeed}",
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
Icon = Icons.NetworkDownIcon,
MoreCommands = _networkPage!.Commands,
};
return _batteryItem is not null
? new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem!, _batteryItem! }
: new[] { _cpuItem!, _memoryItem!, _networkUpItem!, _networkDownItem!, _gpuItem! };

View File

@@ -94,7 +94,7 @@ internal static class Commands
})
{
Title = Resources.Microsoft_plugin_sys_hibernate,
Icon = Icons.SleepIcon, // Icon change needed
Icon = Icons.HibernateIcon,
},
});

View File

@@ -25,4 +25,6 @@ internal sealed class Icons
internal static IconInfo ShutdownIcon { get; } = new IconInfo("\uE7E8");
internal static IconInfo SleepIcon { get; } = new IconInfo("\uE708");
internal static IconInfo HibernateIcon { get; } = new IconInfo("\uE823");
}

View File

@@ -243,5 +243,7 @@ namespace ColorPicker.Helpers
lpPoint.Y += yOffset;
SetCursorPos(lpPoint.X, lpPoint.Y);
}
internal IntPtr GetMainWindowHandle() => _hwndSource?.Handle ?? IntPtr.Zero;
}
}

View File

@@ -0,0 +1,51 @@
// 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.Runtime.InteropServices;
using ManagedCommon;
namespace ColorPicker.Helpers;
internal static class WindowCaptureExclusionHelper
{
// Windows 10 version 2004 (build 19041) is the minimum supported version. PowerToys
// itself requires the same version, so this check is not strictly required, but is
// useful as a safeguard.
private static readonly bool IsSupported =
Environment.OSVersion.Version >= new Version(10, 0, 19041);
// Only logging once per session to avoid repeated identical warnings, as the zoom
// window may be used very often.
private static bool hasLoggedFailure;
internal static bool Exclude(IntPtr hwnd) =>
SetWindowAffinity(hwnd, NativeMethods.WDA_EXCLUDEFROMCAPTURE);
internal static bool Include(IntPtr hwnd) =>
SetWindowAffinity(hwnd, NativeMethods.WDA_NONE);
private static bool SetWindowAffinity(nint hwnd, uint affinity)
{
if (!IsSupported)
{
return false;
}
bool success = NativeMethods.SetWindowDisplayAffinity(hwnd, affinity);
if (!success)
{
int errorCode = Marshal.GetLastWin32Error();
if (!hasLoggedFailure)
{
Logger.LogWarning(
$"Failed to set window display affinity. Error code: {errorCode}");
hasLoggedFailure = true;
}
}
return success;
}
}

View File

@@ -79,12 +79,30 @@ namespace ColorPicker.Helpers
// we just started zooming, copy screen area
if (_previousZoomLevel == 0)
{
var x = (int)point.X - (BaseZoomImageSize / 2);
var y = (int)point.Y - (BaseZoomImageSize / 2);
// First, exclude the color picker window from the capture; otherwise its
// corner will be included in the zoomed-in image.
var mainWindowHandle = _appStateHandler.GetMainWindowHandle();
bool exclusionSuccess =
WindowCaptureExclusionHelper.Exclude(mainWindowHandle);
_graphics.CopyFromScreen(x, y, 0, 0, _bmp.Size, CopyPixelOperation.SourceCopy);
try
{
var x = (int)point.X - (BaseZoomImageSize / 2);
var y = (int)point.Y - (BaseZoomImageSize / 2);
_zoomViewModel.ZoomArea = BitmapToImageSource(_bmp);
_graphics.CopyFromScreen(x, y, 0, 0, _bmp.Size, CopyPixelOperation.SourceCopy);
_zoomViewModel.ZoomArea = BitmapToImageSource(_bmp);
}
finally
{
// Restore the color picker window to normal display affinity so that
// it can be captured again.
if (exclusionSuccess)
{
WindowCaptureExclusionHelper.Include(mainWindowHandle);
}
}
}
_zoomViewModel.ZoomFactor = Math.Pow(ZoomFactor, _currentZoomLevel - 1);

View File

@@ -231,5 +231,17 @@ namespace ColorPicker
var hwnd = new WindowInteropHelper(win).Handle;
_ = SetWindowLong(hwnd, GWL_EX_STYLE, GetWindowLong(hwnd, GWL_EX_STYLE) | WS_EX_TOOLWINDOW);
}
/// <summary>
/// Sets the display affinity of a window, which controls how the window is
/// displayed on a monitor. Used to exclude the picker window from ZoomWindow's
/// source bitmap.
/// </summary>
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint dwAffinity);
internal const uint WDA_NONE = 0x00000000;
internal const uint WDA_EXCLUDEFROMCAPTURE = 0x00000011;
}
}

View File

@@ -139,6 +139,14 @@ namespace KeyboardEventHandlers
if (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN)
{
ResetIfModifierKeyForLowerLevelKeyHandlers(ii, it->first, target);
// If a Ctrl/Alt/Shift key is remapped to a non-modifier key, reset the modifier state to prevent the injected key from being delivered as WM_SYSKEYDOWN instead of WM_KEYDOWN
if (Helpers::IsModifierKey(it->first) && !Helpers::IsModifierKey(target) && target != VK_CAPITAL && !(it->first == VK_LWIN || it->first == VK_RWIN || it->first == CommonSharedConstants::VK_WIN_BOTH))
{
std::vector<INPUT> suppressList;
Helpers::SetKeyEvent(suppressList, INPUT_KEYBOARD, static_cast<WORD>(it->first), KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
ii.SendVirtualInput(suppressList);
}
}
if (remapToKey)

View File

@@ -226,6 +226,27 @@ namespace RemappingLogicTests
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
}
// Test if SendVirtualInput is sent exactly once with the suppress flag when a Ctrl/Alt/Shift key is remapped to a non-modifier key
TEST_METHOD (HandleSingleKeyRemapEvent_ShouldSendVirtualInputWithSuppressFlagExactlyOnce_WhenCtrlAltShiftIsMappedToNonModifierKey)
{
mockedInputHandler.SetSendVirtualInputTestHandler([](LowlevelKeyboardEvent* data) {
if (data->lParam->dwExtraInfo == KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG)
return true;
else
return false;
});
testState.AddSingleKeyRemap(VK_LMENU, (DWORD)VK_BACK);
std::vector<INPUT> inputs{
{ .type = INPUT_KEYBOARD, .ki = { .wVk = VK_LMENU } },
};
mockedInputHandler.SendVirtualInput(inputs);
Assert::AreEqual(1, mockedInputHandler.GetSendVirtualInputCallCount());
}
// Test if correct keyboard states are set for a single key to two key shortcut remap
TEST_METHOD (RemappedKeyToTwoKeyShortcut_ShouldSetTargetKeyState_OnKeyEvent)
{

View File

@@ -22,6 +22,8 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper
public string AppData { get; set; } = string.Empty;
public string SharedStorageDbPath { get; set; } = string.Empty;
public ImageSource WorkspaceIcon() => WorkspaceIconBitMap;
public ImageSource RemoteIcon() => RemoteIconBitMap;

View File

@@ -16,6 +16,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper
public static class VSCodeInstances
{
private static readonly string _userAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
private static readonly string _userProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
public static List<VSCodeInstance> Instances { get; set; } = new List<VSCodeInstance>();
@@ -129,6 +130,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper
var portableData = Path.Join(iconPath, "data");
instance.AppData = Directory.Exists(portableData) ? Path.Join(portableData, "user-data") : Path.Combine(_userAppDataPath, version);
instance.SharedStorageDbPath = GetSharedStorageDbPath(version, iconPath, Directory.Exists(portableData));
var vsCodeIconPath = Path.Join(iconPath, $"{version}.exe");
if (!File.Exists(vsCodeIconPath))
{
@@ -157,5 +159,30 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.VSCodeHelper
Instances.Add(instance);
}
}
private static string GetSharedStorageDbPath(string version, string iconPath, bool isPortable)
{
if (isPortable)
{
return Path.Join(iconPath, "data-shared", "sharedStorage", "state.vscdb");
}
var sharedStorageDirectory = version switch
{
"Code" => ".vscode-shared",
"Code - Insiders" => ".vscode-insiders-shared",
"Code - Exploration" => ".vscode-exploration-shared",
"VSCodium" => ".vscodium-shared",
"VSCodium - Insiders" => ".vscodium-insiders-shared",
_ => string.Empty,
};
if (string.IsNullOrEmpty(sharedStorageDirectory))
{
return string.Empty;
}
return Path.Combine(_userProfilePath, sharedStorageDirectory, "sharedStorage", "state.vscdb");
}
}
}

View File

@@ -97,6 +97,7 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper
// User/globalStorage/state.vscdb - history.recentlyOpenedPathsList - vscode v1.64 or later
var vscode_storage_db = Path.Combine(vscodeInstance.AppData, "User/globalStorage/state.vscdb");
var vscode_shared_storage_db = vscodeInstance.SharedStorageDbPath;
if (File.Exists(vscode_storage))
{
@@ -104,17 +105,37 @@ namespace Community.PowerToys.Run.Plugin.VSCodeWorkspaces.WorkspacesHelper
results.AddRange(storageResults);
}
if (File.Exists(vscode_storage_db))
var storageDbPaths = new[] { vscode_storage_db, vscode_shared_storage_db }
.Where(filePath => !string.IsNullOrEmpty(filePath))
.Distinct(StringComparer.OrdinalIgnoreCase);
foreach (var storageDbPath in storageDbPaths)
{
var storageDbResults = GetWorkspacesInVscdb(vscodeInstance, vscode_storage_db);
results.AddRange(storageDbResults);
if (File.Exists(storageDbPath))
{
var storageDbResults = GetWorkspacesInVscdb(vscodeInstance, storageDbPath);
results.AddRange(storageDbResults);
}
}
}
return results;
return results
.Where(workspace => workspace != null)
.GroupBy(GetWorkspaceKey, StringComparer.OrdinalIgnoreCase)
.Select(workspaceGroup => workspaceGroup.First())
.ToList();
}
}
private static string GetWorkspaceKey(VSCodeWorkspace workspace)
{
return string.Join(
"|",
workspace.VSCodeInstance?.ExecutablePath ?? string.Empty,
workspace.WorkspaceType,
workspace.Path ?? string.Empty);
}
private List<VSCodeWorkspace> GetWorkspacesInJson(VSCodeInstance vscodeInstance, string filePath)
{
var storageFileResults = new List<VSCodeWorkspace>();

View File

@@ -1,48 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Drivers;
namespace PowerDisplay.UnitTests;
[TestClass]
public class DisplayClassifierTests
{
[DataTestMethod]
// Internal: INTERNAL high-bit flag
[DataRow(0x80000000u, true, DisplayName = "INTERNAL bit only")]
[DataRow(0x8000000Bu, true, DisplayName = "INTERNAL | DISPLAYPORT_EMBEDDED")]
// Internal: documented embedded subtypes
[DataRow(11u, true, DisplayName = "DISPLAYPORT_EMBEDDED")]
[DataRow(13u, true, DisplayName = "UDI_EMBEDDED")]
// External: LVDS is not classified internal per docs
[DataRow(6u, false, DisplayName = "LVDS (not classified internal per docs)")]
// External: documented external connectors
[DataRow(5u, false, DisplayName = "HDMI")]
[DataRow(10u, false, DisplayName = "DISPLAYPORT_EXTERNAL")]
[DataRow(12u, false, DisplayName = "UDI_EXTERNAL")]
// External: virtual / wireless
[DataRow(15u, false, DisplayName = "MIRACAST")]
[DataRow(17u, false, DisplayName = "INDIRECT_VIRTUAL")]
// External: OTHER (-1) cast to uint
[DataRow(0xFFFFFFFFu, false, DisplayName = "OTHER (-1 cast to uint)")]
// External: unrecognized values default to external
[DataRow(0xDEADBEEFu, false, DisplayName = "Unknown value defaults to external")]
// External: INTERNAL flag combined with an undocumented subtype is treated as external
// (locks in the docstring's "INTERNAL | unknown subtype = external" rule).
[DataRow(0x80000007u, false, DisplayName = "INTERNAL | unknown subtype 7 (treated as external)")]
public void IsInternal_ReturnsExpectedClassification(uint outputTechnology, bool expected)
{
Assert.AreEqual(expected, DisplayClassifier.IsInternal(outputTechnology));
}
}

View File

@@ -1,62 +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 Microsoft.VisualStudio.TestTools.UnitTesting;
using PowerDisplay.Common.Services;
using static PowerDisplay.Common.Services.LinkedBrightnessPlanner;
namespace PowerDisplay.UnitTests;
/// <summary>
/// Behavior tests for pure linked-brightness decision logic. These cover review-flagged seed
/// cases without needing a WinUI DispatcherQueue.
/// </summary>
[TestClass]
public class LinkedBrightnessPlannerTests
{
private static LinkTarget Monitor(
string id,
int number,
int brightness)
=> new LinkTarget(id, number, brightness);
[TestMethod]
public void Seed_EmptyList_Null()
{
Assert.IsNull(LinkedBrightnessPlanner.Seed(new List<LinkTarget>()));
}
[TestMethod]
public void Seed_PrefersLowestDisplayNumber_RegardlessOfListOrder()
{
// Enumeration order is deliberately reversed; the seed must still come from Display 1.
var monitors = new[]
{
Monitor("c", 3, 90),
Monitor("a", 1, 30),
Monitor("b", 2, 60),
};
Assert.AreEqual(30, LinkedBrightnessPlanner.Seed(monitors));
}
[TestMethod]
public void Seed_UnknownDisplayNumbers_FallBackToIdOrder()
{
// MonitorNumber 0 means "unknown"; those sort last and tie-break by Id for determinism.
var monitors = new[]
{
Monitor("z", 0, 90),
Monitor("m", 0, 45),
};
Assert.AreEqual(45, LinkedBrightnessPlanner.Seed(monitors));
}
[TestMethod]
public void Seed_SingleControllableDisplay_UsesItsBrightness()
{
var monitors = new[] { Monitor("only", 1, 64) };
Assert.AreEqual(64, LinkedBrightnessPlanner.Seed(monitors));
}
}

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.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PowerDisplay.UnitTests;
/// <summary>
/// Covers the persisted shape of the linked-brightness feature on
/// <see cref="PowerDisplayProperties"/>: defaults, JSON property names, and — most importantly —
/// that settings written before the feature existed deserialize to safe defaults without any
/// migration step (the forward-compatibility promise made to the module owner).
/// </summary>
[TestClass]
public class LinkedBrightnessSettingsTests
{
[TestMethod]
public void Defaults_LinkDisabled_AndExclusionListEmptyButNotNull()
{
var properties = new PowerDisplayProperties();
Assert.IsFalse(properties.LinkedLevelsActive, "Linked brightness must default to off.");
Assert.IsNotNull(properties.ExcludedFromSyncMonitorIds, "Exclusion list must never be null.");
Assert.AreEqual(0, properties.ExcludedFromSyncMonitorIds.Count, "Exclusion list must start empty.");
}
[TestMethod]
public void Deserialize_LegacyJsonMissingLinkFields_UsesDefaultsWithoutMigration()
{
// A settings.json captured before the linked-brightness feature shipped: it has none of
// the new keys. Deserializing must fall back to the constructor defaults rather than
// produce nulls or throw — this is the "no migration needed" guarantee.
const string legacyJson = """
{
"monitor_refresh_delay": 5,
"restore_settings_on_startup": false,
"show_system_tray_icon": true
}
""";
var properties = JsonSerializer.Deserialize<PowerDisplayProperties>(legacyJson);
Assert.IsNotNull(properties);
Assert.IsFalse(properties.LinkedLevelsActive);
Assert.IsNotNull(properties.ExcludedFromSyncMonitorIds);
Assert.AreEqual(0, properties.ExcludedFromSyncMonitorIds.Count);
}
[TestMethod]
public void RoundTrip_PreservesLinkStateAndExclusionList()
{
var original = new PowerDisplayProperties
{
LinkedLevelsActive = true,
};
original.ExcludedFromSyncMonitorIds.Add(@"\\?\DISPLAY#DELD1A8#5&abc&0&UID4357");
original.ExcludedFromSyncMonitorIds.Add(@"\\?\DISPLAY#DELD1A8#5&abc&0&UID4358");
var json = JsonSerializer.Serialize(original);
var restored = JsonSerializer.Deserialize<PowerDisplayProperties>(json);
Assert.IsNotNull(restored);
Assert.IsTrue(restored.LinkedLevelsActive);
CollectionAssert.AreEqual(original.ExcludedFromSyncMonitorIds, restored.ExcludedFromSyncMonitorIds);
}
[TestMethod]
public void Serialize_UsesSnakeCaseJsonKeys()
{
var properties = new PowerDisplayProperties { LinkedLevelsActive = true };
properties.ExcludedFromSyncMonitorIds.Add("monitor-id");
var json = JsonSerializer.Serialize(properties);
StringAssert.Contains(json, "\"linked_levels_active\":true");
StringAssert.Contains(json, "\"excluded_from_sync_monitor_ids\"");
}
[TestMethod]
public void ExclusionList_DistinguishesIdenticalModelMonitorsByDevicePath()
{
// Two physically identical monitors share an EdidId (DELD1A8) but differ in the PnP UID
// segment of Monitor.Id. Keying the exclusion set by Monitor.Id keeps them distinct, which
// is the whole reason the issue's "three identical monitors" scenario works.
var properties = new PowerDisplayProperties();
properties.ExcludedFromSyncMonitorIds.Add(@"\\?\DISPLAY#DELD1A8#5&abc&0&UID4357");
Assert.IsTrue(properties.ExcludedFromSyncMonitorIds.Contains(@"\\?\DISPLAY#DELD1A8#5&abc&0&UID4357"));
Assert.IsFalse(
properties.ExcludedFromSyncMonitorIds.Contains(@"\\?\DISPLAY#DELD1A8#5&abc&0&UID4358"),
"A different physical port (UID) must not be treated as excluded.");
}
}

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