Compare commits

...

41 Commits

Author SHA1 Message Date
Muyuan Li (from Dev Box)
df1d6ea7e0 Fix ObtainInstaller offline regression, add missing test includes
- Move readyToInstall and upToDate state checks before the GitHub API
  call so already-downloaded installers can proceed when GitHub is
  unreachable
- Add missing <algorithm>, <iterator>, <vector> includes in
  UpdatingTests.cpp to avoid transitive-include breakage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-14 18:12:20 +08:00
Muyuan Li (from Dev Box)
31e30280de Style: remove extra blank line in Stage 2 relaunch block (PR #46889) 2026-05-14 08:58:55 +08:00
Clint Rutkas
5074588c9a Fix remaining pre-existing bugs D3-D7 in update system
D3 (#46965): Fix CommandLineToArgvW memory leak — added wil::scope_exit
    to LocalFree(args) on all exit paths.

D4 (#46966): Wait for PowerToys to exit after WM_CLOSE — Stage 1 now
    calls GetWindowThreadProcessId + WaitForSingleObject (10s timeout)
    before launching Stage 2, preventing file-in-use installer failures.

D5 (#46967): Use unique temp path — CopySelfToTempDir now appends PID
    to the temp filename, preventing collision on concurrent updates.

D6 (#46968): Remove dead code — deleted unused UPDATE_STAGE2_RESTART_PT
    and UPDATE_STAGE2_DONT_START_PT constants from UpdateUtils.h.

D7 (#46969): Restore configs on failed install — RestoreCorruptedConfigs
    now runs after Stage 2 regardless of success/failure, since a failed
    install may still corrupt config files.

All 30 tests pass locally (vstest.console.exe, x64 Release).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 20:12:46 -07:00
Clint Rutkas
df7a41b457 Fix critical pre-existing bugs in ObtainInstaller and Stage 2 args
D1 (CRITICAL): ObtainInstaller dereferenced *new_version_info via
std::holds_alternative BEFORE checking !new_version_info for errors.
When GitHub API is unreachable, this is undefined behavior / crash.
Fix: check !new_version_info first, return early on error.
All 3 independent code reviewers flagged this.

D2 (HIGH): Stage 2 WinMain used args[2] without checking nArgs >= 3.
If -update_now_stage_2 was invoked without an installer path, this
was an out-of-bounds array read.
Fix: add nArgs < 3 check before Stage 2 processing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 19:27:36 -07:00
Clint Rutkas
b0b073f088 Post-review fixes: add backup integrity check, detailed comments, cut/add tests
Multi-agent code review (Opus 4.6 + GPT-5.4) identified:
- B1: RestoreCorruptedConfigs now validates backup integrity before
  restoring — won't copy corrupted backup over corrupted original
- Cut 2 redundant tests (BackupCreatesConfigBackupDirectory,
  SimulateUpgradeWithNoCorruption)
- Added 4 new tests: RestoreSkipsDeletedOriginals,
  RestoreSkipsCorruptedBackup, BackupSkipsNonJsonFilesInModuleDirs,
  BackupEmptyRootDirSucceeds
- Merged CanRelaunch true/false into single CanRelaunchReflectsArgCount
- Added detailed comments to all 30 tests explaining what product
  code each tests and why
- BuildPowerToysExePath now uses fs::path for correctness
- TempDir uses unique paths per test instance

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 19:23:11 -07:00
Clint Rutkas
c56258d2d3 Extract update lifecycle logic and add Stage 1/2 handoff tests
The relaunch failure (#42004, #43011, #44071) was caused by Stage 2
never receiving the install directory and never relaunching PT.
Extract the argument-building and path-construction logic into
common/updating/updateLifecycle.h so it can be tested:

- BuildStage2Arguments: builds quoted command line with installer
  path + install dir (the install dir was completely missing before)
- BuildPowerToysExePath: constructs the PT.exe relaunch path
- CanRelaunchAfterUpdate: checks if Stage 1 provided the install dir

New UpdateLifecycleTests (8 tests):
- Stage 2 args contain both installer and install dir
- Both paths are properly quoted for spaces
- PT.exe path built correctly (with/without trailing backslash)
- CanRelaunch returns false for old Stage 1 (3 args = no install dir)
- CommandLineToArgvW round-trip: proves the exact Windows command
  line parsing produces the correct argv[2] and argv[3] values

All 29 tests built and pass locally (vstest.console.exe, x64 Release).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 22:56:41 -07:00
Clint Rutkas
236c241107 Add upgrade simulation tests, verify all 21 tests pass locally
Added UpgradeSimulationTests class with 3 end-to-end scenarios:
- SimulateUpgradeWithCorruption: 5 modules, 2 corrupted by installer,
  restore recovers corrupted files, leaves clean ones untouched
- SimulateUpgradeWithNoCorruption: clean install, user changes
  between backup and restore are preserved (not overwritten)
- SimulateUpgradeFromVeryOldVersion: old version with fewer modules,
  new modules created by installer are not affected by restore

All 21 tests built and run locally with vstest.console.exe:
  7 IsJsonFileCorrupted tests
  5 BackupConfigFiles tests
  5 RestoreCorruptedConfigs tests
  3 UpgradeSimulation tests (new)
  1 FullBackupAndRestoreRoundTrip

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 22:31:52 -07:00
Clint Rutkas
d6f61c8439 Fix linker errors: remove VersionHelper tests and strip external deps
VersionHelper tests caused LNK2001 because the test project didn't
link version.lib. Rather than adding the dependency, remove those
tests (already covered by UnitTests-CommonLib) and strip the project
to zero external deps:
- Remove CppWinRT NuGet (not needed for pure file I/O tests)
- Remove RuntimeObject.lib
- Remove WinRT includes from pch.h
- Remove packages.config
- Keep only configBackup tests (IsJsonFileCorrupted, Backup, Restore)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 15:58:22 -07:00
Clint Rutkas
b387bbffdd Fix CI failures: remove spdlog dependency from test, fix settings test default
1. Remove logger/SettingsAPI dependencies from configBackup.h so the
   test project compiles without spdlog NuGet. Logging is done at
   call sites in PowerToys.Update.cpp instead.
2. Fix SetSettingCommandTests: AutoDownloadUpdates test now sets
   'false' (not 'true') since the default changed to true.
3. Remove UpdateState test class that also needed spdlog-dependent
   headers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 15:20:24 -07:00
Clint Rutkas
5003c6c758 Add Updating.UnitTests project to PowerToys.slnx
Register the new test project in the solution so it is built and
run by CI (VSBuild Build;Test targets). The project name ends with
'Tests' so it is correctly skipped in release builds when
BuildTests=false.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-11 14:21:37 -07:00
Clint Rutkas
b2ef4c85bc Extract config backup to shared header and add unit tests
- Extract BackupConfigFiles, RestoreCorruptedConfigs, and
  IsJsonFileCorrupted into common/updating/configBackup.h so they
  can be tested independently and reused by other components
- Add path-parameterized overloads for test isolation
- Add Updating.UnitTests project (CppUnitTest framework) with:
  - IsJsonFileCorruptedTests: clean files, null bytes, large files,
    edge cases matching #46179 corruption pattern
  - BackupConfigFilesTests: directory creation, JSON-only copy,
    module subdirectories, skips Updates/ConfigBackup dirs
  - RestoreCorruptedConfigsTests: selective restore of corrupted
    files, leaves clean files untouched, full round-trip test
  - UpdateState and VersionHelper serialization sanity checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 22:41:58 -07:00
Clint Rutkas
24d56f0524 Fix auto-update relaunch, add config backup, enable auto-download by default
Addresses three critical issues with the PowerToys update experience:

1. Fix relaunch after update (#42004, #43011, #44071):
   - Stage 1 now passes the install directory to Stage 2
   - Stage 2 relaunches PowerToys.exe with -report_update_success
     after a successful install, so PT restarts automatically

2. Add config backup/restore to prevent data corruption (#46179):
   - BackupConfigFiles() snapshots all JSON configs before update
   - RestoreCorruptedConfigs() detects null-byte corruption after
     install and restores from backup automatically

3. Enable auto-download by default (GeneralSettings.cs):
   - New installations now default to AutoDownloadUpdates=true
   - Existing user preferences are preserved (read from settings.json)
   - Combined with the relaunch fix, this means most users will
     seamlessly stay current without manual intervention

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 22:24:12 -07:00
moooyo
0089de33bd [PD] Re-enable PowerDisplay (#46489)
<!-- 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
1. Re-enable PowerDisplay for PowerToys.
2. Add PowerDisplay back into installer.
3. Use new PowerDisplay icon and logo.
4. Fix some DPI related issue.
5. UI/UX improvement.


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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Pull new code from this branch. Set up PowerDisplay.UI as startup
project. Click run in VS.

Or, build whole solution, set up runner as startup project. Click run to
test full experience.

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-04-10 15:14:41 +08:00
Copilot
3e2914a0b2 Add unit tests for Hosts ValidationHelper and ColorPicker format conversions (#46679)
## Summary of the Pull Request

Adds comprehensive unit tests for two previously untested areas to
improve test coverage and prevent regressions:

1. **Hosts ValidationHelper** (`ValidationHelperTest.cs`) — 25+ test
cases covering `ValidIPv4`, `ValidIPv6`, and `ValidHosts` methods
2. **ColorPicker ColorFormatHelper conversions**
(`ColorFormatConversionTest.cs`) — 50+ test cases covering CMYK, HSB,
HSI, HWB, CIE XYZ, CIE LAB, Oklab, Oklch, sRGB-to-linear, NCol
conversions, plus expanded `GetStringRepresentation` tests for Red,
White, Green, and Blue colors across all supported formats

## PR Checklist

- [x] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized —
N/A (test-only changes)
- [ ] **Dev docs:** Added/updated — N/A (test-only changes)
- [ ] **New binaries:** Added on the required places — N/A (no new
binaries)

## Detailed Description of the Pull Request / Additional comments

### Hosts ValidationHelper
(`src/modules/Hosts/Hosts.Tests/ValidationHelperTest.cs`)
- Tests `ValidIPv4` with valid addresses (loopback, private ranges,
broadcast), invalid addresses (out of range octets, wrong format, CIDR
notation), and null/whitespace inputs
- Tests `ValidIPv6` with valid addresses (loopback, full/compressed
notation, link-local, IPv4-mapped), invalid addresses (extra groups,
invalid hex digits), and null/whitespace inputs
- Tests `ValidHosts` with valid hostnames, FQDNs, max host count
boundary (using `Consts.MaxHostsCount` dynamically for both
exact-boundary and exceeds-boundary tests), and invalid hostnames

### ColorPicker Format Conversions
(`src/modules/colorPicker/ColorPickerUI.UnitTests/Helpers/ColorFormatConversionTest.cs`)
- Tests `ConvertToCMYKColor` for Black, White, Red, Green, Blue, and Mid
Gray
- Tests `ConvertToHSBColor`, `ConvertToHSIColor`, `ConvertToHWBColor`
for primary colors
- Tests `ConvertToCIEXYZColor` and `ConvertToCIELABColor` including D65
illuminant verification and negative b* assertion for Blue
- Tests `ConvertToOklabColor` and `ConvertToOklchColor` including chroma
non-negativity
- Tests `ConvertSRGBToLinearRGB` for linear and gamma paths
- Tests `ConvertToNaturalColor` for hue letter mapping (R exact match, G
and B prefix assertions)
- Tests `GetStringRepresentation` with Red, White, Green, Blue across
all 11+ format types (Decimal values use BGR byte order via `%Dv`
format: Red → "255", Blue → "16711680")
- Tests `GetDefaultFormat` returns non-empty strings for all known
format names
- Tests edge cases: empty/null format strings defaulting to hex output

### Spell-check allow list
- Added `SRGBTo` to `.github/actions/spell-check/allow/code.txt` to
resolve unrecognized-spelling alerts

### StyleCop / code analysis fixes
- Resolved SA1512, SA1515, CA1866, and CA1310 analyzer warnings to
comply with repo coding standards

### Code review fixes
- Renamed `ConvertToCIELAB_Blue_HasNegativeA` →
`ConvertToCIELAB_Blue_HasNegativeB` with corrected comment to match the
actual b* axis assertion
- Replaced hardcoded 12-host string with dynamic `Consts.MaxHostsCount +
1` in the exceeds-max-count boundary test
- Renamed `ConvertToNaturalColor_Green_ReturnsG0` →
`ConvertToNaturalColor_Green_HueStartsWithG` and
`ConvertToNaturalColor_Blue_ReturnsB0` →
`ConvertToNaturalColor_Blue_HueStartsWithB` to accurately reflect
prefix-only assertions

## Validation Steps Performed

- Verified test files follow existing MSTest patterns (`[TestClass]`,
`[TestMethod]`, `[DataTestMethod]`, `[DataRow]`)
- Verified all referenced classes and methods exist and are accessible
(correct namespaces and visibility)
- Verified namespace consistency with existing test files in each
project
- Used dynamic `Consts.MaxHostsCount` rather than hardcoded values for
all boundary tests
- Verified Decimal format expected values match the `%Dv` (BGR order)
implementation: `R + G*256 + B*65536`
- Verified test method names accurately describe their assertions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: crutkas <1462282+crutkas@users.noreply.github.com>
2026-04-10 09:07:48 +02:00
Dustin L. Howett
3554f0884b spelling: move to v0.0.26 (#46851)
This fixes, among other things, the issue with fork PRs.

---------

Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2026-04-10 06:43:27 +02:00
Jaylyn Barbee
ad60090096 [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 09:58:10 -05:00
Niels Laute
de6a609d16 [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 10:45:25 -04:00
Rin
243255ecea Fix quote breakout in launcher shell plugin commands (#45554)
## Summary of the Pull Request
Fixes a command breakout vulnerability in the Shell plugin where user
input containing double quotes could be manipulated to execute arbitrary
sub-processes. By escaping double quotes, inputs like `test" & calc.exe`
are treated as literal strings rather than shell command separators.

## PR Checklist
- [ ] Closes: #xxx
- [x] **Communication:** Proactive fix for command execution logic.
- [x] **Tests:** Verified locally.
- [x] **Localization:** N/A
- [x] **Dev docs:** N/A
- [x] **New binaries:** N/A
- [x] **Documentation updated:** N/A

## Detailed Description of the Pull Request / Additional comments
Escaping double quotes in the command string before it is passed to the
shell prevents the breakout vector I identified while still allowing
environment variables to expand as expected. I also removed some
redundant property assignments in the shell helpers to keep the logic
focused on the core fix.

## Validation Steps Performed
Standard diagnostic commands like `ping` and `ipconfig` continue to
function as expected via PowerToys Run. Malicious strings designed to
break out of quotes now fail to execute sub-commands, as confirmed
during local verification.

---------

Co-authored-by: LegendaryBlair <legendaryblair@icloud.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 16:31:24 +08:00
Michael Jolley
1e1bd07087 Add CmdPalLogger, Provider, and extension method (#46768)
This pull request introduces a new logging infrastructure for the CmdPal
module by integrating the Microsoft.Extensions.Logging (MEL) abstraction
and routing all log output through the existing `ManagedCommon.Logger`.
The changes add a custom logger, logger provider, and extension method
for easy registration, and update dependencies and service configuration
to enable the new logging system.

> This logging is not in use currently, but will be in a future PR.

**Logging Infrastructure Integration:**

* Added a new `CmdPalLogger` class implementing `ILogger`, which
delegates logging calls to `ManagedCommon.Logger` to centralize and
standardize log output.
* Implemented `CmdPalLoggerProvider` to create and manage `CmdPalLogger`
instances, allowing MEL-based logging throughout the application.
* Introduced `CmdPalLoggingExtensions.AddCmdPalLogging` for registering
the logger provider via dependency injection, ensuring all MEL logging
is routed appropriately.
* Updated `App.xaml.cs` to register the new logging system with
`services.AddCmdPalLogging()`, enabling the infrastructure at
application startup.
[[1]](diffhunk://#diff-84386fa8a23e7058525bd269788bbf9e352b1f49d08e5f877059386ba3b83222R8)
[[2]](diffhunk://#diff-84386fa8a23e7058525bd269788bbf9e352b1f49d08e5f877059386ba3b83222R129-R130)

**Project and Dependency Updates:**

* Updated the project file `Microsoft.CmdPal.Common.csproj` to reference
the `ManagedCommon` project and to include a folder for the new logging
code.
[[1]](diffhunk://#diff-affab7e2df96d3b8073ab649e4ef5a34d459cd69e525573fd6426d698efec18fR29)
[[2]](diffhunk://#diff-affab7e2df96d3b8073ab649e4ef5a34d459cd69e525573fd6426d698efec18fR64-R67)
2026-04-04 03:14:08 +02:00
Niels Laute
32b4080007 [CmdPal Extension Template] Adding skills and instructions (#46683)
## Summary of the Pull Request

Adds Copilot instructions and skills to the CmdPal extension template so
that when developers create a new extension via "Create a new
extension", the generated project includes AI-assisted development
guidance out of the box.

I verified the skills on my own extension:

<img
src="https://github.com/user-attachments/assets/24bddefd-f38a-4faa-aaf0-686bcb891241">

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

### Problem
When developers create a new Command Palette extension, the generated
project contains only source code and build configuration — no
AI-assisted development guidance. This means Copilot and other AI tools
have no context about CmdPal extension APIs, patterns, or publishing
workflows.

### Solution
Added **2 instruction files** and **5 skills** (11 markdown files total)
to the extension template at
`src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/.github/`:

**Instructions:**
| File | Purpose |
|------|---------|
| `copilot-instructions.md` | Top-level project overview: structure,
conventions, build/deploy workflow, skill inventory |
| `instructions/cmdpal-extension.instructions.md` | Comprehensive
353-line API reference covering extension architecture, all page types,
content types, commands/results, items, icons, dynamic updates, and
debugging |

**Skills:**
| Skill | Description | Why |
|-------|-------------|-----|
| `publish-extension` | Microsoft Store (MSIX) + WinGet (EXE) + GitHub
Actions automation | Every extension eventually needs distribution |
| `add-adaptive-card-form` | Adaptive Cards Designer workflow + template
JSON patterns | Forms are the primary way to collect user input |
| `add-extension-settings` | ToggleSetting / TextSetting /
ChoiceSetSetting + persistence | 12 of 20 built-in extensions use
settings |
| `add-dock-band` | Single/multi-button bands, WrappedDockItem,
live-updating | Enables persistent toolbar widgets |
| `add-fallback-commands` | FallbackCommands() + DynamicListPage +
CancellationToken | 14 of 20 built-in extensions use fallback commands |

### Code changes
- **`ExtensionTemplateService.cs`** — Added `.md` to
`_copyAsIsTemplateExtensions` so markdown files are properly handled
during template extraction
- **`template.zip`** — Regenerated to include the new `.github/`
directory (19KB → 44KB)
- **`.github/actions/spell-check/expect.txt`** — Added 17 Inno Setup
constants/flags, placeholder identifiers, and technical tokens from the
new skill files to the spell-check allowlist

### Content sources
- Instructions derived from the **SamplePagesExtension** (all 20+ sample
pages), the **CmdPal extension SDK** (IDL + toolkit base classes), and
**official MS Learn documentation**
- Publishing skill based on the [Publish Command Palette
extensions](https://learn.microsoft.com/windows/powertoys/command-palette/publishing-your-extension)
docs
- Patterns verified against all 20 real CmdPal extensions in the repo

## Validation Steps Performed

1.  All 7 `ExtensionTemplateServiceTests` pass:
   - `CreateExtension_BuildsExtensionFromTemplateArchive`
   - `CopyTemplateFile_RewritesTextFiles`
   - `CopyTemplateFile_CopiesUnchangedTextFilesVerbatim`
   - `CopyTemplateFile_CopiesBinaryFilesWithoutRewritingContents`
   - `TemplateFileHandling_ThrowsForUnknownExtension`
- `TemplateExtensionCategories_AreDisjointAndCoverTemplateZip` —
validates `.md` is in the extension lists
- `TemplateZipFiles_AllUseKnownHandling` — validates all zip entries
have known handling
2.  Verified `template.zip` contains all 31 entries (20 original + 11
new `.md` files)
3.  Verified directory structure is preserved correctly in the zip
4.  Added 17 flagged tokens to `expect.txt` to resolve `check-spelling`
CI failures — all are valid technical terms (Inno Setup constants/flags,
domain names, Windows environment variables, code example placeholders)
that appear inside code blocks in the skill documentation
5.  `check-spelling` CI passes with 0 new misspelled words found on the
latest commit

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-04-03 21:47:10 +00:00
Jiří Polášek
47c1fb5418 Spelling: Add names added in #46582 to the spellchecker dictionary (#46765)
<!-- 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

See title

Ref: #46582

<!-- 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-04-03 21:00:36 +00:00
Copilot
8ee3d64667 CmdPal: Fix inline code text color in Details panel ignoring theme (#46739)
## Summary of the Pull Request

Inline code (backtick-wrapped text) in the Details panel renders white
regardless of theme, making it invisible on light backgrounds. The
`DefaultMarkdownThemeConfig` in CmdPal was missing `InlineCode*` styling
properties, so the toolkit's `MarkdownTextBlock` fell back to
non-theme-aware defaults. The Settings UI already had this configured
correctly.

Fixes: #46734

## PR Checklist

- [x] **Tests:** XAML-only change; no behavioral logic affected
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

## Detailed Description of the Pull Request / Additional comments

Added theme-aware `InlineCode*` properties to
`DefaultMarkdownThemeConfig` in two files, matching the existing pattern
from `Settings.UI/App.xaml`:

- **`ShellPage.xaml`** — Details panel (the reported bug)
- **`ContentPage.xaml`** — Content page markdown (consistency)

Properties added:
```xml
InlineCodeForeground="{StaticResource TextFillColorSecondaryBrush}"
InlineCodeBackground="{StaticResource ControlFillColorDefaultBrush}"
InlineCodeBorderBrush="{StaticResource ControlElevationBorderBrush}"
InlineCodeCornerRadius="2"
InlineCodePadding="2,0,2,1"
```

These `StaticResource` brushes resolve per-theme
(Light/Dark/HighContrast) automatically.

## Validation Steps Performed

- Verified the Settings UI already uses this exact pattern in `App.xaml`
(`DescriptionTextMarkdownThemeConfig`) and renders inline code correctly
across themes.
- Confirmed no other `MarkdownThemes` definitions exist in the CmdPal
module.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: michaeljolley <1228996+michaeljolley@users.noreply.github.com>
2026-04-03 21:29:32 +02:00
Michael Jolley
51c9bc4930 CmdPal: give each built-in extension its own settings file (#46685)
## Summary of the Pull Request

Each built-in CmdPal extension was reading and writing settings directly
to a shared `settings.json` file, which could be silently overwritten by
the persistence service. This PR gives each extension its own
`{namespace}.settings.json` file and adds transparent migration from the
legacy shared file.

## PR Checklist

- [x] Closes: #46667
- [x] **Communication:** I've discussed this with core contributors
already.
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** N/A — no user-facing strings changed
- [ ] **Dev docs:** N/A — internal implementation detail
- [ ] **New binaries:** N/A — no new binaries

## Detailed Description of the Pull Request / Additional comments

### Core change — `JsonSettingsManager` (extensions SDK)

Added `MigrateFromLegacyFile(string legacyFilePath)` to
`src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSettingsManager.cs`:

- Skips if the per-extension file already exists (idempotent).
- Reads the legacy shared `settings.json`, extracts only the keys the
current extension owns via `Settings.Update()`, and writes them to the
new per-extension path.
- Logs on failure without throwing.

### Per-extension changes (11 SettingsManager files)

Each built-in extension's `SettingsJsonPath()` now returns
`{namespace}.settings.json` instead of `settings.json`, and a new
`LegacySettingsJsonPath()` helper preserves the old path for migration:

- `Microsoft.CmdPal.Ext.Apps` — `AllAppsSettings.cs`
- `Microsoft.CmdPal.Ext.Calc` — `Helper/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.ClipboardHistory` — `Helpers/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.Registry` — `Helpers/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.RemoteDesktop` — `Settings/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.Shell` — `Settings/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.System` — `Helpers/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.TimeDate` — `Helpers/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.WebSearch` — `Helpers/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.WindowWalker` — `Helpers/SettingsManager.cs`
- `Microsoft.CmdPal.Ext.WindowsTerminal` — `Helpers/SettingsManager.cs`

## Validation Steps Performed

- Built the solution and confirmed all 11 extensions compile cleanly.
- Launched CmdPal with an existing shared `settings.json` containing
settings for multiple extensions. Verified each extension created its
own `{namespace}.settings.json` and loaded the correct values.
- Confirmed subsequent launches skip migration (per-extension file
already exists).
- Verified the persistence service no longer overwrites
extension-specific settings.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-03 19:25:34 +00:00
dependabot[bot]
ddff66c088 Chore(deps): Bump azure/login from 2 to 3 (#46323)
Bumps [azure/login](https://github.com/azure/login) from 2 to 3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/azure/login/releases">azure/login's
releases</a>.</em></p>
<blockquote>
<h2>Azure Login Action v3</h2>
<h2>What's Changed</h2>
<ul>
<li>upgrade nodejs from 20 to 24 and update dependencies by <a
href="https://github.com/YanaXu"><code>@​YanaXu</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/578">Azure/login#578</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/login/compare/v2.3.0...v3">https://github.com/Azure/login/compare/v2.3.0...v3</a></p>
<h2>Azure Login Action v3.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Upgrade nodejs from 20 to 24 and update dependencies by <a
href="https://github.com/YanaXu"><code>@​YanaXu</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/578">Azure/login#578</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/login/compare/v2.3.0...v3.0.0">https://github.com/Azure/login/compare/v2.3.0...v3.0.0</a></p>
<h2>Azure Login Action v2.3.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Replace the invalid link for the GitHub Action Doc by <a
href="https://github.com/MoChilia"><code>@​MoChilia</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/510">Azure/login#510</a></li>
<li>Bump braces from 3.0.2 to 3.0.3 by <a
href="https://github.com/YanaXu"><code>@​YanaXu</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/511">Azure/login#511</a></li>
<li>Mention &quot;allow-no-subscriptions&quot; in missing subscriptionId
error by <a
href="https://github.com/MoChilia"><code>@​MoChilia</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/512">Azure/login#512</a></li>
<li>Log more claims for OIDC login by <a
href="https://github.com/MoChilia"><code>@​MoChilia</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/520">Azure/login#520</a></li>
<li>Use <code>--client-id</code> for user-assigned managed identity
authentication in Azure CLI v2.69.0 or later. by <a
href="https://github.com/MoChilia"><code>@​MoChilia</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/514">Azure/login#514</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/login/compare/v2.2.0...v2.3.0">https://github.com/Azure/login/compare/v2.2.0...v2.3.0</a></p>
<h2>Azure Login Action v2.2.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Replace <code>az --version</code> with <code>az version</code> by <a
href="https://github.com/MoChilia"><code>@​MoChilia</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/450">Azure/login#450</a></li>
<li>Update documentation for setting audience when environment is set by
<a href="https://github.com/jcantosz"><code>@​jcantosz</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/455">Azure/login#455</a></li>
<li>Fix <a
href="https://redirect.github.com/azure/login/issues/459">#459</a>:
Errors when registering cloud profile for AzureStack by <a
href="https://github.com/MoChilia"><code>@​MoChilia</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/466">Azure/login#466</a></li>
<li>Fix typo by <a
href="https://github.com/KronosTheLate"><code>@​KronosTheLate</code></a>
in <a
href="https://redirect.github.com/Azure/login/pull/483">Azure/login#483</a></li>
<li>move pre cleanup to main and add pre-if and post-if by <a
href="https://github.com/YanaXu"><code>@​YanaXu</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/484">Azure/login#484</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/jcantosz"><code>@​jcantosz</code></a>
made their first contribution in <a
href="https://redirect.github.com/Azure/login/pull/455">Azure/login#455</a></li>
<li><a
href="https://github.com/KronosTheLate"><code>@​KronosTheLate</code></a>
made their first contribution in <a
href="https://redirect.github.com/Azure/login/pull/483">Azure/login#483</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/login/compare/v2.1.1...v2.2.0">https://github.com/Azure/login/compare/v2.1.1...v2.2.0</a></p>
<h2>v2.1.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Disable information output in Connect-AzAccount by <a
href="https://github.com/YanaXu"><code>@​YanaXu</code></a> in <a
href="https://redirect.github.com/Azure/login/pull/448">Azure/login#448</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/jiasli"><code>@​jiasli</code></a> made
their first contribution in <a
href="https://redirect.github.com/Azure/login/pull/438">Azure/login#438</a></li>
<li><a href="https://github.com/isra-fel"><code>@​isra-fel</code></a>
made their first contribution in <a
href="https://redirect.github.com/Azure/login/pull/446">Azure/login#446</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/login/compare/v2.1.0...v2.1.1">https://github.com/Azure/login/compare/v2.1.0...v2.1.1</a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="532459ea53"><code>532459e</code></a>
prepare release v3.0.0</li>
<li><a
href="893aa84218"><code>893aa84</code></a>
upgrade Azure Login Action version in README (<a
href="https://redirect.github.com/azure/login/issues/579">#579</a>)</li>
<li><a
href="ce6a9ff965"><code>ce6a9ff</code></a>
upgrade nodejs from 20 to 24 and update dependencies (<a
href="https://redirect.github.com/azure/login/issues/578">#578</a>)</li>
<li>See full diff in <a
href="https://github.com/azure/login/compare/v2...v3">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 10:11:05 +00:00
moooyo
e28ed8a566 Revert "Pin check-spelling action to v0.0.26 (a35147f)" (#46749)
Reverts microsoft/PowerToys#46746

ok, gordon said this change is unrelated to the pipeline,  revert it.
2026-04-03 09:20:33 +00:00
moooyo
4f693778f2 Pin check-spelling action to v0.0.26 (a35147f) (#46746)
<!-- 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
discussed here:
https://github.com/check-spelling/check-spelling/issues/103

Merge it first to test if we can fix this issue.

It blocked our PR pipeline which created from the forked repo.

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

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

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

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

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 07:42:38 +00:00
oxygen dioxide
e1ad13ab34 Peek: Auto detect file name encoding when previewing zip file (#44799)
<!-- 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
Auto detect file name encoding when previewing zip file

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

- [x] Closes: #44790
<!-- - [ ] 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
- [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
The encoding of file names in zip files defaults to the native encoding
of the creator's OS. For example, a zip file created on a zh-CN Windows
PC uses GBK. However, currently peek always uses UTF-8 when opening zip
file, resulting in garbled text.
Here I added an auto-detection mechanism in peek to support different
zip filename encodings.

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

1. Turn on peek
2. Download this file:
[chinese-example.zip](https://github.com/user-attachments/files/24155422/chinese-example.zip)
3. Select this file and press Ctrl+Space

Previous behaviour (incorrect):

<img width="1964" height="1326" alt="Image"
src="https://github.com/user-attachments/assets/2d331647-5761-4331-97ba-4c4c01132afb"
/>

Current behaviour (correct):

<img width="2026" height="1269" alt="图片"
src="https://github.com/user-attachments/assets/db456426-f7f6-467c-8f3c-1e01cba44fec"
/>
2026-04-03 14:52:16 +08:00
adelobosko
cea0497bb9 Refactor PadImage to use out param and improve disposal (#44906)
Refactored PadImage to return a bool and use an out parameter for the
padded bitmap, with [NotNullWhen(true)] for nullability. Updated
GetWindowBoundsImage to handle disposal of the original bitmap when
padding is applied, improving memory management and code clarity.

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

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-04-03 01:09:44 +00:00
Clint Rutkas
fd5be6d04e Fix SA1614: Add text to empty parameter documentation (#46706)
Add meaningful descriptions to 3 empty param XML doc tags in
BaseDscTest.cs and ShellHelpers.cs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 03:41:59 +00:00
Clint Rutkas
4dfdf46e0d Fix WMC1506 XAML compiler warnings in AdvancedPaste (#46726)
## Summary

Fixes all 13 **WMC1506** XAML compiler warnings ("OneWay bindings
require at least one of their steps to support raising notifications
when their value changes") by changing \Mode=OneWay\ to \Mode=OneTime\
on \x:Bind\ expressions bound to non-observable properties.

## Details

**Root cause:** \PasteFormat\ (plain sealed class) and \ClipboardItem\
(plain class) do not implement \INotifyPropertyChanged\. Using
\Mode=OneWay\ on their properties creates subscriptions that will never
fire, generating WMC1506 warnings.

**Fix:** Changed to \Mode=OneTime\ which is semantically correct — these
properties are set once and never change after construction.

**Files changed:**
- \ClipboardHistoryItemPreviewControl.xaml\ — 2 bindings (\Header\,
\Timestamp\)
- \MainPage.xaml\ — 11 bindings across \PasteFormat\ and \ClipboardItem\
DataTemplates

**Note on ClipboardHistoryItemPreviewControl:** Its computed properties
(\Header\, \Timestamp\) are refreshed via \Bindings.Update()\ when the
\ClipboardItem\ DependencyProperty changes. \Bindings.Update()\ forces
re-evaluation of all \x:Bind\ bindings regardless of mode, so \OneTime\
works correctly here.

## Validation

- [x] Full solution build passes (exit code 0)
- [x] Zero WMC1506 warnings after changes (was 13 before)
- [x] No behavioral changes — only binding mode optimization

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 00:52:57 +02:00
Jay
1c4ecc23c6 Cleanup md files (root folder) (#46582)
<!-- 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

Cleaning up Markdown files, including:

- [Learn Authoring
Pack](https://marketplace.visualstudio.com/items?itemName=docsmsft.docs-authoring-pack)
in Visual Studio Code
- consolidating list item bullets
- spelling and grammar
- HTML tables and links to Markdown

To do:

- [x] Sentence casing in headers
https://learn.microsoft.com/en-us/style-guide/capitalization#sentence-style-capitalization-in-titles-and-headings
      (Copilot quotum was reached 🤓)
- [ ] NOTICE.md: text in code blocks or not??

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-04-01 21:08:38 +00:00
dependabot[bot]
5888f6eb7f Chore(deps): Bump azure/cli from 2 to 3 (#46562)
Bumps [azure/cli](https://github.com/azure/cli) from 2 to 3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/azure/cli/releases">azure/cli's
releases</a>.</em></p>
<blockquote>
<h2>GitHub Action for Azure CLI v3</h2>
<h2>What's Changed</h2>
<ul>
<li>Updated to use node24 by <a
href="https://github.com/thomas-temby"><code>@​thomas-temby</code></a>
in <a
href="https://redirect.github.com/Azure/cli/pull/197">Azure/cli#197</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/cli/compare/v2.2.0...v3">https://github.com/Azure/cli/compare/v2.2.0...v3</a></p>
<h2>GitHub Action for Azure CLI v3.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Updated to use node24 by <a
href="https://github.com/thomas-temby"><code>@​thomas-temby</code></a>
in <a
href="https://redirect.github.com/Azure/cli/pull/197">Azure/cli#197</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/cli/compare/v2.2.0...v3.0.0">https://github.com/Azure/cli/compare/v2.2.0...v3.0.0</a></p>
<h2>GitHub Action for Azure CLI v2.2.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Mount <code>AZURE_CONFIG_DIR</code> folder instead of
<code>~/.azure</code> by <a
href="https://github.com/MoChilia"><code>@​MoChilia</code></a> in <a
href="https://redirect.github.com/Azure/cli/pull/176">Azure/cli#176</a></li>
<li>FIX: Broken links by appended dot azcliversion errors by <a
href="https://github.com/nselpriv"><code>@​nselpriv</code></a> in <a
href="https://redirect.github.com/Azure/cli/pull/178">Azure/cli#178</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/nselpriv"><code>@​nselpriv</code></a>
made their first contribution in <a
href="https://redirect.github.com/Azure/cli/pull/178">Azure/cli#178</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/cli/compare/v2.1.0...v2.2.0">https://github.com/Azure/cli/compare/v2.1.0...v2.2.0</a></p>
<h2>GitHub Action for Azure CLI v2.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>docs: add yaml syntax highlighting to workflow examples by <a
href="https://github.com/baysideengineer"><code>@​baysideengineer</code></a>
in <a
href="https://redirect.github.com/Azure/cli/pull/141">Azure/cli#141</a></li>
<li>Fix <a
href="https://redirect.github.com/azure/cli/issues/153">#153</a>:
Prevent stdout cutoff in Azure CLI versions by <a
href="https://github.com/MoChilia"><code>@​MoChilia</code></a> in <a
href="https://redirect.github.com/Azure/cli/pull/154">Azure/cli#154</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/baysideengineer"><code>@​baysideengineer</code></a>
made their first contribution in <a
href="https://redirect.github.com/Azure/cli/pull/141">Azure/cli#141</a></li>
<li><a href="https://github.com/isra-fel"><code>@​isra-fel</code></a>
made their first contribution in <a
href="https://redirect.github.com/Azure/cli/pull/151">Azure/cli#151</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Azure/cli/compare/v2.0.0...v2.1.0">https://github.com/Azure/cli/compare/v2.0.0...v2.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Azure/cli/blob/master/ReleaseProcess.md">azure/cli's
changelog</a>.</em></p>
<blockquote>
<p><strong>Releasing a new version</strong></p>
<p>Semanting versioning is used to release different versions of the
action. Following steps are to be followed :</p>
<ol>
<li>Create a new branch for every major version. <br />
Example, releases/v1, releases/v2.</li>
<li>For every minor and patch release for a major version, update the
corresponding release branch. <br />
Example, for releasing v1.1.1, update releases/v1.</li>
<li>Create tags for every new release (major/minor/patch). <br />
Example,v1.0.0. , v1.0.1, v2.0.1, etc. and also have tags like v1, v2
for every major version release.</li>
<li>On releasing minor and patch versions, update the tag of the
corresponding major version. <br />
Example, for releasing v1.0.1, update the v1 tag to point to the ref of
the current release. <br />
The following commands are to be run on the release\v1 branch so that it
picks the latest commit and updates the v1 tag accordingly :
(Ensure that you are on same commit locally as you want to release)</li>
</ol>
<ul>
<li><code>git tag -fa v1 -m &quot;Update v1 tag&quot;</code></li>
<li><code>git push origin v1 --force</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9eb25b8360"><code>9eb25b8</code></a>
Release v3.0.0 (<a
href="https://redirect.github.com/azure/cli/issues/199">#199</a>)</li>
<li><a
href="c1ad80439a"><code>c1ad804</code></a>
Add changes (<a
href="https://redirect.github.com/azure/cli/issues/198">#198</a>)</li>
<li><a
href="41fca1b4f8"><code>41fca1b</code></a>
Updated to use node24 (<a
href="https://redirect.github.com/azure/cli/issues/197">#197</a>)</li>
<li><a
href="cbea6ec14d"><code>cbea6ec</code></a>
change the assignee (<a
href="https://redirect.github.com/azure/cli/issues/191">#191</a>)</li>
<li>See full diff in <a
href="https://github.com/azure/cli/compare/v2...v3">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 22:22:51 +02:00
Clint Rutkas
152f64151b Fix MSTEST0017: Correct assertion argument order (#46712)
Swap expected/actual arguments in 22 Assert calls to follow the correct
MSTest convention of Assert.AreEqual(expected, actual).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 13:15:14 -07:00
Clint Rutkas
76b773b016 Fix SA1616: Add text to empty return value documentation (#46718)
Add meaningful descriptions to 6 empty returns XML doc tags across DSC,
CmdPal, and Extensions.Toolkit.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 21:58:09 +02:00
Copilot
0da5602f68 CmdPal: Update CommunityToolkit.WinUI to 8.2.251219 and remove SearchBar debouncer hacks (#46027)
## Summary of the Pull Request

Updates all `CommunityToolkit.WinUI` packages from `8.2.250402` to
`8.2.251219` (latest stable) and removes three workaround hacks from
`SearchBar.xaml.cs` that were added to paper over bugs in the
`CommunityToolkit.WinUI.Extensions` debouncer (`Debounce` with
`immediate: true` not firing correctly). Those bugs were fixed upstream
and are included in `8.2.251219`.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

### Package update (`Directory.Packages.props`)

All `CommunityToolkit.WinUI` packages bumped from `8.2.250402` →
`8.2.251219`:

- `CommunityToolkit.WinUI.Animations`
- `CommunityToolkit.WinUI.Collections`
- `CommunityToolkit.WinUI.Controls.Primitives`
- `CommunityToolkit.WinUI.Controls.SettingsControls`
- `CommunityToolkit.WinUI.Controls.Segmented`
- `CommunityToolkit.WinUI.Controls.Sizers`
- `CommunityToolkit.WinUI.Converters`
- `CommunityToolkit.WinUI.Extensions`

### Hack removals (`SearchBar.xaml.cs`)

All three hacks were in `SearchBar.xaml.cs` (`Controls/`), tagged `TODO
GH #245`:

- **`FilterBox_TextChanged` — "TERRIBLE HACK"**: Forced
`DoFilterBoxUpdate()` immediately for any single-character input, then
returned early—bypassing the debouncer entirely. Now the debouncer's
`immediate: FilterBox.Text.Length <= 1` path handles this correctly.

- **Escape key handler**: After `FilterBox.Text = string.Empty`,
manually pushed the empty string to
`CurrentPageViewModel.SearchTextBox`. The `TextChanged` event fires
after the assignment and the debouncer (with `immediate: true` for
length 0) now handles propagation.

- **Backspace key handler (`else if (e.Key == VirtualKey.Back)`
block)**: Pre-emptively set `CurrentPageViewModel.SearchTextBox` to the
*pre-deletion* text in `FilterBox_KeyDown`. Entire block removed;
`TextChanged` + debouncer handle the post-deletion update correctly.

## Validation Steps Performed

Manually verified in CmdPal that:
- Typing aliases (single-character triggers) still activates filtering
immediately
- Pressing Escape clears the search box and resets the filter
- Pressing Backspace correctly updates search results after each
deletion

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>Update to the latest `CommunityToolkit.WinUI.Extensions`
and remove hacks</issue_title>
> <issue_description>_originally filed by @zadjii-msft_
> 
> See
https://github.com/zadjii-msft/PowerToys/pull/236#discussion_r1887714771
> 
> I had to stick a couple of HACKs into `SearchBar.xaml.cs` to work
around bugs in the toolkit debouncer. Those bugs have since been fixed
upstream, hooray! We just need a new version of the package shipped and
we can get rid of them.
> 
> ref https://github.com/zadjii-msft/PowerToys/issues/236
> 
> ----
> 
> Also!
> 
> Revert 
> 
> ```
> // TODO(stefan): REVERT THIS TO DASHBOARD PAGE!!!! SPELCHHHHEEK FAIL
> ```
> 
> from
https://github.com/zadjii-msft/PowerToys/issues/215</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> <comment_new><author>@niels9001</author><body>
> @zadjii-msft @michaeljolley I assume we are on a later version now? Do
we still need to remove the hacks?</body></comment_new>
> <comment_new><author>@zadjii-msft</author><body>
> We sure do!
> 
> There's the SearchBar.xaml.cs ones, and I also had to manually copy
over the `TypedEventHandlerExtensions.cs`</body></comment_new>
> </comments>
> 


</details>



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

- Fixes microsoft/PowerToys#38285

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

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
2026-04-01 13:41:23 -05:00
Clint Rutkas
565094abbe Fix SA1623: Fix property documentation summaries (#46717)
Update 29 property XML doc summaries to begin with Gets, Gets or sets,
or Gets a value indicating whether as required by StyleCop SA1623.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 09:46:25 -07:00
Clint Rutkas
1314f68602 Fix SA1622: Add text to empty generic type parameter documentation (#46707)
Add meaningful description to 1 empty typeparam XML doc tag in
BaseDscTest.cs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-01 09:07:40 -07:00
Niels Laute
fbad0dce9c Add /need-monitor-info command (#45636)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-04-01 20:38:16 +08:00
Dustin L. Howett
36a5b77e6c chore: Update to WIL 1.0.250325.1 (#43503)
## Summary of the Pull Request

Updates the Windows Implementation Library (WIL) to version
1.0.250325.1. This fixes some static analysis warnings in C++ projects
that use WIL.

The version is now managed centrally via `Directory.Packages.props`
(Central Package Management), replacing the previous per-project
`packages.config` approach.

## PR Checklist

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

## Detailed Description of the Pull Request / Additional comments

- Updated `Microsoft.Windows.ImplementationLibrary` from `1.0.231216.1`
to `1.0.250325.1` in `Directory.Packages.props`.
- The change is a single-line update since the codebase uses Central
Package Management — all C++ projects reference WIL via
`PackageReference` without specifying a version number directly.

## Validation Steps Performed

- Verified `Directory.Packages.props` correctly reflects the new WIL
version `1.0.250325.1`.
- Merged latest `main` branch to resolve conflicts arising from the
migration to Central Package Management.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-04-01 20:27:56 +08:00
moooyo
4ce451edd0 [ImageResizer] Fix the missing settings of png encoder (#46695)
- Apply codec-specific encoder properties (e.g. JPEG quality) in the
transcode path when transforms are required, matching WPF behavior.
- Apply PngInterlaceOption to the WinRT PNG encoder via the
"InterlaceOption" BitmapPropertySet entry; previously the setting was
persisted but never passed to the encoder.

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

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

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

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

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

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 10:25:53 +00:00
Ray Cheung
42924e71c7 Fix the build.ps1 that does not work well with -RestoreOnly switch (#46012)
<!-- 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

Fix the `build.ps1` that does not work well with `-RestoreOnly` switch.
Also there is `.slnf` now and the build script should support it.

<!-- 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-04-01 10:14:37 +00:00
185 changed files with 8789 additions and 4549 deletions

View File

@@ -19,6 +19,7 @@ OLIVEGREEN
PALEBLUE
PArgb
Pbgra
SRGBTo
WHITEONBLACK
@@ -48,7 +49,6 @@ nupkg
petabyte
resw
resx
runtimeconfig
srt
Stereolithography
terabyte
@@ -332,6 +332,7 @@ REGSTR
INVOKEIDLIST
MEMORYSTATUSEX
ABE
Mdt
HTCAPTION
POSCHANGED
QUERYPOS
@@ -341,6 +342,29 @@ WINEVENTPROC
WORKERW
FULLSCREENAPP
# COM/WinRT interface prefixes and type fragments
BAlt
BShift
Cmanifest
Cmodule
Cuuid
Dng
IApplication
IDisposable
IEnum
IFolder
IInitialize
IMemory
IOle
ipreview
IProperty
IShell
ithumbnail
IVirtual
# Test frameworks
MSTEST
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
DDDD
FFF

View File

@@ -178,7 +178,9 @@ Taras
TBM
Teutsch
tilovell
traies
Triet
udit
urnotdfs
vednig
waaverecords

View File

@@ -140,8 +140,6 @@
^tools/project_template/ModuleTemplate/resource\.h$
^tools/Verification scripts/Check preview handler registration\.ps1$
ignore$
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
^src/common/CalculatorEngineCommon/exprtk\.hpp$
src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage.cs
^src/modules/powerrename/unittests/testdata/avif_test\.avif$
^src/modules/powerrename/unittests/testdata/heif_test\.heic$

View File

@@ -87,6 +87,7 @@ AUTOCHECKBOX
AUTOHIDE
AUTOHSCROLL
AUTOMATIONPROPERTIES
autopf
AUTORADIOBUTTON
Autorun
AUTOTICKS
@@ -241,6 +242,7 @@ CPower
cpptools
cppvsdbg
cppwinrt
createallsubdirs
createdump
CREATEPROCESS
CREATESCHEDULEDTASK
@@ -277,6 +279,7 @@ CYSMICON
CYVIRTUALSCREEN
Dac
dacl
DArchitectures
datareader
datatracker
Dayof
@@ -353,6 +356,7 @@ dlib
dllhost
dllmain
Dmdo
DMy
DNLEN
DONOTROUND
DONTVALIDATEPATH
@@ -472,6 +476,7 @@ FILEMUSTEXIST
FILEOP
FILEOPENDIALOGOPTIONS
FILEOS
filesandordirs
FILESUBTYPE
FILESYSPATH
Filetime
@@ -655,6 +660,7 @@ IEXPLORE
IFACEMETHOD
IFACEMETHODIMP
IGNOREUNKNOWN
ignoreversion
IGo
iid
Iindex
@@ -705,6 +711,8 @@ ipcmanager
IPREVIEW
irprops
isbi
ISCC
isdl
iss
issecret
ISSEPARATOR
@@ -719,6 +727,7 @@ jobject
JOBOBJECT
jpe
jpnime
jrsoftware
Jsons
jsonval
jxr
@@ -939,6 +948,8 @@ muxc
mvvm
MVVMTK
MWBEx
mycompany
myextension
MYICON
myorg
myrepo
@@ -1026,9 +1037,7 @@ NORMALDISPLAY
NORMALUSER
NOSEARCH
NOSENDCHANGING
NOSIZE
notdefault
Nosize
NOTHOUSANDS
NOTICKS
NOTIFICATIONSDLL
@@ -1260,6 +1269,7 @@ Quarternary
QUERYENDSESSION
QUERYOPEN
QUEUESYNC
quicklinks
QUNS
RAII
RAlt
@@ -1280,6 +1290,7 @@ recents
RECTDESTINATION
rectp
RECTSOURCE
recursesubdirs
recyclebin
Redist
Reencode
@@ -1540,6 +1551,7 @@ suntimes
swp
sug
Superbar
SUPPRESSMSGBOXES
sut
svchost
SVGIn
@@ -1570,6 +1582,7 @@ sysmenu
systemai
SYSTEMAPPS
SYSTEMMODAL
systemroot
SYSTEMTIME
TARGETAPPHEADER
targetentrypoint
@@ -1663,6 +1676,7 @@ uncompilable
UNCPRIORITY
UNDNAME
UNICODETEXT
uninsdeletekey
uninstalls
Uniquifies
unitconverter
@@ -1678,6 +1692,7 @@ UOI
UPDATENOW
updown
UPGRADINGPRODUCTCODE
upserts
Uptool
urld
Usb
@@ -1708,6 +1723,7 @@ VERIFYCONTEXT
VERSIONINFO
VERTRES
VERTSIZE
VERYSILENT
VFT
vget
vgetq
@@ -1761,7 +1777,6 @@ webpage
websites
wekyb
wgpocpl
WIC
wic
wifi
winapi
@@ -2159,7 +2174,7 @@ nodiscard
nologo
nomove
nosize
notopmost
NOTOPMOST
Notupdated
notwindows
nowarn

View File

@@ -191,15 +191,6 @@ aka\.ms/[a-zA-Z0-9]+
# #pragma lib
^\s*#pragma comment\(lib, ".*?"\)
# UnitTests
\[DataRow\(.*\)\]
# AdditionalDependencies
<AdditionalDependencies>.*<
# the last line of mimetype="application/x-microsoft.net.object.bytearray.base64" things in .resx files
^\s*[-a-zA-Z=;:/0-9+]*[-a-zA-Z;:/0-9+][-a-zA-Z=;:/0-9+]*=$
RegExp\(@?([`'"]).*?\g{-1}\)|(?:escapes|regEx):\s*(?:/.*/|([`'"]).*?\g{-1})|return/.*?/
# Questionably acceptable forms of `in to`

View File

@@ -233,6 +233,30 @@ configuration:
- addReply:
reply: Hi! Thanks for making us aware of the problem. We raised the issue with our internal localization team. This issue should be fixed hopefully in the next version of PowerToys.
description:
- if:
- payloadType: Issue_Comment
- commentContains:
pattern: '\/need-monitor-info'
isRegex: True
- hasLabel:
label: Product-Cursor Wrap
- or:
- activitySenderHasAssociation:
association: Owner
- activitySenderHasAssociation:
association: Member
- activitySenderHasAssociation:
association: Collaborator
then:
- removeLabel:
label: Needs-Triage
- removeLabel:
label: Needs-Team-Response
- addLabel:
label: Needs-Author-Feedback
- addReply:
reply: "To help debug your layout, please run [this script](https://github.com/microsoft/PowerToys/blob/main/src/modules/MouseUtils/CursorWrap/CursorWrapTests/Capture-MonitorLayout.ps1) and attach the generated JSON output to this thread.\n\nThis allows us to better understand the issue and investigate potential fixes."
description:
- if:
- payloadType: Issue_Comment
- commentContains:

View File

@@ -23,7 +23,7 @@ jobs:
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh)
- name: Log in to Azure
uses: azure/login@v2
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -47,7 +47,7 @@ jobs:
- uses: microsoft/setup-msstore-cli@v1
- name: Fetch Store Credential
uses: azure/cli@v2
uses: azure/cli@v3
with:
azcliversion: latest
inlineScript: |-

View File

@@ -93,7 +93,7 @@ jobs:
steps:
- name: check-spelling
id: spelling
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
with:
config: .github/actions/spell-check
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
@@ -135,6 +135,7 @@ jobs:
cspell:cpp/compiler-msvc.txt
cspell:python/common/extra.txt
cspell:scala/scala.txt
ignored: ignored-expect-variant
comment-push:
name: Report (Push)
@@ -147,10 +148,8 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
steps:
- name: comment
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
with:
config: .github/actions/spell-check
checkout: true
spell_check_this: microsoft/PowerToys@main
task: ${{ needs.spelling.outputs.followup }}
@@ -166,10 +165,8 @@ jobs:
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
with:
config: .github/actions/spell-check
checkout: true
spell_check_this: check-spelling/spell-check-this@prerelease
task: ${{ needs.spelling.outputs.followup }}
experimental_apply_changes_via_bot: ${{ github.repository_owner != 'microsoft' && 1 }}
@@ -193,7 +190,7 @@ jobs:
cancel-in-progress: false
steps:
- name: apply spelling updates
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
with:
experimental_apply_changes_via_bot: ${{ github.repository_owner != 'microsoft' && 1 }}
checkout: true

View File

@@ -217,7 +217,11 @@
"PowerToys.PowerAccentModuleInterface.dll",
"PowerToys.PowerAccentKeyboardService.dll",
"PowerToys.PowerDisplayModuleInterface.dll",
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
"PowerDisplay.Lib.dll",
"PowerDisplay.Models.dll",
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
"WinUI3Apps\\PowerToys.PowerRename.exe",

View File

@@ -3,7 +3,7 @@ description: 'Top-level AI contributor guidance for developing PowerToys - a col
applyTo: '**'
---
# PowerToys AI Contributor Guide
# PowerToys AI contributor guide
This is the top-level guidance for AI contributions to PowerToys. Keep changes atomic, follow existing patterns, and cite exact paths in PRs.
@@ -26,13 +26,15 @@ For architecture details and module types, see [Architecture Overview](doc/devdo
## Conventions
For detailed coding conventions, see:
- [Coding Guidelines](doc/devdocs/development/guidelines.md) Dependencies, testing, PR management
- [Coding Style](doc/devdocs/development/style.md) Formatting, C++/C#/XAML style rules
- [Logging](doc/devdocs/development/logging.md) C++ spdlog and C# Logger usage
### Component-Specific Instructions
### Component-specific instructions
These instruction files are automatically applied when working in their respective areas:
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md) IPC contracts, schema migrations
- [Common Libraries](.github/instructions/common-libraries.instructions.md) ABI stability, shared code guidelines
@@ -44,7 +46,7 @@ These instruction files are automatically applied when working in their respecti
- Windows 10 1803+ (April 2018 Update or newer)
- Initialize submodules once: `git submodule update --init --recursive`
### Build Commands
### Build commands
| Task | Command |
|------|---------|
@@ -52,7 +54,7 @@ These instruction files are automatically applied when working in their respecti
| Build current folder | `tools\build\build.cmd` |
| Build with options | `build.ps1 -Platform x64 -Configuration Release` |
### Build Discipline
### Build discipline
1. One terminal per operation (build → test). Do not switch or open new ones mid-flow
2. After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`)
@@ -62,9 +64,10 @@ These instruction files are automatically applied when working in their respecti
6. On failure, read the errors log: `build.<config>.<platform>.errors.log`
7. Do not start tests or launch Runner until the build succeeds
### Build Logs
### Build logs
Located next to the solution/project being built:
- `build.<configuration>.<platform>.errors.log` errors only (check this first)
- `build.<configuration>.<platform>.all.log` full log
- `build.<configuration>.<platform>.trace.binlog` for MSBuild Structured Log Viewer
@@ -73,18 +76,18 @@ For complete details, see [Build Guidelines](tools/build/BUILD-GUIDELINES.md).
## Tests
### Test Discovery
### Test discovery
- Find test projects by product code prefix (e.g., `FancyZones`, `AdvancedPaste`)
- Look for sibling folders or 1-2 levels up named `<Product>*UnitTests` or `<Product>*UITests`
### Running Tests
### Running tests
1. **Build the test project first**, wait for exit code 0
2. Run via VS Test Explorer (`Ctrl+E, T`) or `vstest.console.exe` with filters
3. **Avoid `dotnet test`** in this repo use VS Test Explorer or vstest.console.exe
### Test Types
### Test types
| Type | Requirements | Setup |
|------|--------------|-------|
@@ -92,13 +95,13 @@ For complete details, see [Build Guidelines](tools/build/BUILD-GUIDELINES.md).
| UI Tests | WinAppDriver v1.2.1, Developer Mode | Install from [WinAppDriver releases](https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1) |
| Fuzz Tests | OneFuzz, .NET 8 | See [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md) |
### Test Discipline
### Test discipline
1. Add or adjust tests when changing behavior
2. If tests skipped, state why (e.g., comment-only change, string rename)
3. New modules handling file I/O or user input **must** implement fuzzing tests
### Special Requirements
### Special requirements
- **Mouse Without Borders**: Requires 2+ physical computers (not VMs)
- **Multi-monitor utilities**: Test with 2+ monitors, different DPI settings
@@ -107,14 +110,14 @@ For UI test setup details, see [UI Tests](doc/devdocs/development/ui-tests.md).
## Boundaries
### Ask for Clarification When
### Ask for clarification when
- Ambiguous spec after scanning relevant docs
- Cross-module impact (shared enum/struct) is unclear
- Security, elevation, or installer changes involved
- GPO or policy handling modifications needed
### Areas Requiring Extra Care
### Areas requiring extra care
| Area | Concern | Reference |
|------|---------|-----------|
@@ -123,7 +126,7 @@ For UI test setup details, see [UI Tests](doc/devdocs/development/ui-tests.md).
| Installer files | Release impact | Careful review required |
| Elevation/GPO logic | Security | Confirm no regression in policy handling |
### What NOT to Do
### What not to do
- Don't merge incomplete features into main (use feature branches)
- Don't break IPC/JSON contracts without updating both runner and settings-ui
@@ -143,23 +146,27 @@ Before finishing, verify:
## Documentation Index
### Core Architecture
### Core architecture
- [Architecture Overview](doc/devdocs/core/architecture.md)
- [Runner](doc/devdocs/core/runner.md)
- [Settings System](doc/devdocs/core/settings/readme.md)
- [Module Interface](doc/devdocs/modules/interface.md)
### Development
- [Coding Guidelines](doc/devdocs/development/guidelines.md)
- [Coding Style](doc/devdocs/development/style.md)
- [Logging](doc/devdocs/development/logging.md)
- [UI Tests](doc/devdocs/development/ui-tests.md)
- [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md)
### Build & Tools
### Build & tools
- [Build Guidelines](tools/build/BUILD-GUIDELINES.md)
- [Tools Overview](doc/devdocs/tools/readme.md)
### Instructions (Auto-Applied)
### Instructions (auto-applied)
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
- [Common Libraries](.github/instructions/common-libraries.instructions.md)

View File

@@ -1,84 +1,109 @@
# Community
The PowerToys team is extremely grateful to have the support of an amazing active community. The work you do is incredibly important. PowerToys wouldnt be near what it is without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thanks and to recognize your work. This is a living document dedicated to highlighting the high impact community members and their contributions.
The PowerToys team is extremely grateful to have the support of an amazing active community. The work you do is incredibly important. PowerToys wouldn't be near what it is without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thanks and to recognize your work. This is a living document dedicated to highlighting the high impact community members and their contributions.
Names are in alphabetical order based on first name.
Names are in alphabetical order, based on first name.
## High impact community members
### [@cgaarden](https://github.com/cgaarden) - [Christian Gaarden Gaardmark](https://www.onegreatworld.com)
Christian contributed New+ utility
### [@cgaarden](https://github.com/cgaarden) - [Christian Gaarden Gaardmark](https://www.onegreatworld.com)
Christian contributed the New+ utility
### [@CleanCodeDeveloper](https://github.com/CleanCodeDeveloper)
CleanCodeDeveloper helped do massive amounts of code stability and image resizer work.
### [@plante-msft](https://github.com/plante-msft) - Connor Plante
Connor was the creator of Workspaces and helped create Command Palette (PowerToys Run v2)
### [@damienleroy](https://github.com/damienleroy) - [Damien Leroy](https://www.linkedin.com/in/Damien-Leroy-b2734416a/)
Damien has helped out by developing and contributing the Quick Accent utility.
### [@daverayment](https://github.com/daverayment) - [David Rayment](https://www.linkedin.com/in/david-rayment-168b5251/)
Dave has helped improve the experience inside of Peek by adding in new features and fixing bugs.
### [@davidegiacometti](https://github.com/davidegiacometti) - [Davide Giacometti](https://www.linkedin.com/in/davidegiacometti/)
Davide has helped fix multiple bugs, added new utilities, features, as well as help us with the ARM64 effort by porting applications to .NET Core.
### [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang
Ethan helped run PowerToys and worked on improving and prototyping out next generation PowerToys
### [@franky920920](https://github.com/franky920920) - [Franky Chen](https://frankychen.net)
Franky has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes to PowerToys.
### [@htcfreek](https://github.com/htcfreek) - Heiko
Heiko has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes to PowerToys.
### [@Jay-o-Way](https://github.com/Jay-o-Way) - Jay
Jay has helped triaging, discussing, creating a substantial number of issues and PRs.
### [@jefflord](https://github.com/Jjefflord) - Jeff Lord
Jeff added in multiple new features into Keyboard manager, such as key chord support and launching apps. He also contributed multiple features/fixes to PowerToys.
Jeff added multiple new features to Keyboard Manager, such as key chord support and launching apps. He also contributed multiple features/fixes to PowerToys.
### [@snickler](https://github.com/snickler) - [Jeremy Sinclair](http://sinclairinat0r.com)
Jeremy has helped drive large sums of the ARM64 support inside PowerToys
Jeremy has helped drive substantial ARM64 support within PowerToys.
### [@jiripolasek](https://github.com/jiripolasek) - [Jiří Polášek](https://github.com/jiripolasek)
Jiří has contributed a massive number of features and improvements to Command Palette, including drag & drop support, custom themes, Web Search enhancements, Remote Desktop extension fixes, and many UX improvements.
### [@TheJoeFin](https://github.com/TheJoeFin) - [Joe Finney](https://joefinapps.com)
Joe has helped triaging, discussing, issues as well as fixing bugs and building features for Text Extractor.
Joe has helped with triaging, discussing issues as well as fixing bugs and building features for Text Extractor.
### [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie
Jordi helped innovate amazing new features into Advanced Paste and helped create Command Palette (PowerToys Run v2)
### [@jsoref](https://github.com/jsoref) - [Josh Soref](https://check-spelling.dev/)
Helping keep our spelling correct :)
### [@martinchrzan](https://github.com/martinchrzan/) - Martin Chrzan
Color Picker is from Martin.
### [@mikeclayton](https://github.com/mikeclayton) - [Michael Clayton](https://michael-clayton.com)
Michael contributed the [initial version](https://github.com/microsoft/PowerToys/issues/23216) of the Mouse Jump tool and [a number of updates](https://github.com/microsoft/PowerToys/pulls?q=is%3Apr+author%3Amikeclayton) based on his FancyMouse utility.
### [@Noraa-Junker](https://github.com/Noraa-Junker) - [Noraa Junker](https://noraajunker.ch)
Noraa has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes. Noraa was the primary person for helping build the File Explorer preview pane handler for developer files.
### [@pedrolamas](https://github.com/pedrolamas/) - Pedro Lamas
Pedro helped create the thumbnail and File Explorer previewers for 3D files like STL and GCode. If you like 3D printing, these are very helpful.
Pedro helped create the thumbnail and File Explorer previewers for 3D files like STL and GCode. If you like 3D printing, these are very helpful.
### [@PesBandi](https://github.com/PesBandi/) - PesBandi
PesBandi has helped do massive amounts of Quick Accent and bug fixes.
### [@riverar](https://github.com/riverar) - [Rafael Rivera](https://withinrafael.com/)
Rafael has helped do the [upgrade from CppWinRT 1.x to 2.0](https://github.com/microsoft/PowerToys/issues/1907). He directly provided feedback to the CppWinRT team for bugs from this migration as well.
Rafael has helped do the [upgrade from CppWinRT 1.x to 2.0](https://github.com/microsoft/PowerToys/issues/1907). He directly provided feedback to the CppWinRT team for bugs from this migration as well.
### [@royvou](https://github.com/royvou)
Roy has helped out contributing multiple features to PowerToys Run
### [@ThiefZero](https://github.com/ThiefZero)
ThiefZero has helped out contributing a features to PowerToys Run such as the unit converter plugin
ThiefZero has helped contribute features to PowerToys Run, such as the unit converter plugin
### [@TobiasSekan](https://github.com/TobiasSekan) - Tobias Sekan
Tobias Sekan has helped out contributing features to PowerToys Run such as Settings plugin, Registry plugin
## Open source projects
@@ -94,7 +119,8 @@ Their fork of Wox was the base of PowerToys Run.
Initial base of jjw24's fork, which makes it the base of PowerToys Run.
### [Text-Grab](https://github.com/TheJoeFin/Text-Grab) - Joseph Finney
Joe helped develop and contribute to the Text Extractor utility. It is directly based on his Text Grab application.
Joe helped develop and contribute to the Text Extractor utility. It is directly based on his Text Grab application.
## Microsoft community members
@@ -102,7 +128,7 @@ We would like to also directly call out some extremely helpful Microsoft employe
### [@betsegaw](https://github.com/betsegaw/) - [Betsegaw Tadele](http://www.dreamsofameaningfullife.com/)
Window Walker, inside PowerToys Run, is from Beta.
Window Walker, inside PowerToys Run, is from Beta.
### [@TheMrJukes](https://github.com/TheMrJukes/) - Bret Anderson
@@ -125,6 +151,7 @@ PowerToys Awake is a tool to keep your computer awake.
Randy contributed Registry Preview and some very early conversations about keyboard remapping.
### [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon
Kayla was a former lead for PowerToys and helped create multiple utilities, maintained the GitHub repo, and collaborated with the community to improve the overall product
### [@oldnewthing](https://github.com/oldnewthing) - Raymond Chen
@@ -135,46 +162,48 @@ Find My Mouse is based on Raymond Chen's SuperSonar.
Crop And Lock is based on the original work of Robert Mikhayelyan, with Program Manager support from [@kevinguo305](https://github.com/kevinguo305) - Kevin Guo.
ZoomIt's Video Recording Session code is based on Robert Mikhayelyan's https://github.com/robmikh/capturevideosample code.
ZoomIt's Video Recording Session code is based on Robert Mikhayelyan's <https://github.com/robmikh/capturevideosample> code.
### Microsoft InVEST team
This amazing team helped PowerToys develop PowerToys Run and Keyboard manager as well as update our Settings to v2. @alekhyareddy28, @arjunbalgovind, @jyuwono @laviusmotileng-ms, @ryanbodrug-microsoft, @saahmedm, @somil55, @traies, @udit3333
This amazing team helped PowerToys develop PowerToys Run and Keyboard manager as well as update our Settings to v2. @alekhyareddy28, @arjunbalgovind, @jyuwono @laviusmotileng-ms, @ryanbodrug-microsoft, @saahmedm, @somil55, @traies, @udit3333
## Mouse Without Borders original contributors
*Project creator: Truong Do (Đỗ Đức Trường)*
Project creator: Truong Do (Đỗ Đức Trường)
Other contributors:
* Microsoft Garage: Quinn Hawkins, Michael Low, Joe Coplen, Nino Yuniardi, Gwyneth Marshall, David Andrews, Karen Luecking
* Peter Hauge - Visual Studio
* Bruce Dawson - Windows Fundamentals
* Alan Myrvold - Office Security
* Adrian Garside - WEX
* Scott Bradner - Surface
* Aleks Gershaft - Windows Azure
* Chinh Huynh - Windows Azure
* Long Nguyen - Data Center
* Triet Le - Cloud Engineering
* Luke Schoen - Excel
* Bao Nguyen - Bing
* Ross Nichols - Windows
* Ryan Baltazar - Windows
* Ed Essey - The Garage
* Mario Madden - The Garage
* Karthick Mahalingam - ACE
* Pooja Kamra - ACE
* Justin White - SA
* Chris Ransom - SA
* Mike Ricks - Red Team
* Randy Santossio - Surface
* Ashish Sen Jaswal - Device Health
* Zoltan Harmath - Security Tools
* Luciano Krigun - Security Products
* Jo Hemmerlein - Red Team
* Chris Johnson - Surface Hub
* Loren Ponten - Surface Hub
* Paul Schmitt - WWL
* And many other Users!
- Microsoft Garage: Quinn Hawkins, Michael Low, Joe Coplen, Nino Yuniardi, Gwyneth Marshall, David Andrews, Karen Luecking
- Peter Hauge - Visual Studio
- Bruce Dawson - Windows Fundamentals
- Alan Myrvold - Office Security
- Adrian Garside - WEX
- Scott Bradner - Surface
- Aleks Gershaft - Windows Azure
- Chinh Huynh - Windows Azure
- Long Nguyen - Data Center
- Triet Le - Cloud Engineering
- Luke Schoen - Excel
- Bao Nguyen - Bing
- Ross Nichols - Windows
- Ryan Baltazar - Windows
- Ed Essey - The Garage
- Mario Madden - The Garage
- Karthick Mahalingam - ACE
- Pooja Kamra - ACE
- Justin White - SA
- Chris Ransom - SA
- Mike Ricks - Red Team
- Randy Santossio - Surface
- Ashish Sen Jaswal - Device Health
- Zoltan Harmath - Security Tools
- Luciano Krigun - Security Products
- Jo Hemmerlein - Red Team
- Chris Johnson - Surface Hub
- Loren Ponten - Surface Hub
- Paul Schmitt - WWL
- And many other Users!
## ZoomIt original contributors

View File

@@ -1,4 +1,4 @@
# PowerToys Contributor's Guide
# PowerToys contributor's guide
Below is our guidance for reporting issues, proposing new features, and submitting contributions via Pull Requests (PRs). Our philosophy is to understand the problem and scenarios first, which is why we follow this pattern before work starts.
@@ -6,46 +6,46 @@ Below is our guidance for reporting issues, proposing new features, and submitti
2. There has been a conversation.
3. There is agreement on the problem, the fit for PowerToys, and the solution to the problem (implementation).
## Filing an Issue
## Filing an issue
**Importance of Filing an Issue First**
Please follow this rule to help eliminate wasted effort and frustration, and to ensure an efficient and effective use of everyones time:
Please follow this rule to help eliminate wasted effort and frustration, and to ensure an efficient and effective use of everyone's time:
> 👉 If you have a question, think you've discovered an issue, or would like to propose a new feature, please find/file an issue **BEFORE** starting work to fix/implement it.
When requesting new features or enhancements, providing additional evidence, data, tweets, blog posts, or research is extremely helpful. This information gives context to the scenario that may otherwise be lost.
* Unsure whether its an issue or feature request? File an issue.
* Have a question that isn't answered in the docs, videos, etc.? File an issue.
* Want to know if were planning a particular feature? File an issue.
* Got a great idea for a new utility or feature? File an issue/request/idea.
* Dont understand how to do something? File an issue/Community Guidance Request.
* Found an existing issue that describes yours? Great! Upvote and add additional commentary, info, or repro steps.
- Unsure whether it's an issue or feature request? File an issue.
- Have a question that isn't answered in the docs, videos, etc.? File an issue.
- Want to know if we're planning a particular feature? File an issue.
- Got a great idea for a new utility or feature? File an issue/request/idea.
- Don't understand how to do something? File an issue/Community Guidance Request.
- Found an existing issue that describes yours? Great! Upvote and add additional commentary, info, or repro steps.
A quick search before filing an issue could be helpful. Its likely someone else has found the same problem, and they may even be working on or have already contributed a fix!
A quick search before filing an issue could be helpful. It's likely someone else has found the same problem, and they may even be working on or have already contributed a fix!
### Indicating Interest in Issues
### Indicating interest in issues
To let the team know which issues are important, upvote by clicking the [+😊] button and the 👍 icon on the original issue post. Avoid comments like "+1" or "me too" as they clutter the discussion and make it harder to prioritize requests.
---
## Contributing Fixes/Features
## Contributing fixes or features
Please comment on our ["Would you like to contribute to PowerToys?"](https://github.com/microsoft/PowerToys/issues/28769) thread to let us know you're interested in working on something before you start. This helps avoid multiple people unexpectedly working on the same thing and ensures everyone is clear on what should be done. It's less work for everyone to establish this up front.
Please comment on our [Would you like to contribute to PowerToys?](https://github.com/microsoft/PowerToys/issues/28769) thread to let us know you're interested in working on something before you start. This helps avoid multiple people unexpectedly working on the same thing and ensures everyone is clear on what should be done. It's less work for everyone to establish this up front.
### Localization Issues
### Localization issues
For localization issues, please file an issue to notify our internal localization team, as community PRs for localization aren't accepted. Localization is handled exclusively by the internal Microsoft team.
### To Spec or Not to Spec
### To spec or not to spec
A key point is for everyone to understand the approach that will be taken. We want to be sure that any work done will be accepted. Larger-scope items will require a spec to outline the approach and allow for discussion. Specs help collaborators consider different solutions, describe feature behavior, and plan for errors. Achieving agreement in a spec before writing code often results in simpler code and less wasted effort.
Once a team member has agreed with your approach, proceed to the "Development" section below. Team members are happy to help review specs and guide them to completion.
### Help Wanted
### Help wanted
Once the team has approved an issue/spec approach, development can proceed. If no developers are immediately available, the spec may be parked and labeled "Help Wanted," ready for a developer to get started. For development opportunities, visit [Issues labeled Help Wanted](https://github.com/microsoft/PowerToys/labels/Help%20Wanted).
@@ -55,18 +55,18 @@ Once the team has approved an issue/spec approach, development can proceed. If n
Follow the [development guidelines](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md).
### Naming Features and Functionality
### Naming features and functionality
Names should be descriptive and straightforward, clearly reflecting functionality and usefulness.
### Becoming a Collaborator on the PowerToys Team
### Becoming a collaborator on the PowerToys team
Be an active community member! Make helpful contributions by filing bugs, offering suggestions, developing fixes and features, conducting code reviews, and updating documentation.
Be an active community member! Make helpful contributions by filing bugs, offering suggestions, developing fixes and features, conducting code reviews, and updating documentation.
When the time comes, Microsoft will reach out to you about becoming a formal team member. Just make sure they have a way to contact you. 😊
---
## Thank You
## Thank you
Thank you in advance for your contribution! We appreciate your help in making PowerToys a better tool for everyone.

File diff suppressed because it is too large Load Diff

View File

@@ -18,15 +18,15 @@
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.12" />
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Collections" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Collections" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
<PackageVersion Include="ControlzEx" Version="6.0.0" />
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
@@ -75,7 +75,7 @@
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260209005" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.260203002" />

View File

@@ -17,7 +17,7 @@ This software incorporates material from third parties.
### Martin Chrzan's Color Picker
**Source**: https://github.com/martinchrzan/ColorPicker
**Source**: <https://github.com/martinchrzan/ColorPicker>
MIT License
@@ -49,7 +49,7 @@ We use the WyHash NuGet package for calculating stable hashes for strings.
**Source**: [https://github.com/wangyi-fudan/wyhash](https://github.com/wangyi-fudan/wyhash)
```
```text
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
@@ -82,7 +82,7 @@ We use the ToolGood.Words.Pinyin NuGet package for converting Chinese characters
**Source**: [https://github.com/toolgood/ToolGood.Words.Pinyin](https://github.com/toolgood/ToolGood.Words.Pinyin)
```
```text
MIT License
Copyright (c) 2020 ToolGood
@@ -106,8 +106,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## Utility: Command Palette Built-in Extensions
## Utility: Command palette built-in extensions
### Calculator
@@ -117,7 +116,7 @@ We use the exprtk library (exprtk.hpp) to evaluate mathematical expressions.
**Source**: [https://github.com/ArashPartow/exprtk](https://github.com/ArashPartow/exprtk)
```
```text
MIT License
Copyright (c) 1999-2024 Arash Partow
@@ -144,7 +143,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```
## Utility: PowerToys Run Built-in Extensions
## Utility: PowerToys Run built-in extensions
### Calculator
@@ -154,7 +153,7 @@ We use the Mages NuGet package for calculating the result of expression.
**Source**: [https://github.com/FlorianRappl/Mages](https://github.com/FlorianRappl/Mages)
```
```text
The MIT License (MIT)
Copyright (c) 2016 - 2025 Florian Rappl
@@ -178,13 +177,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## Utility: File Explorer Add-ins
## Utility: File Explorer add-ins
### Monaco Editor
**Source**: https://github.com/Microsoft/monaco-editor
**Source**: <https://github.com/Microsoft/monaco-editor>
**Additional third party notifications:** https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt
**Additional third party notifications:** <https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt>
The MIT License (MIT)
@@ -208,9 +207,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### The Quite OK Image Format reference decoder
### The Quite OK image format reference decoder
**Source**: https://github.com/phoboslab/qoi
**Source**: <https://github.com/phoboslab/qoi>
**Note**: [@pedrolamas](https://github.com/pedrolamas) translated and adapted the reference decoder code to C# that is in PowerToys from the original C++ implementation.
@@ -240,9 +239,9 @@ SOFTWARE.
We use the UTF.Unknown NuGet package for detecting encoding in text/code files.
**Source**: https://github.com/CharsetDetector/UTF-unknown
**Source**: <https://github.com/CharsetDetector/UTF-unknown>
```
```text
MOZILLA PUBLIC LICENSE
Version 1.1
@@ -716,9 +715,9 @@ EXHIBIT A -Mozilla Public License.
## Utility: ImageResizer
### Brice Lams's Image Resizer License
### Brice Lams's Image Resizer license
**Source**: https://github.com/bricelam/ImageResizer/
**Source**: <https://github.com/bricelam/ImageResizer/>
The MIT License (MIT)
@@ -744,10 +743,10 @@ THE SOFTWARE.
## Utility: PowerToys Run
### Wox License
### Wox license
**Fork project source**: https://github.com/jjw24/Wox/
**Base project source**: https://github.com/Wox-launcher/Wox
**Fork project source**: <https://github.com/jjw24/Wox/>
**Base project source**: <https://github.com/Wox-launcher/Wox>
The MIT License (MIT)
@@ -770,9 +769,9 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### Beta Tadele's Window Walker License
### Beta Tadele's Window Walker license
**Source**: https://github.com/betsegaw/windowwalker
**Source**: <https://github.com/betsegaw/windowwalker>
The MIT License (MIT)
@@ -786,9 +785,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
## Utility: PowerRename
### Chris Davis's SmartRename License
### Chris Davis's SmartRename license
**Source**: https://github.com/chrdavis/SmartRename
**Source**: <https://github.com/chrdavis/SmartRename>
MIT License
@@ -816,7 +815,7 @@ SOFTWARE.
### spdlog
**Source**: https://github.com/gabime/spdlog
**Source**: <https://github.com/gabime/spdlog>
The MIT License (MIT)
@@ -841,12 +840,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-- NOTE: Third party dependency used by this software --
This software depends on the fmt lib (MIT License),
and users must comply to its license: https://github.com/fmtlib/fmt/blob/master/LICENSE.rst
This software depends on the fmt lib (MIT License), and users must comply to its license:
<https://github.com/fmtlib/fmt/blob/master/LICENSE.rst>
### expected-lite
**Source**: https://github.com/martinmoene/expected-lite
**Source**: <https://github.com/martinmoene/expected-lite>
Boost Software License - Version 1.0 - August 17th, 2003
@@ -874,7 +873,7 @@ DEALINGS IN THE SOFTWARE.
### zip
**Source**: https://github.com/kuba--/zip
**Source**: <https://github.com/kuba--/zip>
All Rights Reserved.
@@ -902,7 +901,7 @@ THE SOFTWARE.
We adopted some functions from it.
**Source**: https://github.com/DLTcollab/sse2neon
**Source**: <https://github.com/DLTcollab/sse2neon>
sse2neon is freely redistributable under the MIT License.
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -925,9 +924,9 @@ SOFTWARE.
### Monaco Editor
**Source**: https://github.com/Microsoft/monaco-editor
**Source**: <https://github.com/Microsoft/monaco-editor>
**Additional third party notifications:** https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt
**Additional third party notifications:** <https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt>
The MIT License (MIT)
@@ -951,11 +950,11 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### The Quite OK Image Format reference decoder
### The Quite OK image format reference decoder
**Source**: https://github.com/phoboslab/qoi
**Source**: <https://github.com/phoboslab/qoi>
**Note**: [@pedrolamas](https://github.com/pedrolamas) translated and adapted the reference decoder code to C# that is in PowerToys from the original C++ implementation.
**Note**: [@pedrolamas](https://github.com/pedrolamas) translated and adapted the reference decoder code to C# that is in PowerToys, from the original C++ implementation.
MIT License
@@ -979,13 +978,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### UTF Unknown
### UTF unknown
We use the UTF.Unknown NuGet package for detecting encoding in text/code files.
**Source**: https://github.com/CharsetDetector/UTF-unknown
**Source**: <https://github.com/CharsetDetector/UTF-unknown>
```
```text
MOZILLA PUBLIC LICENSE
Version 1.1
@@ -1463,9 +1462,9 @@ EXHIBIT A -Mozilla Public License.
We use HexBox.WinUI to show a preview of binary values.
**Source**: https://github.com/hotkidfamily/HexBox.WinUI
**Source**: <https://github.com/hotkidfamily/HexBox.WinUI>
```
```text
MIT License
Copyright (c) 2019 Filip Jeremic
@@ -1492,11 +1491,11 @@ SOFTWARE.
### Monaco Editor
**Source**: https://github.com/Microsoft/monaco-editor
**Source**: <https://github.com/Microsoft/monaco-editor>
**Additional third party notifications:** https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt
**Additional third party notifications:** <https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt>
```
```text
The MIT License (MIT)
Copyright (c) 2016 - present Microsoft Corporation
@@ -1526,7 +1525,7 @@ SOFTWARE.
PowerDisplay's DDC/CI implementation references techniques from Twinkle Tray.
**Source**: https://github.com/xanderfrangos/twinkle-tray
**Source**: <https://github.com/xanderfrangos/twinkle-tray>
MIT License

View File

@@ -57,6 +57,7 @@
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
</Folder>
<Folder Name="/common/interop/">
@@ -709,17 +710,19 @@
</Project>
</Folder>
<Folder Name="/modules/PowerDisplay/">
<Project Path="src/modules/powerdisplay/PowerDisplay.Models/PowerDisplay.Models.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<!-- TEMPORARILY_DISABLED: PowerDisplay
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
-->
</Folder>
<Folder Name="/modules/PowerDisplay/Tests/">
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">

View File

@@ -19,14 +19,13 @@
<span> · </span>
<a href="#-whats-new">Release notes</a>
</h3>
<br/><br/>
## 🔨 Utilities
PowerToys includes over 25 utilities to help you customize and optimize your Windows experience:
PowerToys includes over 30 utilities to help you customize and optimize your Windows experience:
| | | |
|---|---|---|
| --- | --- | --- |
| [<img src="doc/images/icons/AdvancedPaste.png" alt="Advanced Paste icon" height="16"> Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [<img src="doc/images/icons/Always%20On%20Top.png" alt="Always on Top icon" height="16"> Always on Top](https://aka.ms/PowerToysOverview_AoT) | [<img src="doc/images/icons/Awake.png" alt="Awake icon" height="16"> Awake](https://aka.ms/PowerToysOverview_Awake) |
| [<img src="doc/images/icons/Color%20Picker.png" alt="Color Picker icon" height="16"> Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [<img src="doc/images/icons/Command%20Not%20Found.png" alt="Command Not Found icon" height="16"> Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [<img src="doc/images/icons/Command Palette.png" alt="Command Palette icon" height="16"> Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
| [<img src="doc/images/icons/Crop%20And%20Lock.png" alt="Crop and Lock icon" height="16"> Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [<img src="doc/images/icons/Environment%20Manager.png" alt="Environment Variables icon" height="16"> Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [<img src="doc/images/icons/FancyZones.png" alt="FancyZones icon" height="16"> FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
@@ -38,28 +37,27 @@ PowerToys includes over 25 utilities to help you customize and optimize your Win
| [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
| [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) | | |
## 📦 Installation
## 📋 Installation
For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
But to get started quickly, choose one of the installation methods below:
<br/><br/>
<details open>
<summary><strong>Download .exe from GitHub</strong></summary>
<summary><strong>Download the .exe file from GitHub</strong></summary>
<br/>
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
Go to the [PowerToys GitHub releases](https://aka.ms/installPowerToys), select **Assets** to reveal the installation files, and choose the one that matches your architecture and install scope. For most devices, that would be _x64 per-user_.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.99%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-arm64.exe
| Description | Filename |
|----------------|----------|
| Description | Filename |
| --- | --- |
| Per user - x64 | [PowerToysUserSetup-0.98.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.98.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.98.1-x64.exe][ptMachineX64] |
@@ -83,14 +81,16 @@ You can easily install PowerToys from the Microsoft Store:
<details>
<summary><strong>WinGet</strong></summary>
<br/>
Download PowerToys from <a href="https://github.com/microsoft/winget-cli#installing-the-client">WinGet</a>. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
Download PowerToys from [WinGet](https://github.com/microsoft/winget-cli#installing-the-client). Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
- User scope installer (default)
*User scope installer [default]*
```powershell
winget install Microsoft.PowerToys -s winget
```
*Machine-wide scope installer*
- Machine-wide scope installer
```powershell
winget install --scope machine Microsoft.PowerToys -s winget
```
@@ -99,7 +99,7 @@ winget install --scope machine Microsoft.PowerToys -s winget
<details>
<summary><strong>Other methods</strong></summary>
<br/>
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
There are [community driven install methods](https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
</details>
## ✨ What's new?
@@ -108,28 +108,26 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.1).
## 🛣️ Roadmap
## 🛣️ Roadmap
We are planning some nice new features and improvements for the next releases PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.99][github-next-release-work]!
## ❤️ PowerToys Community
## ❤️ PowerToys Community
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
## Contributing
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so. For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
## Contributing
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so. For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
## Privacy Statement
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
## Code of conduct
[oss-CLA]: https://cla.opensource.microsoft.com
[oss-conduct-code]: CODE_OF_CONDUCT.md
[community-link]: COMMUNITY.md
[github-release-link]: https://aka.ms/installPowerToys
[microsoft-store-link]: https://aka.ms/getPowertoys
[winget-link]: https://github.com/microsoft/winget-cli#installing-the-client
[roadmap]: https://github.com/microsoft/PowerToys/wiki/Roadmap
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
## Privacy statement
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
[oss-CLA]: https://cla.opensource.microsoft.com
[oss-conduct-code]: CODE_OF_CONDUCT.md
[community-link]: COMMUNITY.md

View File

@@ -1,36 +1,36 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
## Security
# Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
## Reporting Security Issues
## Reporting security issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
If you prefer to submit without logging in, send an email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
## Preferred Languages
## Preferred languages
We prefer all communications to be in English.

View File

@@ -1,24 +1,21 @@
# Support
## How to use Microsoft PowerToys
## How to use Microsoft PowerToys
For more info on [PowerToys overviews and how to use the utilities][usingPowerToys-docs-link], or any other tools and resources for [Windows development environments](https://learn.microsoft.com/windows/dev-environment/overview), head over to [learn.microsoft.com][usingPowerToys-docs-link]!
For more information about PowerToys overviews, how to use the utilities, and other tools and resources for [Windows development environments](https://learn.microsoft.com/windows/dev-environment/overview), visit [learn.microsoft.com][usingPowerToys-docs-link].
## How to file issues and get help
This project uses [GitHub Issues][gh-issue] to [track bugs][gh-bug] and [feature requests][gh-feature]. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or
feature request as a new Issue.
This project uses [GitHub Issues][gh-issue] to [track bugs][gh-bug] and [feature requests][gh-feature]. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue.
For help and questions about using this project, please look at our Wiki for using PowerToys and our [Contributor's Guide][contributor] if you want to work on PowerToys.
For help and questions about using this project, please visit our documentation and [Contributor's Guide][contributor] if you want to contribute to PowerToys.
## Microsoft Support Policy
## Microsoft support policy
Support for PowerToys is limited to the resources listed above.
[gh-issue]: https://github.com/microsoft/PowerToys/issues/new/choose
[gh-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=Issue-Bug&template=bug_report.md&title=
[gh-feature]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=feature_request.md&title=
[wiki]: https://github.com/microsoft/PowerToys/wiki
[gh-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=Issue-Bug&template=bug_report.md
[gh-feature]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=feature_request.md
[contributor]: https://github.com/microsoft/PowerToys/blob/main/CONTRIBUTING.md
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs

View File

@@ -1594,6 +1594,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.PowerRename.exe",
L"PowerToys.ImageResizer.exe",
L"PowerToys.LightSwitchService.exe",
L"PowerToys.PowerDisplay.exe",
L"PowerToys.GcodeThumbnailProvider.exe",
L"PowerToys.BgcodeThumbnailProvider.exe",
L"PowerToys.PdfThumbnailProvider.exe",

View File

@@ -47,6 +47,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
@@ -123,6 +124,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
<Compile Include="KeyboardManager.wxs" />
<Compile Include="Peek.wxs" />
<Compile Include="PowerRename.wxs" />
<Compile Include="PowerDisplay.wxs" />
<Compile Include="DscResources.wxs" />
<Compile Include="RegistryPreview.wxs" />
<Compile Include="Run.wxs" />

View File

@@ -53,6 +53,7 @@
<ComponentGroupRef Id="LightSwitchComponentGroup" />
<ComponentGroupRef Id="PeekComponentGroup" />
<ComponentGroupRef Id="PowerRenameComponentGroup" />
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
<ComponentGroupRef Id="RunComponentGroup" />
<ComponentGroupRef Id="SettingsComponentGroup" />

View File

@@ -212,6 +212,10 @@ Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRo
Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\"
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs
#PowerDisplay
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay\"
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
#RegistryPreview
Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\"
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs

View File

@@ -14,6 +14,8 @@
#include <common/updating/updating.h>
#include <common/updating/updateState.h>
#include <common/updating/installer.h>
#include <common/updating/configBackup.h>
#include <common/updating/updateLifecycle.h>
#include <common/utils/elevation.h>
#include <common/utils/HttpClient.h>
@@ -21,6 +23,8 @@
#include <common/utils/resources.h>
#include <common/utils/timeutil.h>
#include <wil/resource.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/logger/logger.h>
@@ -38,15 +42,16 @@ namespace fs = std::filesystem;
std::optional<fs::path> CopySelfToTempDir()
{
// D5 fix: Use unique temp path with PID to avoid collision on concurrent updates
std::error_code error;
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
auto dst_path = fs::temp_directory_path() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
if (error)
{
return std::nullopt;
}
return std::move(dst_path);
return dst_path;
}
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
@@ -57,34 +62,9 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
auto state = UpdateState::read();
const auto new_version_info = std::move(get_github_version_info_async()).get();
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
else if (state.state == UpdateState::readyToInstall)
// Handle readyToInstall first — the installer is already on disk,
// so we don't need a GitHub API call (which may fail if offline).
if (state.state == UpdateState::readyToInstall)
{
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
if (fs::is_regular_file(installer))
@@ -97,12 +77,44 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
return std::nullopt;
}
}
else if (state.state == UpdateState::upToDate)
if (state.state == UpdateState::upToDate)
{
isUpToDate = true;
return std::nullopt;
}
const auto new_version_info = std::move(get_github_version_info_async()).get();
// Check for error BEFORE dereferencing — the old code crashed here
// when GitHub API was unreachable (new_version_info held an error string).
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
Logger::error("Invoked with -update_now argument, but update state was invalid");
return std::nullopt;
}
@@ -116,13 +128,29 @@ bool InstallNewVersionStage1(fs::path installer)
if (pt_main_window != nullptr)
{
// Get the process that owns the tray window so we can wait for it to exit
DWORD ptProcessId = 0;
GetWindowThreadProcessId(pt_main_window, &ptProcessId);
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
// D4 fix: Wait for PT to actually exit before launching installer.
// Without this, the installer may find PT files locked.
if (ptProcessId != 0)
{
wil::unique_handle ptProcess{ OpenProcess(SYNCHRONIZE, FALSE, ptProcessId) };
if (ptProcess)
{
WaitForSingleObject(ptProcess.get(), 10000); // 10 second timeout
}
}
}
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
arguments += L" \"";
arguments += installer.c_str();
arguments += L"\"";
// Pass the install directory so Stage 2 can relaunch PowerToys after install
const std::wstring installDir = get_module_folderpath();
std::wstring arguments = updating::BuildStage2Arguments(
UPDATE_NOW_LAUNCH_STAGE2, installer, fs::path(installDir));
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = copy_in_temp->c_str();
@@ -190,9 +218,16 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
if (!args || nArgs < 2)
{
if (args)
{
LocalFree(args);
}
return 1;
}
// D3 fix: ensure args is freed on all exit paths
auto freeArgs = wil::scope_exit([&] { LocalFree(args); });
std::wstring_view action{ args[1] };
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
@@ -201,6 +236,10 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
if (action == UPDATE_NOW_LAUNCH_STAGE1)
{
// Backup config files before the update to protect against corruption
Logger::info("Backing up config files before update");
updating::BackupConfigFiles(fs::path(PTSettingsHelper::get_root_save_folder_location()));
bool isUpToDate = false;
auto installerPath = ObtainInstaller(isUpToDate);
bool failed = !installerPath.has_value();
@@ -217,6 +256,12 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
}
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
{
if (nArgs < 3)
{
Logger::error("Stage 2 invoked without installer path argument");
return 1;
}
using namespace std::string_view_literals;
const bool failed = !InstallNewVersionStage2(args[2]);
if (failed)
@@ -227,6 +272,37 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
state.state = UpdateState::errorDownloading;
});
}
// D7 fix: Always check for corrupted configs after Stage 2, regardless
// of install success/failure. A failed install may still corrupt configs.
Logger::info("Checking for corrupted config files after update");
updating::RestoreCorruptedConfigs(fs::path(PTSettingsHelper::get_root_save_folder_location()));
if (!failed)
{
// Relaunch PowerToys from the install directory
if (updating::CanRelaunchAfterUpdate(nArgs))
{
std::wstring ptExePath = updating::BuildPowerToysExePath(args[3]);
Logger::info(L"Relaunching PowerToys after update: {}", ptExePath);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = ptExePath.c_str();
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = UPDATE_REPORT_SUCCESS;
if (!ShellExecuteExW(&sei))
{
Logger::error(L"Failed to relaunch PowerToys after update");
}
}
else
{
Logger::warn("Install directory not provided to Stage 2 - cannot relaunch PowerToys");
}
}
return failed;
}

View File

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

View File

@@ -0,0 +1,679 @@
// 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.
#include "pch.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
#include <common/updating/configBackup.h>
#include <common/updating/updateLifecycle.h>
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace fs = std::filesystem;
namespace UpdatingUnitTests
{
// Helper to create a temp directory for test isolation.
// Each instance gets a unique subdirectory to prevent test interference.
class TempDir
{
public:
TempDir()
{
wchar_t tempPath[MAX_PATH + 1];
GetTempPathW(MAX_PATH, tempPath);
static std::atomic<int> counter{0};
m_path = fs::path(tempPath) / (L"PowerToysUpdateTests_" + std::to_wstring(counter++));
// Ensure clean state
std::error_code ec;
fs::remove_all(m_path, ec);
fs::create_directories(m_path, ec);
}
~TempDir()
{
std::error_code ec;
fs::remove_all(m_path, ec);
}
const fs::path& path() const { return m_path; }
// Write a file with the given content
void WriteFile(const fs::path& relativePath, const std::string& content)
{
auto fullPath = m_path / relativePath;
fs::create_directories(fullPath.parent_path());
std::ofstream file(fullPath, std::ios::binary);
file.write(content.data(), content.size());
}
// Write a file with raw bytes (including null bytes for corruption testing)
void WriteFileBytes(const fs::path& relativePath, const std::vector<char>& bytes)
{
auto fullPath = m_path / relativePath;
fs::create_directories(fullPath.parent_path());
std::ofstream file(fullPath, std::ios::binary);
file.write(bytes.data(), bytes.size());
}
// Read file content as string
std::string ReadFile(const fs::path& relativePath)
{
auto fullPath = m_path / relativePath;
std::ifstream file(fullPath, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
}
bool FileExists(const fs::path& relativePath)
{
return fs::exists(m_path / relativePath);
}
private:
fs::path m_path;
};
TEST_CLASS(IsJsonFileCorruptedTests)
{
public:
// Tests IsJsonFileCorrupted: valid JSON with no null bytes returns false.
// Covers: configBackup.h IsJsonFileCorrupted — happy path, full file scan.
TEST_METHOD(CleanJsonFileIsNotCorrupted)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark","startup":true})");
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
}
// Tests IsJsonFileCorrupted: zero-length file returns false (empty is not corrupted).
// Covers: configBackup.h IsJsonFileCorrupted — file.read returns 0 bytes immediately.
TEST_METHOD(EmptyFileIsNotCorrupted)
{
TempDir dir;
dir.WriteFile(L"empty.json", "");
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"empty.json"));
}
// Tests IsJsonFileCorrupted: file containing embedded null bytes returns true.
// Covers: configBackup.h IsJsonFileCorrupted — null byte detection within buffer.
TEST_METHOD(FileWithNullBytesIsCorrupted)
{
TempDir dir;
std::vector<char> corrupted = { '{', '"', 'a', '"', ':', '\0', '\0', '\0', '}' };
dir.WriteFileBytes(L"corrupted.json", corrupted);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"corrupted.json"));
}
// Tests IsJsonFileCorrupted: file entirely filled with 0x00 bytes returns true.
// Reproduces the exact bug from #46179 where installer zeroed out JSON files.
// Covers: configBackup.h IsJsonFileCorrupted — first byte is null.
TEST_METHOD(FileFilledWithNullBytesIsCorrupted)
{
TempDir dir;
std::vector<char> allNulls(1024, '\0');
dir.WriteFileBytes(L"workspaces.json", allNulls);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"workspaces.json"));
}
// Tests IsJsonFileCorrupted: path that does not exist returns false.
// Covers: configBackup.h IsJsonFileCorrupted — file.is_open() check.
TEST_METHOD(NonExistentFileIsNotCorrupted)
{
TempDir dir;
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"missing.json"));
}
// Tests IsJsonFileCorrupted: file larger than the 4096-byte read chunk
// with no null bytes returns false.
// Covers: configBackup.h IsJsonFileCorrupted — multi-chunk while loop.
TEST_METHOD(LargeCleanFileIsNotCorrupted)
{
TempDir dir;
std::string largeContent(8192, 'x');
dir.WriteFile(L"large.json", largeContent);
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"large.json"));
}
// Tests IsJsonFileCorrupted: null byte placed after the first 4096-byte
// chunk boundary is still detected.
// Covers: configBackup.h IsJsonFileCorrupted — second chunk scan.
TEST_METHOD(NullByteAtEndOfLargeFileIsDetected)
{
TempDir dir;
std::string content(5000, 'x');
content[4999] = '\0';
std::vector<char> bytes(content.begin(), content.end());
dir.WriteFileBytes(L"sneaky.json", bytes);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"sneaky.json"));
}
};
TEST_CLASS(BackupConfigFilesTests)
{
public:
// Tests BackupConfigFiles: root-level .json files are copied to ConfigBackup.
// Covers: configBackup.h BackupConfigFiles — root directory_iterator,
// is_regular_file && extension == ".json" branch.
// Setup: Two root-level JSON files.
TEST_METHOD(BackupCopiesRootJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"UpdateState.json", R"({"state":0})");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\UpdateState.json"));
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
}
// Tests BackupConfigFiles: .json files inside module subdirectories are
// copied to ConfigBackup/<module>/.
// Covers: configBackup.h BackupConfigFiles — is_directory branch,
// module directory_iterator with extension filter.
// Setup: Root JSON + two module directories with JSON files.
TEST_METHOD(BackupCopiesModuleJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[]})");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
Assert::AreEqual(std::string(R"({"zones":[]})"),
dir.ReadFile(L"ConfigBackup\\FancyZones\\settings.json"));
}
// Tests BackupConfigFiles: non-.json files at root level are not copied.
// Covers: configBackup.h BackupConfigFiles — extension filter excludes .log.
// Setup: One JSON file + one .log file at root.
TEST_METHOD(BackupSkipsNonJsonFiles)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"debug.log", "log data");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\debug.log"));
}
// Tests BackupConfigFiles: the "Updates" directory is explicitly skipped.
// Covers: configBackup.h BackupConfigFiles — dirName == L"Updates" continue.
// Setup: Root JSON + Updates directory containing a file.
TEST_METHOD(BackupSkipsUpdatesDirectory)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
dir.WriteFile(L"Updates\\installer.exe", "fake exe");
updating::BackupConfigFiles(dir.path());
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\Updates"));
}
// Tests BackupConfigFiles: running backup twice overwrites the previous
// backup with current file content.
// Covers: configBackup.h BackupConfigFiles — fs::remove_all(backupDir) +
// copy_options::overwrite_existing.
// Setup: Backup, modify original, backup again.
TEST_METHOD(BackupOverwritesPreviousBackup)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"version":1})");
updating::BackupConfigFiles(dir.path());
// Update the original
dir.WriteFile(L"settings.json", R"({"version":2})");
updating::BackupConfigFiles(dir.path());
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
}
// Tests BackupConfigFiles: non-.json files inside module subdirectories
// (e.g., FancyZones/zones.dat) should NOT be backed up.
// Covers: configBackup.h BackupConfigFiles — extension filter in module loop.
TEST_METHOD(BackupSkipsNonJsonFilesInModuleDirs)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
dir.WriteFile(L"FancyZones\\zones.dat", "binary data");
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\FancyZones\\zones.dat"));
}
// Tests BackupConfigFiles: empty root directory with no files produces
// an empty ConfigBackup dir without errors.
// Covers: configBackup.h BackupConfigFiles — empty directory_iterator.
TEST_METHOD(BackupEmptyRootDirSucceeds)
{
TempDir dir;
// Root dir exists but has no files
updating::BackupConfigFiles(dir.path());
Assert::IsTrue(dir.FileExists(L"ConfigBackup"));
}
};
TEST_CLASS(RestoreCorruptedConfigsTests)
{
public:
// Tests RestoreCorruptedConfigs: corrupted root-level JSON file is restored
// from the good backup copy.
// Covers: configBackup.h RestoreCorruptedConfigs — root file restore branch,
// fs::exists + IsJsonFileCorrupted + backup integrity check.
// Setup: Good file -> backup -> corrupt original -> restore.
TEST_METHOD(RestoreFixesCorruptedRootFile)
{
TempDir dir;
const std::string goodContent = R"({"theme":"dark"})";
dir.WriteFile(L"settings.json", goodContent);
// Backup
updating::BackupConfigFiles(dir.path());
// Corrupt the original
std::vector<char> corrupted(goodContent.size(), '\0');
dir.WriteFileBytes(L"settings.json", corrupted);
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
// Restore
updating::RestoreCorruptedConfigs(dir.path());
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
Assert::AreEqual(goodContent, dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: corrupted module-level JSON file is restored
// from the good backup copy.
// Covers: configBackup.h RestoreCorruptedConfigs — module directory branch,
// moduleBackupEntry restore with integrity check.
// Setup: Module file + root file -> backup -> corrupt module file -> restore.
TEST_METHOD(RestoreFixesCorruptedModuleFile)
{
TempDir dir;
const std::string goodContent = R"({"workspaces":[]})";
dir.WriteFile(L"Workspaces\\workspaces.json", goodContent);
dir.WriteFile(L"settings.json", R"({})");
updating::BackupConfigFiles(dir.path());
// Corrupt the module file
std::vector<char> corrupted(goodContent.size(), '\0');
dir.WriteFileBytes(L"Workspaces\\workspaces.json", corrupted);
updating::RestoreCorruptedConfigs(dir.path());
Assert::AreEqual(goodContent, dir.ReadFile(L"Workspaces\\workspaces.json"));
}
// Tests RestoreCorruptedConfigs: clean (non-corrupted) files are NOT
// overwritten by backup — preserves user changes made after backup.
// Covers: configBackup.h RestoreCorruptedConfigs — IsJsonFileCorrupted
// returns false, copy_file is skipped.
// Setup: File -> backup -> modify (but keep valid) -> restore.
TEST_METHOD(RestoreLeavesCleanFilesUntouched)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"version":1})");
updating::BackupConfigFiles(dir.path());
// Modify original (but keep it clean JSON)
dir.WriteFile(L"settings.json", R"({"version":2})");
updating::RestoreCorruptedConfigs(dir.path());
// Should NOT have been restored since it's not corrupted
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: when no ConfigBackup directory exists,
// restore silently does nothing (no crash, no data loss).
// Covers: configBackup.h RestoreCorruptedConfigs — !fs::exists(backupDir)
// early return.
// Setup: File with no prior backup.
TEST_METHOD(RestoreHandlesMissingBackupDirectory)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
// No backup was created - restore should silently do nothing
updating::RestoreCorruptedConfigs(dir.path());
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"settings.json"));
}
// Tests RestoreCorruptedConfigs: end-to-end scenario with multiple modules,
// some corrupted and some clean, verifying selective restore.
// Covers: configBackup.h RestoreCorruptedConfigs — both root and module
// branches, selective restore based on corruption status.
// Setup: 4 modules -> backup -> corrupt 2 -> restore -> verify all 4.
TEST_METHOD(FullBackupAndRestoreRoundTrip)
{
TempDir dir;
// Set up a realistic config structure
dir.WriteFile(L"settings.json", R"({"startup":true,"theme":"dark"})");
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[{"id":1}]})");
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[{"name":"dev"}]})");
dir.WriteFile(L"KeyboardManager\\default.json", R"({"remaps":[]})");
// Backup
updating::BackupConfigFiles(dir.path());
// Corrupt some files (simulating #46179 scenario)
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(100, '\0'));
dir.WriteFileBytes(L"settings.json", std::vector<char>(50, '\0'));
// Leave FancyZones and KBM clean
// Restore
updating::RestoreCorruptedConfigs(dir.path());
// Corrupted files should be restored
Assert::AreEqual(std::string(R"({"startup":true,"theme":"dark"})"), dir.ReadFile(L"settings.json"));
Assert::AreEqual(std::string(R"({"workspaces":[{"name":"dev"}]})"), dir.ReadFile(L"Workspaces\\workspaces.json"));
// Clean files should be unchanged
Assert::AreEqual(std::string(R"({"zones":[{"id":1}]})"), dir.ReadFile(L"FancyZones\\settings.json"));
Assert::AreEqual(std::string(R"({"remaps":[]})"), dir.ReadFile(L"KeyboardManager\\default.json"));
}
// Tests RestoreCorruptedConfigs: when the original file has been deleted
// (not corrupted), restore should NOT recreate it from backup. The installer
// may have intentionally removed obsolete config files.
// Covers: configBackup.h RestoreCorruptedConfigs — fs::exists guard.
TEST_METHOD(RestoreSkipsDeletedOriginals)
{
TempDir dir;
dir.WriteFile(L"obsolete.json", R"({"old":true})");
updating::BackupConfigFiles(dir.path());
// Installer deletes the file
std::error_code ec;
fs::remove(dir.path() / L"obsolete.json", ec);
updating::RestoreCorruptedConfigs(dir.path());
// Should NOT be recreated
Assert::IsFalse(dir.FileExists(L"obsolete.json"));
}
// Tests RestoreCorruptedConfigs: when the backup file itself is corrupted
// (e.g., disk error during backup), restore should NOT copy corrupted
// backup over the original — that would make things worse.
// Covers: configBackup.h RestoreCorruptedConfigs — backup integrity check (B2 fix).
TEST_METHOD(RestoreSkipsCorruptedBackup)
{
TempDir dir;
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
updating::BackupConfigFiles(dir.path());
// Corrupt BOTH the original AND the backup
std::vector<char> nulls(50, '\0');
dir.WriteFileBytes(L"settings.json", nulls);
dir.WriteFileBytes(L"ConfigBackup\\settings.json", nulls);
updating::RestoreCorruptedConfigs(dir.path());
// Original should still be corrupted — we don't restore from bad backup
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
}
};
// Simulates what actually happens during a PowerToys upgrade:
// 1. User has settings from normal use
// 2. Updater backs up before install (Stage 1)
// 3. Installer runs and corrupts some files (simulated)
// 4. Updater restores corrupted files (Stage 2)
// 5. PT relaunches and finds working configs
TEST_CLASS(UpgradeSimulationTests)
{
public:
// Tests full upgrade simulation: backup -> installer corrupts files -> restore.
// Verifies that corrupted files are restored and clean files are untouched.
// Covers: configBackup.h BackupConfigFiles + RestoreCorruptedConfigs —
// end-to-end with 5 modules, 2 corrupted, 3 clean.
// Setup: Realistic config structure with multiple modules.
TEST_METHOD(SimulateUpgradeWithCorruption)
{
TempDir dir;
// === User's real config state before upgrade ===
dir.WriteFile(L"settings.json",
R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})");
dir.WriteFile(L"FancyZones\\settings.json",
R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})");
dir.WriteFile(L"Workspaces\\workspaces.json",
R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})");
dir.WriteFile(L"KeyboardManager\\default.json",
R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})");
dir.WriteFile(L"MouseWithoutBorders\\settings.json",
R"({"machineKey":"abc123","connectToAll":true})");
// Non-JSON files that should be left alone
dir.WriteFile(L"update.log", "2026-04-11 update started");
// === Stage 1: Backup before killing PT ===
updating::BackupConfigFiles(dir.path());
// Verify backup was created correctly
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\KeyboardManager\\default.json"));
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\MouseWithoutBorders\\settings.json"));
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\update.log"));
// === Installer runs: some files get corrupted (the #46179 scenario) ===
// Workspaces JSON filled with null bytes
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(512, '\0'));
// Main settings partially corrupted (null bytes injected)
std::vector<char> partialCorrupt = { '{', '"', 's', '\0', '\0', '\0', '\0', '}' };
dir.WriteFileBytes(L"settings.json", partialCorrupt);
// FancyZones, KBM, and MWB survive the install fine
// (this is realistic - not all files get corrupted)
// === Stage 2: Restore after install completes ===
updating::RestoreCorruptedConfigs(dir.path());
// === Verify: PT relaunches and finds working configs ===
// Corrupted files should be restored from backup
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"Workspaces\\workspaces.json"));
Assert::AreEqual(
std::string(R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})"),
dir.ReadFile(L"settings.json"));
Assert::AreEqual(
std::string(R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})"),
dir.ReadFile(L"Workspaces\\workspaces.json"));
// Clean files should be untouched (not overwritten with backup)
Assert::AreEqual(
std::string(R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})"),
dir.ReadFile(L"FancyZones\\settings.json"));
Assert::AreEqual(
std::string(R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})"),
dir.ReadFile(L"KeyboardManager\\default.json"));
Assert::AreEqual(
std::string(R"({"machineKey":"abc123","connectToAll":true})"),
dir.ReadFile(L"MouseWithoutBorders\\settings.json"));
}
// Tests upgrade from an old version that has fewer modules than the new version.
// Verifies that new module configs (created by the installer) are not touched
// by restore, while corrupted old configs are restored.
// Covers: configBackup.h RestoreCorruptedConfigs — module dir in root that
// has no corresponding backup entry.
// Setup: Old version with 1 module -> backup -> new installer adds module -> corrupt old -> restore.
TEST_METHOD(SimulateUpgradeFromVeryOldVersion)
{
TempDir dir;
// Old version had fewer modules - only settings.json
dir.WriteFile(L"settings.json", R"({"theme":"dark","powertoys_version":"v0.60.0"})");
// Backup
updating::BackupConfigFiles(dir.path());
// New installer creates new module dirs that didn't exist before
dir.WriteFile(L"NewModule\\settings.json", R"({"enabled":true})");
// Old settings get corrupted during upgrade
dir.WriteFileBytes(L"settings.json", std::vector<char>(100, '\0'));
// Restore
updating::RestoreCorruptedConfigs(dir.path());
// Old settings restored
Assert::AreEqual(
std::string(R"({"theme":"dark","powertoys_version":"v0.60.0"})"),
dir.ReadFile(L"settings.json"));
// New module settings untouched (no backup existed for them)
Assert::AreEqual(
std::string(R"({"enabled":true})"),
dir.ReadFile(L"NewModule\\settings.json"));
}
};
// Tests for the update lifecycle: argument passing between Stage 1 and Stage 2,
// relaunch path construction, and the handoff that was broken in #42004/#43011/#44071.
TEST_CLASS(UpdateLifecycleTests)
{
public:
// Tests BuildStage2Arguments: output contains the stage 2 flag, installer path,
// and install directory — all three components needed for Stage 2.
// Covers: updateLifecycle.h BuildStage2Arguments — concatenation logic.
// Setup: Typical paths with spaces (Program Files).
TEST_METHOD(BuildStage2ArgumentsContainsInstallerAndInstallDir)
{
const auto args = updating::BuildStage2Arguments(
L"-update_now_stage_2",
L"C:\\Users\\test\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-x64.exe",
L"C:\\Program Files\\PowerToys");
// Must contain the stage 2 flag
Assert::IsTrue(args.find(L"-update_now_stage_2") != std::wstring::npos);
// Must contain the installer path (quoted)
Assert::IsTrue(args.find(L"powertoyssetup-x64.exe") != std::wstring::npos);
// Must contain the install directory (quoted) — this was MISSING before our fix
Assert::IsTrue(args.find(L"C:\\Program Files\\PowerToys") != std::wstring::npos);
}
// Tests BuildStage2Arguments: both paths are wrapped in double quotes to
// survive CommandLineToArgvW parsing when paths contain spaces.
// Covers: updateLifecycle.h BuildStage2Arguments — quote wrapping.
// Setup: Installer path with spaces.
TEST_METHOD(BuildStage2ArgumentsQuotesBothPaths)
{
const auto args = updating::BuildStage2Arguments(
L"-update_now_stage_2",
L"C:\\path with spaces\\installer.exe",
L"C:\\Program Files\\PowerToys");
// Count quotes — should have 4 (open/close for each path)
size_t quoteCount = std::count(args.begin(), args.end(), L'"');
Assert::AreEqual(size_t{ 4 }, quoteCount);
}
// Tests BuildPowerToysExePath: appends "PowerToys.exe" to the install dir.
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path / operator.
// Setup: Standard install path without trailing backslash.
TEST_METHOD(BuildPowerToysExePathAppendsExeName)
{
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys");
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
}
// Tests BuildPowerToysExePath: trailing backslash does not produce double
// backslash (e.g., "...PowerToys\\PowerToys.exe").
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path normalizes separators.
// Setup: Install path with trailing backslash.
TEST_METHOD(BuildPowerToysExePathHandlesTrailingBackslash)
{
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys\\");
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
}
// Tests BuildPowerToysExePath: empty string produces just "PowerToys.exe".
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path with empty input.
// Setup: Empty install directory string.
TEST_METHOD(BuildPowerToysExePathHandlesEmptyString)
{
const auto path = updating::BuildPowerToysExePath(L"");
Assert::AreEqual(std::wstring(L"PowerToys.exe"), path);
}
// Tests CanRelaunchAfterUpdate: returns true when Stage 2 receives
// the install directory (argCount >= 4), false otherwise.
// This is the gate that prevents relaunch when using an old Stage 1
// that didn't pass the install dir (#42004/#43011/#44071).
// Covers: updateLifecycle.h CanRelaunchAfterUpdate.
TEST_METHOD(CanRelaunchReflectsArgCount)
{
// Old Stage 1 (pre-fix): only passed action + installer = 3 args
Assert::IsFalse(updating::CanRelaunchAfterUpdate(0));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(1));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(2));
Assert::IsFalse(updating::CanRelaunchAfterUpdate(3));
// New Stage 1 (post-fix): passes action + installer + installDir = 4 args
Assert::IsTrue(updating::CanRelaunchAfterUpdate(4));
Assert::IsTrue(updating::CanRelaunchAfterUpdate(5));
}
// Tests BuildStage2Arguments + CommandLineToArgvW round-trip: the exact
// scenario where Stage 1 builds args and Windows parses them in Stage 2.
// Verifies quoting is correct so paths with spaces survive the round trip.
// Covers: updateLifecycle.h BuildStage2Arguments — quote correctness.
// Setup: Realistic paths with spaces and version numbers.
TEST_METHOD(Stage2ArgumentsCanBeRoundTrippedThroughCommandLineToArgvW)
{
const std::wstring installerPath = L"C:\\Users\\test user\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-0.86.0-x64.exe";
const std::wstring installDir = L"C:\\Program Files\\PowerToys";
const auto args = updating::BuildStage2Arguments(L"-update_now_stage_2", installerPath, installDir);
// Simulate what Windows does: prepend a fake exe name and parse
std::wstring commandLine = L"PowerToys.Update.exe " + args;
int argc = 0;
LPWSTR* argv = CommandLineToArgvW(commandLine.c_str(), &argc);
Assert::IsNotNull(argv);
Assert::AreEqual(4, argc);
Assert::AreEqual(std::wstring(L"-update_now_stage_2"), std::wstring(argv[1]));
Assert::AreEqual(installerPath, std::wstring(argv[2]));
Assert::AreEqual(installDir, std::wstring(argv[3]));
LocalFree(argv);
}
};
}

View File

@@ -0,0 +1,45 @@
<?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" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>UpdatingUnitTests</RootNamespace>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
<ProjectName>Updating.UnitTests</ProjectName>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseOfMfc>false</UseOfMfc>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UpdatingUnitTests\</OutDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="UpdatingTests.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

View File

@@ -0,0 +1,5 @@
// 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.
#include "pch.h"

View File

@@ -0,0 +1,17 @@
// 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.
#ifndef PCH_H
#define PCH_H
#include <atomic>
#include <Windows.h>
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
#pragma warning(push)
#pragma warning(disable : 26466)
#include "CppUnitTest.h"
#pragma warning(pop)
#endif //PCH_H

View File

@@ -0,0 +1,170 @@
// 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.
#pragma once
#include <filesystem>
#include <fstream>
#include <string>
namespace updating
{
namespace fs = std::filesystem;
// Check if a JSON file is corrupted (contains null bytes, as seen in #46179)
inline bool IsJsonFileCorrupted(const fs::path& filePath)
{
try
{
std::ifstream file(filePath, std::ios::binary);
if (!file.is_open())
{
return false;
}
constexpr size_t c_readChunkSize{ 4096 };
char buffer[c_readChunkSize];
while (file.read(buffer, c_readChunkSize) || file.gcount() > 0)
{
const auto bytesRead = file.gcount();
for (std::streamsize i = 0; i < bytesRead; ++i)
{
if (buffer[i] == '\0')
{
return true;
}
}
}
return false;
}
catch (...)
{
return true;
}
}
// Backup all JSON config files before update to protect against corruption (#46179)
inline void BackupConfigFiles(const fs::path& rootPath)
{
try
{
const fs::path backupDir = rootPath / L"ConfigBackup";
std::error_code ec;
fs::remove_all(backupDir, ec);
// Note: remove_all failure means stale backup may persist; continue anyway
// since create_directories will overlay
fs::create_directories(backupDir, ec);
if (ec)
{
return;
}
for (const auto& entry : fs::directory_iterator(rootPath, ec))
{
if (ec)
{
break;
}
if (entry.is_regular_file() && entry.path().extension() == L".json")
{
fs::copy_file(entry.path(), backupDir / entry.path().filename(), fs::copy_options::overwrite_existing, ec);
}
else if (entry.is_directory())
{
const auto dirName = entry.path().filename().wstring();
if (dirName == L"ConfigBackup" || dirName == L"Updates")
{
continue;
}
const auto moduleBackup = backupDir / entry.path().filename();
fs::create_directories(moduleBackup, ec);
std::error_code moduleEc;
for (const auto& moduleEntry : fs::directory_iterator(entry.path(), moduleEc))
{
if (moduleEc)
{
break;
}
if (moduleEntry.is_regular_file() && moduleEntry.path().extension() == L".json")
{
fs::copy_file(moduleEntry.path(), moduleBackup / moduleEntry.path().filename(), fs::copy_options::overwrite_existing, moduleEc);
}
}
}
}
}
catch (...)
{
// Intentionally swallowed — update must not fail due to backup errors.
// Logging would require spdlog dependency which is unavailable in test context.
}
}
// Restore JSON configs from backup if corruption is detected after update
inline void RestoreCorruptedConfigs(const fs::path& rootPath)
{
try
{
const fs::path backupDir = rootPath / L"ConfigBackup";
if (!fs::exists(backupDir))
{
return;
}
std::error_code ec;
for (const auto& backupEntry : fs::directory_iterator(backupDir, ec))
{
if (ec)
{
break;
}
if (backupEntry.is_regular_file() && backupEntry.path().extension() == L".json")
{
const auto originalPath = rootPath / backupEntry.path().filename();
// Only restore if the backup itself is valid
if (fs::exists(originalPath) && IsJsonFileCorrupted(originalPath) && !IsJsonFileCorrupted(backupEntry.path()))
{
fs::copy_file(backupEntry.path(), originalPath, fs::copy_options::overwrite_existing, ec);
}
}
else if (backupEntry.is_directory())
{
const auto moduleDir = rootPath / backupEntry.path().filename();
std::error_code moduleEc;
for (const auto& moduleBackupEntry : fs::directory_iterator(backupEntry.path(), moduleEc))
{
if (moduleEc)
{
break;
}
if (moduleBackupEntry.is_regular_file() && moduleBackupEntry.path().extension() == L".json")
{
const auto originalModulePath = moduleDir / moduleBackupEntry.path().filename();
// Only restore if the backup itself is valid
if (fs::exists(originalModulePath) && IsJsonFileCorrupted(originalModulePath) && !IsJsonFileCorrupted(moduleBackupEntry.path()))
{
fs::copy_file(moduleBackupEntry.path(), originalModulePath, fs::copy_options::overwrite_existing, moduleEc);
}
}
}
}
}
}
catch (...)
{
// Intentionally swallowed — update must not fail due to backup errors.
// Logging would require spdlog dependency which is unavailable in test context.
}
}
}

View File

@@ -0,0 +1,47 @@
// 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.
#pragma once
#include <filesystem>
#include <string>
namespace updating
{
namespace fs = std::filesystem;
// Build the command-line arguments for Stage 2.
// Stage 1 passes the installer path and the PT install directory
// so Stage 2 can run the installer and relaunch PowerToys afterward.
// Note: paths containing embedded double-quote characters are not supported.
// This is safe because install paths come from get_module_folderpath().
inline std::wstring BuildStage2Arguments(
const std::wstring& stage2Flag,
const fs::path& installerPath,
const fs::path& installDir)
{
std::wstring arguments{ stage2Flag };
arguments += L" \"";
arguments += installerPath.c_str();
arguments += L"\" \"";
arguments += installDir.c_str();
arguments += L"\"";
return arguments;
}
// Build the full path to PowerToys.exe from the install directory.
// Used by Stage 2 to relaunch PT after a successful update.
inline std::wstring BuildPowerToysExePath(const std::wstring& installDir)
{
return (std::filesystem::path(installDir) / L"PowerToys.exe").wstring();
}
// Determine whether Stage 2 has enough information to relaunch PT.
// Returns true if the install directory argument was provided.
inline bool CanRelaunchAfterUpdate(int argCount)
{
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
return argCount >= 4;
}
}

View File

@@ -26,7 +26,7 @@ public class BaseDscTest
/// </summary>
/// <param name="name">The name of the resource string.</param>
/// <param name="args">The arguments to format the resource string with.</param>
/// <returns></returns>
/// <returns>The formatted resource string.</returns>
public string GetResourceString(string name, params string[] args)
{
return string.Format(CultureInfo.InvariantCulture, _resourceManager.GetString(name, CultureInfo.InvariantCulture), args);
@@ -35,9 +35,9 @@ public class BaseDscTest
/// <summary>
/// Execute a dsc command with the provided arguments.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="args"></param>
/// <returns></returns>
/// <typeparam name="T">The type of the DSC command to execute.</typeparam>
/// <param name="args">The command-line arguments to pass to the DSC command.</param>
/// <returns>The result of the DSC command execution.</returns>
protected DscExecuteResult ExecuteDscCommand<T>(params string[] args)
where T : Command, new()
{

View File

@@ -47,6 +47,6 @@ public interface ISettingsFunctionData
/// <summary>
/// Gets the schema for the settings resource object.
/// </summary>
/// <returns></returns>
/// <returns>The JSON schema string for the settings resource object.</returns>
public string Schema();
}

View File

@@ -37,7 +37,7 @@ public class BaseResourceObject
/// <summary>
/// Generates a JSON representation of the resource object.
/// </summary>
/// <returns></returns>
/// <returns>A JSON representation of the resource object.</returns>
public JsonNode ToJson()
{
return JsonSerializer.SerializeToNode(this, GetType(), _options) ?? new JsonObject();

View File

@@ -150,7 +150,6 @@
<decimal value="0" />
</disabledValue>
</policy>
<!-- TEMPORARILY_DISABLED: PowerDisplay
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
@@ -161,7 +160,6 @@
<decimal value="0" />
</disabledValue>
</policy>
-->
<policy name="ConfigureEnabledUtilityEnvironmentVariables" class="Both" displayName="$(string.ConfigureEnabledUtilityEnvironmentVariables)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityEnvironmentVariables">
<parentCategory ref="PowerToys" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />

View File

@@ -249,7 +249,7 @@ If you don't configure this policy, the user will be able to control the setting
<string id="ConfigureEnabledUtilityCmdPal">CmdPal: Configure enabled state</string>
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
<string id="ConfigureEnabledUtilityLightSwitch">Light Switch: Configure enabled state</string>
<!-- <string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string> --><!-- TEMPORARILY_DISABLED: PowerDisplay -->
<string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string>
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>

View File

@@ -70,12 +70,12 @@
Spacing="2">
<TextBlock
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind Header, Mode=OneWay}"
Text="{x:Bind Header, Mode=OneTime}"
TextWrapping="NoWrap" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Timestamp, Converter={StaticResource DateTimeToFriendlyStringConverter}, Mode=OneWay}" />
Text="{x:Bind Timestamp, Converter={StaticResource DateTimeToFriendlyStringConverter}, Mode=OneTime}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -29,31 +29,31 @@
Padding="-9,0,0,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AutomationProperties.AcceleratorKey="{x:Bind ShortcutText, Mode=OneWay}"
AutomationProperties.AcceleratorKey="{x:Bind ShortcutText, Mode=OneTime}"
AutomationProperties.AutomationControlType="ListItem"
AutomationProperties.FullDescription="{x:Bind ToolTip, Mode=OneWay}"
AutomationProperties.HelpText="{x:Bind Name, Mode=OneWay}"
AutomationProperties.Name="{x:Bind AccessibleName, Mode=OneWay}">
AutomationProperties.FullDescription="{x:Bind ToolTip, Mode=OneTime}"
AutomationProperties.HelpText="{x:Bind Name, Mode=OneTime}"
AutomationProperties.Name="{x:Bind AccessibleName, Mode=OneTime}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ToolTipService.ToolTip>
<TextBlock Text="{x:Bind ToolTip, Mode=OneWay}" />
<TextBlock Text="{x:Bind ToolTip, Mode=OneTime}" />
</ToolTipService.ToolTip>
<FontIcon
Margin="0,0,0,0"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
FontSize="16"
Glyph="{x:Bind IconGlyph, Mode=OneWay}" />
Glyph="{x:Bind IconGlyph, Mode=OneTime}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
x:Phase="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name, Mode=OneWay}" />
Text="{x:Bind Name, Mode=OneTime}" />
<TextBlock
Grid.Column="2"
Margin="0,0,8,0"
@@ -61,7 +61,7 @@
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ShortcutText, Mode=OneWay}" />
Text="{x:Bind ShortcutText, Mode=OneTime}" />
</Grid>
</DataTemplate>
</controls:PasteFormatTemplateSelector.ItemTemplate>
@@ -83,13 +83,13 @@
Margin="0,0,0,0"
VerticalAlignment="Center"
FontSize="16"
Glyph="{x:Bind IconGlyph, Mode=OneWay}" />
Glyph="{x:Bind IconGlyph, Mode=OneTime}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
x:Phase="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Name, Mode=OneWay}" />
Text="{x:Bind Name, Mode=OneTime}" />
</Grid>
</DataTemplate>
</controls:PasteFormatTemplateSelector.ItemTemplateDisabled>
@@ -198,7 +198,7 @@
<ItemsView.ItemTemplate>
<DataTemplate x:DataType="local:ClipboardItem">
<ItemContainer
AutomationProperties.Name="{x:Bind Description, Mode=OneWay}"
AutomationProperties.Name="{x:Bind Description, Mode=OneTime}"
CornerRadius="16"
ToolTipService.ToolTip="{x:Bind Content}">
<Grid

View File

@@ -0,0 +1,180 @@
// 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.Linq;
using HostsUILib;
using HostsUILib.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Hosts.Tests
{
[TestClass]
public class ValidationHelperTest
{
[DataTestMethod]
[DataRow("0.0.0.0")]
[DataRow("127.0.0.1")]
[DataRow("192.168.1.1")]
[DataRow("255.255.255.255")]
[DataRow("10.0.0.1")]
[DataRow("172.16.0.1")]
[DataRow("1.2.3.4")]
[DataRow("01.01.01.01")]
[DataRow("0.0.0.1")]
public void ValidIPv4_ValidAddresses_ReturnsTrue(string address)
{
Assert.IsTrue(ValidationHelper.ValidIPv4(address));
}
[DataTestMethod]
[DataRow("256.0.0.0")]
[DataRow("0.256.0.0")]
[DataRow("0.0.256.0")]
[DataRow("0.0.0.256")]
[DataRow("999.999.999.999")]
[DataRow("1.2.3")]
[DataRow("1.2.3.4.5")]
[DataRow("1.2.3.")]
[DataRow(".1.2.3")]
[DataRow("1..2.3")]
[DataRow("abc.def.ghi.jkl")]
[DataRow("192.168.1.1/24")]
[DataRow("192.168.1.1:80")]
[DataRow("192.168.1")]
[DataRow("-1.0.0.0")]
public void ValidIPv4_InvalidAddresses_ReturnsFalse(string address)
{
Assert.IsFalse(ValidationHelper.ValidIPv4(address));
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[DataRow("\t")]
[DataRow("\n")]
public void ValidIPv4_NullOrWhitespace_ReturnsFalse(string address)
{
Assert.IsFalse(ValidationHelper.ValidIPv4(address));
}
[DataTestMethod]
[DataRow("::1")]
[DataRow("::")]
[DataRow("2001:0db8:85a3:0000:0000:8a2e:0370:7334")]
[DataRow("2001:db8:85a3:0:0:8a2e:370:7334")]
[DataRow("2001:db8:85a3::8a2e:370:7334")]
[DataRow("fe80::1")]
[DataRow("ff02::1")]
[DataRow("2001:db8::1")]
[DataRow("::ffff:192.168.1.1")]
[DataRow("fe80::1%eth0")]
public void ValidIPv6_ValidAddresses_ReturnsTrue(string address)
{
Assert.IsTrue(ValidationHelper.ValidIPv6(address));
}
[DataTestMethod]
[DataRow("2001:db8:85a3:0:0:8a2e:370:7334:extra")]
[DataRow("gggg::1")]
[DataRow("12345::1")]
[DataRow("192.168.1.1")]
[DataRow("::ffff:999.999.999.999")]
[DataRow("hello")]
[DataRow("2001:db8:85a3::8a2e:370:7334:1234:5678")]
public void ValidIPv6_InvalidAddresses_ReturnsFalse(string address)
{
Assert.IsFalse(ValidationHelper.ValidIPv6(address));
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[DataRow("\t")]
public void ValidIPv6_NullOrWhitespace_ReturnsFalse(string address)
{
Assert.IsFalse(ValidationHelper.ValidIPv6(address));
}
[DataTestMethod]
[DataRow("localhost")]
[DataRow("example.com")]
[DataRow("sub.domain.example.com")]
[DataRow("my-host")]
[DataRow("host1 host2")]
[DataRow("host1 host2 host3")]
[DataRow("example.com www.example.com")]
public void ValidHosts_ValidHostnames_ReturnsTrue(string hosts)
{
Assert.IsTrue(ValidationHelper.ValidHosts(hosts, validateHostsLength: false));
}
[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[DataRow("\t")]
public void ValidHosts_NullOrWhitespace_ReturnsFalse(string hosts)
{
Assert.IsFalse(ValidationHelper.ValidHosts(hosts, validateHostsLength: false));
}
[TestMethod]
public void ValidHosts_WithLengthValidation_ExceedsMaxCount_ReturnsFalse()
{
// Create a host string with one more than MaxHostsCount hosts
var hosts = string.Join(" ", Enumerable.Range(1, Consts.MaxHostsCount + 1).Select(i => $"h{i}"));
Assert.IsFalse(ValidationHelper.ValidHosts(hosts, validateHostsLength: true));
}
[TestMethod]
public void ValidHosts_WithLengthValidation_AtMaxCount_ReturnsTrue()
{
// Create a host string with exactly MaxHostsCount hosts
var hosts = string.Join(" ", Enumerable.Range(1, Consts.MaxHostsCount).Select(i => $"h{i}"));
Assert.IsTrue(ValidationHelper.ValidHosts(hosts, validateHostsLength: true));
}
[TestMethod]
public void ValidHosts_WithLengthValidation_BelowMaxCount_ReturnsTrue()
{
string hosts = "host1 host2 host3";
Assert.IsTrue(ValidationHelper.ValidHosts(hosts, validateHostsLength: true));
}
[TestMethod]
public void ValidHosts_WithoutLengthValidation_ExceedsMaxCount_ReturnsTrue()
{
// When validateHostsLength is false, exceeding max count should still return true
var hosts = string.Join(" ", Enumerable.Range(1, Consts.MaxHostsCount + 1).Select(i => $"h{i}"));
Assert.IsTrue(ValidationHelper.ValidHosts(hosts, validateHostsLength: false));
}
[TestMethod]
public void ValidHosts_SingleHost_ReturnsTrue()
{
Assert.IsTrue(ValidationHelper.ValidHosts("localhost", validateHostsLength: true));
}
[TestMethod]
public void ValidHosts_InvalidHostname_ReturnsFalse()
{
Assert.IsFalse(ValidationHelper.ValidHosts("host_with!invalid@chars", validateHostsLength: false));
}
[TestMethod]
public void ValidHosts_HostWithSubdomains_ReturnsTrue()
{
Assert.IsTrue(ValidationHelper.ValidHosts("sub.domain.example.com", validateHostsLength: true));
}
[TestMethod]
public void ValidHosts_MultipleValidHosts_WithLengthValidation_ReturnsTrue()
{
Assert.IsTrue(ValidationHelper.ValidHosts("example.com www.example.com api.example.com", validateHostsLength: true));
}
}
}

View File

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

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
@@ -29,11 +30,12 @@ namespace PowerOCR;
internal sealed class ImageMethods
{
internal static Bitmap PadImage(Bitmap image, int minW = 64, int minH = 64)
internal static bool PadImage(Bitmap image, [NotNullWhen(true)] out Bitmap? paddedBitmap, int minW = 64, int minH = 64)
{
if (image.Height >= minH && image.Width >= minW)
{
return image;
paddedBitmap = null;
return false;
}
int width = Math.Max(image.Width + 16, minW + 16);
@@ -45,8 +47,9 @@ internal sealed class ImageMethods
gd.Clear(image.GetPixel(0, 0));
gd.DrawImageUnscaled(image, 8, 8);
paddedBitmap = destination;
return destination;
return true;
}
internal static ImageSource GetWindowBoundsImage(OCROverlay passedWindow)
@@ -77,8 +80,15 @@ internal sealed class ImageMethods
bmp.Size,
CopyPixelOperation.SourceCopy);
bmp = PadImage(bmp);
return bmp;
if (PadImage(bmp, out var paddedBmp))
{
bmp.Dispose();
return paddedBmp;
}
else
{
return bmp;
}
}
internal static async Task<string> GetRegionsText(OCROverlay? passedWindow, Rectangle selectedRegion, Language? preferredLanguage)

View File

@@ -0,0 +1,48 @@
# Command Palette Extension Copilot Instructions
Concise guidance for AI-assisted development of this Command Palette extension.
## Project Structure
| Folder | Purpose |
|--------|---------|
| `Pages/` | Extension pages (ListPage, ContentPage, DynamicListPage implementations) |
| `Assets/` | Icons and images (StoreLogo.png, etc.) |
| `Properties/` | Launch settings and publish profiles |
| Root `.cs` files | Extension entry point, COM server (Program.cs), and CommandsProvider |
## Key Conventions
- Extensions run **out-of-process** via COM server registration
- `Program.cs` hosts the COM server — do not modify the hosting pattern
- The `CommandProvider` subclass is the entry point for all commands
- Pages are **ICommand** implementations — they can be used anywhere commands are used
- Always **Deploy** (not just Build) to register the MSIX package
- After deploying, use the **Reload** command in Command Palette to refresh
## Build & Deploy
1. In Visual Studio, use **Build > Deploy** (not just Build)
2. In Command Palette, run `Reload` → select "Reload Command Palette extensions"
3. For debugging, run in Debug configuration (F5) and check Output window (Ctrl+Alt+O)
## Source Control
If using git, remove these lines from `.gitignore` (needed for deployment):
- `**/Properties/launchSettings.json`
- `*.pubxml`
## Available Skills
This project includes Copilot skills for common workflows:
- **add-adaptive-card-form** — Create form-based UI with Adaptive Cards
- **add-extension-settings** — Add a settings page to your extension
- **add-dock-band** — Add persistent toolbar widgets
- **add-fallback-commands** — Add catch-all search commands
- **publish-extension** — Publish to Microsoft Store or WinGet
## Documentation
- [Creating an extension](https://learn.microsoft.com/windows/powertoys/command-palette/creating-an-extension)
- [Extension samples](https://learn.microsoft.com/windows/powertoys/command-palette/samples)
- [Extensibility overview](https://learn.microsoft.com/windows/powertoys/command-palette/extensibility-overview)

View File

@@ -0,0 +1,353 @@
---
description: 'Comprehensive guide for developing Command Palette extensions — covers pages, content, commands, items, icons, settings, dock, and debugging'
applyTo: '**/*.cs'
---
# Command Palette Extension Development
Complete reference for building Command Palette (CmdPal) extensions. Extensions run out-of-process as MSIX-packaged COM servers.
## Extension Architecture
### IExtension Interface
The root class implements `IExtension` and `IDisposable`:
```csharp
[Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")]
public sealed partial class MyExtension : IExtension, IDisposable
{
private readonly ManualResetEvent _extensionDisposedEvent;
private readonly MyCommandsProvider _provider = new();
public MyExtension(ManualResetEvent extensionDisposedEvent)
{
_extensionDisposedEvent = extensionDisposedEvent;
}
public object? GetProvider(ProviderType providerType) => providerType switch
{
ProviderType.Commands => _provider,
_ => null,
};
public void Dispose() => _extensionDisposedEvent.Set();
}
```
- Only `ProviderType.Commands` is currently supported
- The `[Guid]` must match the CLSID in `Package.appxmanifest`
### CommandProvider
Override `TopLevelCommands()` to register main commands. Optionally override `FallbackCommands()` and `GetDockBands()`:
```csharp
public partial class MyCommandsProvider : CommandProvider
{
public MyCommandsProvider()
{
DisplayName = "My Extension";
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
}
public override ICommandItem[] TopLevelCommands() => [
new CommandItem(new MyPage()) { Title = DisplayName },
];
}
```
### COM Server (Program.cs)
`Program.cs` hosts the COM server. Do not change this pattern:
```csharp
public class Program
{
[MTAThread]
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
{
global::Shmuelie.WinRTServer.ComServer server = new();
ManualResetEvent extensionDisposedEvent = new(false);
var extensionInstance = new MyExtension(extensionDisposedEvent);
server.RegisterClass<MyExtension, IExtension>(() => extensionInstance);
server.Start();
extensionDisposedEvent.WaitOne();
server.Stop();
server.UnsafeDispose();
}
}
}
```
### Package.appxmanifest
Two critical extension registrations must be present:
1. **COM server**`com:ComServer` with matching CLSID and `-RegisterProcessAsComServer` args
2. **App extension**`uap3:AppExtension` with `Name="com.microsoft.commandpalette"` and `CreateInstance ClassId` matching the GUID
The CLSID must be identical in three places: the `[Guid]` attribute, the `com:Class Id`, and the `CreateInstance ClassId`.
## Page Types
### ListPage (Most Common)
Displays a searchable list of items:
```csharp
internal sealed partial class MyPage : ListPage
{
public MyPage()
{
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Title = "My page";
Name = "Open";
}
public override IListItem[] GetItems() => [
new ListItem(new OpenUrlCommand("https://example.com")) { Title = "Example" },
];
}
```
### DynamicListPage (Search-Reactive)
Responds to search text changes for filtering or live queries:
```csharp
internal sealed partial class MyDynamicPage : DynamicListPage
{
private IListItem[] _filteredItems = [];
public override void UpdateSearchText(string oldSearch, string newSearch)
{
_filteredItems = _allItems
.Where(i => i.Title.Contains(newSearch, StringComparison.OrdinalIgnoreCase))
.ToArray();
RaiseItemsChanged();
}
public override IListItem[] GetItems() => _filteredItems;
}
```
- Supports `Filters` property for category filtering
- Call `RaiseItemsChanged()` after updating items to notify the UI
### ContentPage (Rich Content)
Displays rich content like markdown, forms, or images:
```csharp
internal sealed partial class MyContentPage : ContentPage
{
public override IContent[] GetContent() => [
new MarkdownContent("# Hello\nThis is **markdown**."),
];
}
```
- Can return multiple `IContent` items (mix markdown, forms, images, etc.)
- Supports `Commands` property for context menu items via `CommandContextItem`
## Content Types
| Type | Description |
|------|-------------|
| `MarkdownContent(string)` | Renders markdown with headers, links, code blocks, tables, images |
| `FormContent` | Adaptive Cards forms with `TemplateJson`, optional `DataJson`, and `SubmitForm()` |
| `PlainTextContent(string)` | Plain text; optional `FontFamily.Monospace` and `WrapWords` |
| `ImageContent` | Images with `MaxWidth`/`MaxHeight` constraints |
| `TreeContent` | Hierarchical nested content; override `GetChildren()` for child `IContent[]` |
### MarkdownContent Images
Supports `file:`, `data:` (base64), and `https:` URLs. Image hints control rendering:
```markdown
![alt](https://example.com/img.png?--x-cmdpal-fit=fit&--x-cmdpal-maxwidth=400)
```
### FormContent (Adaptive Cards)
```csharp
internal sealed partial class MyForm : FormContent
{
public MyForm()
{
TemplateJson = """{ "type": "AdaptiveCard", ... }""";
DataJson = """{ "name": "default" }""";
}
public override CommandResult SubmitForm(string payload)
{
var data = JsonSerializer.Deserialize<MyFormData>(payload);
return CommandResult.Dismiss();
}
}
```
- Design cards visually at [adaptivecards.io/designer](https://adaptivecards.io/designer)
- Use `${...}` placeholders in `TemplateJson` bound to `DataJson` properties
## Commands
### InvokableCommand
Actions that do something when activated:
```csharp
internal sealed partial class MyCommand : InvokableCommand
{
public override string Name => "Do it";
public override IconInfo Icon => new("\uE945");
public override CommandResult Invoke()
{
// Do work here
return CommandResult.Dismiss();
}
}
```
### Built-in Command Helpers
| Helper | Purpose |
|--------|---------|
| `OpenUrlCommand(string url)` | Open URL in default browser |
| `CopyTextCommand(string text)` | Copy to clipboard with toast |
| `NoOpCommand()` | Does nothing (placeholder) |
| `AnonymousCommand(Action? action)` | Lambda command; set `Result` property for navigation |
### CommandResult Types
| Result | Behavior |
|--------|----------|
| `CommandResult.Dismiss()` | Hide palette, go home |
| `CommandResult.KeepOpen()` | Stay on current page |
| `CommandResult.Hide()` | Hide palette, keep page state |
| `CommandResult.GoBack()` | Navigate back one page |
| `CommandResult.GoHome()` | Navigate to home page |
| `CommandResult.ShowToast("msg")` | Show toast notification, then dismiss |
| `CommandResult.Confirm(args)` | Show confirmation dialog before proceeding |
## ListItem Properties
```csharp
new ListItem(command)
{
Title = "Display name",
Subtitle = "Secondary text",
Icon = new IconInfo("\uE8A7"),
Tags = [new Tag("label") { Foreground = ColorHelpers.FromRgb(255, 0, 0) }],
Details = new Details
{
Title = "Detail panel",
Body = "**Markdown** body",
HeroImage = IconHelpers.FromRelativePath("Assets\\hero.png"),
Size = ContentSize.Medium,
Metadata = [
new DetailsLink("URL", "https://example.com"),
new DetailsSeparator(),
],
},
MoreCommands = [
new CommandContextItem(deleteCommand)
{
RequestedShortcut = KeyChordHelpers.FromModifiers(
true, false, false, (int)VirtualKey.Delete),
},
],
}
```
## Sections and Grid Layouts
### Sections
Group items under section headers:
```csharp
public override ISection[] GetSections() => [
new Section { Title = "Group A", Items = itemsA },
new Section { Title = "Group B", Items = itemsB },
];
```
### Grid Layouts
Set `GridProperties` on a `ListPage`:
| Layout | Description |
|--------|-------------|
| `GalleryGridLayout()` | Large tiles with title + subtitle |
| `SmallGridLayout()` | Compact grid |
| `MediumGridLayout()` | Medium tiles with title |
## Icons
```csharp
// Segoe Fluent UI icons (most common)
new IconInfo("\uE8A5") // Document
new IconInfo("\uE945") // Lightning bolt
// Emoji
new IconInfo("📂")
// Image from package assets
IconHelpers.FromRelativePath("Assets\\StoreLogo.png")
// Remote URL or SVG
new IconInfo("https://example.com/icon.svg")
// From exe/dll resource
new IconInfo("%systemroot%\\system32\\shell32.dll,3")
```
## Dynamic Updates
- Call `RaiseItemsChanged()` on any page to trigger a UI refresh of its items
- Call `RaisePropertyChanged(propertyName)` for individual property updates (e.g., title)
- For top-level command changes, call `RaiseItemsChanged()` on the `CommandProvider`
- Use `System.Timers.Timer` for periodic background updates
## Status Messages and Toasts
```csharp
// Inline status message (e.g., loading indicator)
var msg = new StatusMessage
{
Message = "Loading...",
State = MessageState.Info,
Progress = new ProgressState { IsIndeterminate = true },
};
ExtensionHost.ShowStatus(msg, StatusContext.Page);
ExtensionHost.HideStatus(msg);
// Transient toast notification
new ToastStatusMessage("Copied to clipboard").Show();
```
## Build & Debug
1. Select **Debug** configuration
2. **Deploy** via Build > Deploy (not just Build) — this registers the MSIX package
3. Press **F5** to launch with debugger attached
4. Use `Debug.Write()` / `Debug.WriteLine()` for diagnostic output
5. Check Output window (**Ctrl+Alt+O**) set to "Debug"
6. In Command Palette, run `Reload` → "Reload Command Palette extensions"
Use the `(Package)` launch profile, not `(Unpackaged)`.
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Building without deploying | Use Build > Deploy so the MSIX package is updated |
| Running "(Unpackaged)" profile | Select the "(Package)" launch profile |
| Forgetting to reload extensions | Run `Reload` in Command Palette after deploying |
| CLSID mismatch | Ensure `[Guid]` in .cs matches `ClassId` in Package.appxmanifest (both places) |
| Logging in hot paths | `GetItems()` is called frequently — avoid expensive work or logging here |

View File

@@ -0,0 +1,145 @@
---
name: add-adaptive-card-form
description: >-
Create form-based UI for your Command Palette extension using Adaptive Cards.
Use when asked to add forms, user input fields, toggle switches, text inputs,
dropdown menus, data entry, surveys, configuration dialogs, or interactive
content pages. Supports the Adaptive Cards Designer for visual form building.
---
# Add Forms with Adaptive Cards
Create interactive forms in your Command Palette extension using Adaptive Cards. Forms allow you to collect user input through text fields, toggles, dropdowns, and other controls.
## When to Use This Skill
- Adding a form to collect user input (name, settings, feedback)
- Creating interactive configuration dialogs
- Building data entry interfaces
- Adding toggle switches or dropdown menus
- Displaying complex layouts beyond simple lists
## Prerequisites
- Familiarity with [Adaptive Cards](https://adaptivecards.io/)
- Optional: Use the [Adaptive Card Designer](https://adaptivecards.io/designer/) to visually build your form
## Quick Start
### Step 1: Create a ContentPage with FormContent
Create a new file in your `Pages/` directory:
```csharp
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using System.Text.Json.Nodes;
namespace YourExtension;
internal sealed partial class MyFormPage : ContentPage
{
private readonly MyForm _form = new();
public MyFormPage()
{
Name = "Open";
Title = "My Form";
Icon = new IconInfo("\uECA5");
}
public override IContent[] GetContent() => [_form];
}
internal sealed partial class MyForm : FormContent
{
public MyForm()
{
TemplateJson = """
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.Text",
"label": "Name",
"id": "Name",
"isRequired": true,
"errorMessage": "Name is required",
"placeholder": "Enter your name"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
}
]
}
""";
}
public override CommandResult SubmitForm(string payload)
{
var formInput = JsonNode.Parse(payload)?.AsObject();
if (formInput == null)
{
return CommandResult.GoHome();
}
var name = formInput["Name"]?.ToString() ?? "Unknown";
return CommandResult.ShowToast($"Hello, {name}!");
}
}
```
### Step 2: Register the Page
In your `CommandsProvider`, add the form page:
```csharp
_commands = [
new CommandItem(new MyFormPage()) { Title = "My Form" },
];
```
### Step 3: Deploy and Test
1. Deploy your extension
2. In Command Palette, run `Reload`
3. Navigate to your form and submit it
## Key Concepts
### TemplateJson
The JSON layout of your form (from Adaptive Cards schema). Design it at https://adaptivecards.io/designer/
### DataJson (Optional)
Dynamic data binding using `${...}` placeholders in your TemplateJson:
```csharp
TemplateJson = """{ "body": [{ "type": "TextBlock", "text": "${title}" }] }""";
DataJson = """{ "title": "Dynamic Title" }""";
```
### SubmitForm
Called when the user submits. Parse `payload` as JSON to read input values by their `id`.
### Mixing Content Types
You can combine forms with markdown on the same page:
```csharp
public override IContent[] GetContent() => [
new MarkdownContent("# Instructions\nFill out the form below."),
_form,
];
```
## Common Form Patterns
See [form-patterns.md](references/form-patterns.md) for template JSON for common form types.
## Documentation
- [Get user input with forms](https://learn.microsoft.com/windows/powertoys/command-palette/using-form-pages)
- [Adaptive Card Designer](https://adaptivecards.io/designer/)
- [Adaptive Cards Schema](https://adaptivecards.io/explorer/)

View File

@@ -0,0 +1,536 @@
# Common Adaptive Card Form Patterns
Reusable template JSON and handler code for the most common form types in Command Palette extensions.
---
## Simple Text Input Form
A basic form with one or two text fields and a submit button.
### TemplateJson
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.Text",
"id": "FirstName",
"label": "First Name",
"placeholder": "Enter your first name",
"isRequired": true,
"errorMessage": "First name is required"
},
{
"type": "Input.Text",
"id": "Email",
"label": "Email Address",
"placeholder": "user@example.com",
"style": "Email",
"isRequired": true,
"errorMessage": "A valid email is required"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
}
]
}
```
### SubmitForm Handler
```csharp
public override CommandResult SubmitForm(string payload)
{
var input = JsonNode.Parse(payload)?.AsObject();
if (input == null) return CommandResult.GoHome();
var firstName = input["FirstName"]?.ToString() ?? "";
var email = input["Email"]?.ToString() ?? "";
return CommandResult.ShowToast($"Registered {firstName} ({email})");
}
```
---
## Toggle/Checkbox Form
Use `Input.Toggle` for boolean on/off settings. Combine with `DataJson` for dynamic defaults.
### TemplateJson
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "Preferences",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "Input.Toggle",
"id": "AcceptsTerms",
"title": "I accept the terms and conditions",
"valueOn": "true",
"valueOff": "false",
"value": "false"
},
{
"type": "Input.Toggle",
"id": "EnableNotifications",
"title": "Enable notifications",
"valueOn": "true",
"valueOff": "false",
"value": "${notificationsDefault}"
},
{
"type": "Input.Toggle",
"id": "DarkMode",
"title": "Use dark mode",
"valueOn": "true",
"valueOff": "false",
"value": "${darkModeDefault}"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Save Preferences"
}
]
}
```
### DataJson (Dynamic Defaults)
```csharp
DataJson = """
{
"notificationsDefault": "true",
"darkModeDefault": "false"
}
""";
```
### SubmitForm Handler
```csharp
public override CommandResult SubmitForm(string payload)
{
var input = JsonNode.Parse(payload)?.AsObject();
if (input == null) return CommandResult.GoHome();
var accepted = input["AcceptsTerms"]?.ToString() == "true";
var notifications = input["EnableNotifications"]?.ToString() == "true";
var darkMode = input["DarkMode"]?.ToString() == "true";
if (!accepted)
{
return CommandResult.ShowToast("You must accept the terms to continue.");
}
// Save preferences...
return CommandResult.ShowToast("Preferences saved!");
}
```
---
## Choice Set (Dropdown/Radio) Form
Use `Input.ChoiceSet` for single-select dropdowns or radio buttons.
### Compact Style (Dropdown)
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.ChoiceSet",
"id": "Priority",
"label": "Priority Level",
"style": "compact",
"value": "medium",
"choices": [
{ "title": "Low", "value": "low" },
{ "title": "Medium", "value": "medium" },
{ "title": "High", "value": "high" },
{ "title": "Critical", "value": "critical" }
]
},
{
"type": "Input.ChoiceSet",
"id": "Category",
"label": "Category",
"style": "compact",
"choices": [
{ "title": "Bug Report", "value": "bug" },
{ "title": "Feature Request", "value": "feature" },
{ "title": "Documentation", "value": "docs" },
{ "title": "Question", "value": "question" }
]
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Create Issue"
}
]
}
```
### Expanded Style (Radio Buttons)
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.ChoiceSet",
"id": "Theme",
"label": "Select a theme",
"style": "expanded",
"value": "system",
"choices": [
{ "title": "Light", "value": "light" },
{ "title": "Dark", "value": "dark" },
{ "title": "System Default", "value": "system" }
]
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Apply"
}
]
}
```
---
## Multi-Section Form
Combine multiple input types with TextBlock headers to create organized, multi-section forms. Use `Action.ShowCard` for progressive disclosure of optional sections.
### TemplateJson
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "Personal Information",
"weight": "Bolder",
"size": "Medium",
"separator": true
},
{
"type": "Input.Text",
"id": "FullName",
"label": "Full Name",
"placeholder": "Enter your full name",
"isRequired": true,
"errorMessage": "Name is required"
},
{
"type": "Input.Text",
"id": "Email",
"label": "Email",
"placeholder": "user@example.com",
"style": "Email"
},
{
"type": "TextBlock",
"text": "Preferences",
"weight": "Bolder",
"size": "Medium",
"separator": true,
"spacing": "Large"
},
{
"type": "Input.ChoiceSet",
"id": "Language",
"label": "Preferred Language",
"style": "compact",
"value": "en",
"choices": [
{ "title": "English", "value": "en" },
{ "title": "Spanish", "value": "es" },
{ "title": "French", "value": "fr" },
{ "title": "German", "value": "de" }
]
},
{
"type": "Input.Toggle",
"id": "Newsletter",
"title": "Subscribe to newsletter",
"valueOn": "true",
"valueOff": "false",
"value": "true"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Save Profile"
},
{
"type": "Action.ShowCard",
"title": "Advanced Options",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"id": "ApiKey",
"label": "API Key (optional)",
"placeholder": "Enter your API key"
},
{
"type": "Input.Toggle",
"id": "DebugMode",
"title": "Enable debug mode",
"valueOn": "true",
"valueOff": "false",
"value": "false"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Save All"
}
]
}
}
]
}
```
---
## Feedback Form
A common pattern for collecting user feedback with a multiline text area and a rating.
### TemplateJson
```json
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "We'd love your feedback!",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "TextBlock",
"text": "Tell us what you think and how we can improve.",
"wrap": true,
"spacing": "Small"
},
{
"type": "Input.ChoiceSet",
"id": "Rating",
"label": "How would you rate your experience?",
"style": "expanded",
"isRequired": true,
"errorMessage": "Please select a rating",
"choices": [
{ "title": "⭐ Poor", "value": "1" },
{ "title": "⭐⭐ Fair", "value": "2" },
{ "title": "⭐⭐⭐ Good", "value": "3" },
{ "title": "⭐⭐⭐⭐ Great", "value": "4" },
{ "title": "⭐⭐⭐⭐⭐ Excellent", "value": "5" }
]
},
{
"type": "Input.Text",
"id": "Comments",
"label": "Comments",
"placeholder": "Share your thoughts...",
"isMultiline": true,
"maxLength": 500
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Send Feedback"
}
]
}
```
### SubmitForm Handler with Confirmation Dialog
```csharp
public override CommandResult SubmitForm(string payload)
{
var input = JsonNode.Parse(payload)?.AsObject();
if (input == null) return CommandResult.GoHome();
var rating = input["Rating"]?.ToString() ?? "0";
var comments = input["Comments"]?.ToString() ?? "";
return CommandResult.Confirm(new ConfirmationArgs
{
Title = "Submit feedback?",
Description = $"Rating: {rating}/5\n\n{(string.IsNullOrEmpty(comments) ? "No comments" : comments)}",
PrimaryCommand = new AnonymousCommand(() =>
{
// Process and store feedback
new ToastStatusMessage("Thank you for your feedback!").Show();
})
{
Name = "Submit",
Result = CommandResult.Dismiss(),
},
});
}
```
---
## Tree Content with Forms (Comment/Reply Pattern)
Use `TreeContent` to create nested, threaded discussions where each node can contain a form for replies.
### Post Content (Tree Node)
```csharp
internal sealed partial class PostContent : TreeContent
{
private readonly string _author;
private readonly string _body;
private readonly PostReplyForm _replyForm;
private readonly List<PostContent> _childPosts = [];
public PostContent(string author, string body)
{
_author = author;
_body = body;
_replyForm = new PostReplyForm(this);
TemplateJson = """
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "TextBlock",
"text": "${author}",
"weight": "Bolder"
},
{
"type": "TextBlock",
"text": "${body}",
"wrap": true
}
]
}
""";
DataJson = $$"""{ "author": "{{_author}}", "body": "{{_body}}" }""";
}
public override IContent[] GetChildren() => [_replyForm, .. _childPosts];
public void AddReply(PostContent reply) => _childPosts.Add(reply);
}
```
### Reply Form (Child of Tree Node)
```csharp
internal sealed partial class PostReplyForm : FormContent
{
private readonly PostContent _parent;
public PostReplyForm(PostContent parent)
{
_parent = parent;
TemplateJson = """
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.6",
"body": [
{
"type": "Input.Text",
"id": "ReplyText",
"placeholder": "Write a reply...",
"isMultiline": true
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Reply"
}
]
}
""";
}
public override CommandResult SubmitForm(string payload)
{
var input = JsonNode.Parse(payload)?.AsObject();
if (input == null) return CommandResult.GoHome();
var replyText = input["ReplyText"]?.ToString();
if (!string.IsNullOrWhiteSpace(replyText))
{
_parent.AddReply(new PostContent("You", replyText));
}
return CommandResult.KeepOpen();
}
}
```
### Hosting the Thread on a ContentPage
```csharp
internal sealed partial class ThreadPage : ContentPage
{
private readonly PostContent _rootPost;
public ThreadPage()
{
Name = "Discussion";
Title = "Discussion Thread";
Icon = new IconInfo("\uE90A");
_rootPost = new PostContent("Alice", "Has anyone tried the new API?");
_rootPost.AddReply(new PostContent("Bob", "Yes! It works great."));
}
public override IContent[] GetContent() => [_rootPost];
}
```

View File

@@ -0,0 +1,149 @@
---
name: add-dock-band
description: >-
Add dock band support to your Command Palette extension for persistent toolbar widgets.
Use when asked to add dock support, toolbar buttons, persistent UI widgets,
taskbar integration, live-updating status displays, quick-access buttons,
or always-visible controls. Supports single buttons, multi-button strips,
and live-updating content.
---
# Add Dock Band Support
The Command Palette Dock is a persistent toolbar at the edge of the user's screen. Your extension can provide **dock bands** — strips of items that appear in the Dock — giving users quick access to commands without opening the full Command Palette.
## When to Use This Skill
- Adding a quick-access button to the persistent toolbar
- Creating a multi-button toolbar strip
- Displaying live-updating information (clock, CPU usage, etc.)
- Providing frequently-used commands without opening the full palette
## Prerequisites
- Command Palette Extension SDK version 0.9 or later (`Microsoft.CommandPalette.Extensions` ≥ 0.9.260303001)
## Quick Start: Single Button Dock Band
Override `GetDockBands()` in your `CommandProvider`:
```csharp
public partial class MyCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly ICommandItem _dockBand;
public MyCommandsProvider()
{
DisplayName = "My Extension";
Id = "com.mycompany.myextension"; // Unique ID required for dock
var mainPage = new MyPage();
_dockBand = new CommandItem(mainPage) { Title = DisplayName };
_commands = [new CommandItem(mainPage) { Title = DisplayName }];
}
public override ICommandItem[] TopLevelCommands() => _commands;
public override ICommandItem[]? GetDockBands() => [_dockBand];
}
```
## Multi-Button Dock Band
Use `WrappedDockItem` to create a band with multiple buttons:
```csharp
public override ICommandItem[]? GetDockBands()
{
var button1 = new ListItem(new OpenUrlCommand("https://github.com"))
{
Title = "GitHub",
Icon = new IconInfo("\uE774"),
};
var button2 = new ListItem(new OpenUrlCommand("https://learn.microsoft.com"))
{
Title = "Learn",
Icon = new IconInfo("\uE82D"),
};
var band = new WrappedDockItem(
[button1, button2],
"com.mycompany.myextension.quicklinks", // Unique band ID
"Quick Links");
return [band];
}
```
## Live-Updating Dock Band
Create a dock band that updates its content periodically (like a clock):
```csharp
internal sealed partial class LiveStatusBand : ListItem
{
private readonly System.Timers.Timer _timer;
public LiveStatusBand()
: base(new NoOpCommand() { Result = CommandResult.KeepOpen() })
{
Title = DateTime.Now.ToString("HH:mm");
Icon = new IconInfo("\uE823"); // Clock icon
_timer = new System.Timers.Timer(60_000); // Update every minute
_timer.Elapsed += (s, e) =>
{
Title = DateTime.Now.ToString("HH:mm");
Subtitle = DateTime.Now.ToString("dddd, MMMM d");
};
_timer.Start();
}
}
// In CommandProvider:
public override ICommandItem[]? GetDockBands()
{
var band = new WrappedDockItem(
[new LiveStatusBand()],
"com.mycompany.myextension.status",
"Live Status");
return [band];
}
```
## How Dock Bands Render
| Command Type on ICommandItem | Dock Behavior |
|------------------------------|---------------|
| `IInvokableCommand` | Single button that executes the command |
| `IListPage` | Each list item renders as a separate button in one band |
| `IContentPage` | Single expandable button with a flyout |
## Support Pinning Nested Commands
By default, only top-level commands and dock bands can be pinned. To allow pinning nested commands:
```csharp
public override ICommandItem? GetCommandItem(string id)
{
// Look up commands by their Id
foreach (var item in GetAllCommands())
{
if (item?.Command is ICommand cmd && cmd.Id == id)
return item;
}
return null;
}
```
## Important Notes
- All dock band `ICommandItem` objects must have a `Command` with a **non-empty `Id`** — items without an ID are ignored
- Set `Id` on your `CommandProvider` (e.g., `Id = "com.mycompany.myextension"`)
- Use `WrappedDockItem` for multi-button bands backed by a `ListPage`
- Keep dock band updates lightweight — they run frequently
## Documentation
- [Adding Dock support](https://learn.microsoft.com/windows/powertoys/command-palette/adding-dock-support)

View File

@@ -0,0 +1,202 @@
---
name: add-extension-settings
description: >-
Add a settings page to your Command Palette extension.
Use when asked to add settings, preferences, configuration options,
toggles, text inputs, dropdowns, or user-customizable behavior.
Covers ToggleSetting, TextSetting, ChoiceSetSetting, and persistence.
---
# Add Extension Settings
Add a settings page to your Command Palette extension using the built-in settings helpers. Settings are automatically persisted and restored by the extension host.
## When to Use This Skill
- Adding user-configurable options to your extension
- Creating toggle switches for features
- Adding text input fields for configuration
- Creating dropdown menus for option selection
- Persisting user preferences across sessions
## Quick Start
### Step 1: Create a Settings Manager
Create a new file `SettingsManager.cs`:
```csharp
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace YourExtension;
internal sealed class SettingsManager
{
private readonly Settings _settings;
public SettingsManager()
{
_settings = new Settings();
var maxResults = new TextSetting(
"maxResults",
"Maximum Results",
"Maximum number of results to display",
"10");
var showSubtitles = new ToggleSetting(
"showSubtitles",
"Show Subtitles",
"Display subtitle text under each result",
true);
var sortOrder = new ChoiceSetSetting(
"sortOrder",
"Sort Order",
"How to sort results",
[
new ChoiceSetSetting.Choice("Alphabetical", "alpha"),
new ChoiceSetSetting.Choice("Most Recent", "recent"),
new ChoiceSetSetting.Choice("Most Used", "frequent"),
],
"alpha");
_settings.AddSetting(maxResults);
_settings.AddSetting(showSubtitles);
_settings.AddSetting(sortOrder);
// React to settings changes
_settings.SettingsChanged += OnSettingsChanged;
}
public ICommandSettings Settings => _settings;
public int MaxResults => int.TryParse(
_settings.GetSetting<string>("maxResults"), out var val) ? val : 10;
public bool ShowSubtitles =>
_settings.GetSetting<bool>("showSubtitles");
public string SortOrder =>
_settings.GetSetting<string>("sortOrder") ?? "alpha";
private void OnSettingsChanged(object? sender, EventArgs e)
{
// React to settings changes (e.g., refresh data)
}
}
```
### Step 2: Wire into CommandProvider
In your `CommandsProvider`, expose the settings:
```csharp
public partial class MyCommandsProvider : CommandProvider
{
private readonly SettingsManager _settingsManager = new();
private readonly ICommandItem[] _commands;
public MyCommandsProvider()
{
DisplayName = "My Extension";
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
Settings = _settingsManager.Settings; // This exposes settings to CmdPal
_commands = [
new CommandItem(new MyPage(_settingsManager)) { Title = DisplayName },
];
}
public override ICommandItem[] TopLevelCommands() => _commands;
}
```
### Step 3: Use Settings in Pages
```csharp
internal sealed partial class MyPage : ListPage
{
private readonly SettingsManager _settings;
public MyPage(SettingsManager settings)
{
_settings = settings;
}
public override IListItem[] GetItems()
{
var items = GetAllItems();
return items.Take(_settings.MaxResults).ToArray();
}
}
```
## Setting Types
| Type | UI Control | Value Type | Constructor Parameters |
|------|-----------|------------|----------------------|
| `ToggleSetting` | Toggle switch | `bool` | `(id, label, description, defaultValue)` |
| `TextSetting` | Text input | `string` | `(id, label, description, defaultValue)` |
| `ChoiceSetSetting` | Dropdown | `string` | `(id, label, description, choices[], defaultValue)` |
## Key Points
- Settings are automatically persisted by the CmdPal host
- Use `SettingsChanged` event to react to changes in real-time
- Access values via `GetSetting<T>(id)` with the setting's string id
- Pass the settings manager to pages/commands that need configuration
- Settings page appears automatically when `Settings` is set on `CommandProvider`
## Grouping Settings
For extensions with many settings, organize them into logical groups:
```csharp
public SettingsManager()
{
_settings = new Settings();
// Appearance group
var theme = new ChoiceSetSetting("theme", "Theme", "UI theme",
[
new ChoiceSetSetting.Choice("Light", "light"),
new ChoiceSetSetting.Choice("Dark", "dark"),
new ChoiceSetSetting.Choice("System", "system"),
],
"system");
var fontSize = new TextSetting("fontSize", "Font Size", "Display font size", "14");
// Behavior group
var autoRefresh = new ToggleSetting("autoRefresh", "Auto-Refresh",
"Automatically refresh results", true);
var refreshInterval = new TextSetting("refreshInterval", "Refresh Interval",
"Seconds between auto-refreshes", "30");
_settings.AddSetting(theme);
_settings.AddSetting(fontSize);
_settings.AddSetting(autoRefresh);
_settings.AddSetting(refreshInterval);
}
```
## Reacting to Changes
Use the `SettingsChanged` event to update behavior when the user modifies settings:
```csharp
private void OnSettingsChanged(object? sender, EventArgs e)
{
// Invalidate cached data
_cachedItems = null;
// Notify pages to refresh
OnItemsChanged?.Invoke(this, EventArgs.Empty);
}
```
## Documentation
- [SampleSettingsPage.cs](https://github.com/microsoft/PowerToys/blob/main/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleSettingsPage.cs)

View File

@@ -0,0 +1,164 @@
---
name: add-fallback-commands
description: >-
Add fallback commands to your Command Palette extension for catch-all search behavior.
Use when asked to add search functionality, query matching, direct input handling,
calculator-style evaluation, URL opening, command execution, or results that appear
when no other extension matches. Used by 14 of 20 built-in extensions.
---
# Add Fallback Commands
Fallback commands are shown in Command Palette when no other results match the user's query. They enable your extension to act as a catch-all handler — perfect for calculators, web search, command execution, file path opening, and more.
## When to Use This Skill
- Adding search functionality that responds to any user input
- Creating a calculator that evaluates expressions as the user types
- Building a web search that triggers on unmatched queries
- Opening files or URLs typed directly into the palette
- Executing shell commands from the search bar
## How Fallback Commands Work
1. User types a query in Command Palette
2. If no top-level commands match, CmdPal asks extensions for fallback results
3. Your extension's `FallbackCommands()` provides items that respond to the query
4. The fallback items can be static (always shown) or dynamic (filtered by query)
## Quick Start: Static Fallback
Override `FallbackCommands()` in your `CommandProvider`:
```csharp
public partial class MyCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly FallbackCommandItem[] _fallbacks;
public MyCommandsProvider()
{
DisplayName = "Web Search";
Icon = new IconInfo("\uE721"); // Search icon
var searchPage = new WebSearchPage();
_commands = [new CommandItem(searchPage) { Title = DisplayName }];
_fallbacks = [new FallbackCommandItem(searchPage) { Title = "Search the web" }];
}
public override ICommandItem[] TopLevelCommands() => _commands;
public override IFallbackCommandItem[] FallbackCommands() => _fallbacks;
}
```
## Dynamic Fallback with DynamicListPage
For fallbacks that filter results based on the query, use `DynamicListPage`:
```csharp
internal sealed partial class WebSearchPage : DynamicListPage
{
private string _query = string.Empty;
public WebSearchPage()
{
Icon = new IconInfo("\uE721");
Title = "Web Search";
Name = "Search";
PlaceholderText = "Type to search...";
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
_query = newSearch;
RaiseItemsChanged();
}
public override IListItem[] GetItems()
{
if (string.IsNullOrWhiteSpace(_query))
return [];
return [
new ListItem(new OpenUrlCommand($"https://www.google.com/search?q={Uri.EscapeDataString(_query)}"))
{
Title = $"Search Google for \"{_query}\"",
Icon = new IconInfo("\uE721"),
},
new ListItem(new OpenUrlCommand($"https://www.bing.com/search?q={Uri.EscapeDataString(_query)}"))
{
Title = $"Search Bing for \"{_query}\"",
Icon = new IconInfo("\uE721"),
},
];
}
}
```
## Responsive Fallback with Cancellation
For expensive operations (API calls, file searches), use cancellation to stay responsive:
```csharp
internal sealed partial class SmartSearchPage : DynamicListPage
{
private CancellationTokenSource? _cts;
private IListItem[] _results = [];
public override void UpdateSearchText(string oldSearch, string newSearch)
{
// Cancel any in-flight search
_cts?.Cancel();
_cts = new CancellationTokenSource();
var token = _cts.Token;
_ = Task.Run(async () =>
{
// Debounce: wait for user to stop typing
await Task.Delay(300, token);
if (token.IsCancellationRequested) return;
// Perform search
_results = await SearchAsync(newSearch, token);
RaiseItemsChanged();
}, token);
}
public override IListItem[] GetItems() => _results;
private async Task<IListItem[]> SearchAsync(string query, CancellationToken token)
{
// Your search logic here
// Check token.IsCancellationRequested periodically
return [];
}
}
```
## Real-World Examples (from built-in extensions)
| Extension | Fallback Behavior |
|-----------|------------------|
| **Apps** | Search installed applications by name |
| **Calc** | Evaluate mathematical expressions directly |
| **Shell** | Execute command-line commands |
| **WebSearch** | Search the web with configured engine |
| **Indexer** | Open files by path |
| **TimeDate** | Parse time/date queries |
| **WindowsSettings** | Jump to Windows Settings pages |
| **WinGet** | Search WinGet packages |
| **WindowWalker** | Find and switch to open windows |
## Key Points
- `FallbackCommands()` returns `IFallbackCommandItem[]` (not `ICommandItem[]`)
- Use `FallbackCommandItem` wrapper (not `CommandItem`)
- Wrap a `DynamicListPage` for query-reactive results
- Cancel previous searches when new input arrives
- Keep fallback responses fast — users expect instant results
- Use `PlaceholderText` on your page to guide users
## Documentation
- [Extension samples](https://learn.microsoft.com/windows/powertoys/command-palette/samples)
- [Extensibility overview](https://learn.microsoft.com/windows/powertoys/command-palette/extensibility-overview)

View File

@@ -0,0 +1,66 @@
---
name: publish-extension
description: >-
Publish your Command Palette extension to the Microsoft Store or WinGet.
Use when asked to publish, distribute, release, deploy to store,
create MSIX packages, submit to WinGet, set up CI/CD for releases,
or automate builds with GitHub Actions.
---
# Publish Your Command Palette Extension
Guide for distributing your Command Palette extension through the Microsoft Store, WinGet, or both.
## When to Use This Skill
- Publishing your extension to the Microsoft Store
- Submitting your extension to WinGet for `winget install` discovery
- Setting up GitHub Actions to automate builds and releases
- Creating MSIX packages for Store submission
- Creating EXE installers for WinGet submission
## Publishing Options
| Channel | Package Format | Discovery | Auto-Updates |
|---------|---------------|-----------|--------------|
| Microsoft Store | MSIX bundle | Store app, `ms-windows-store://` link | Yes |
| WinGet | EXE installer | `winget install`, CmdPal browse | Yes (via manifest) |
**Recommendation**: Publish to both for maximum reach. WinGet enables direct discovery from within Command Palette.
## Workflows
### Microsoft Store Publishing
See [store-publishing.md](references/store-publishing.md) for the complete step-by-step guide.
**Summary:**
1. Register for Partner Center
2. Update `Package.appxmanifest` and `.csproj` with Partner Center identity
3. Build MSIX for x64 and ARM64
4. Create MSIX bundle
5. Submit to Partner Center
### WinGet Publishing
See [winget-publishing.md](references/winget-publishing.md) for the complete step-by-step guide.
**Summary:**
1. Switch project to unpackaged mode
2. Create Inno Setup installer script
3. Build EXE installers
4. Submit manifest via `wingetcreate new`
5. Optionally automate with GitHub Actions
## Prerequisites
- [Visual Studio](https://visualstudio.microsoft.com/) with C# and WinUI workloads
- [Partner Center account](https://partner.microsoft.com/dashboard/home) (for Store publishing)
- [GitHub CLI](https://cli.github.com/) (for WinGet publishing)
- [WingetCreate](https://github.com/microsoft/winget-create) — `winget install Microsoft.WingetCreate`
- [Inno Setup](https://jrsoftware.org/isdl.php) (for WinGet EXE packaging)
## Important Notes
- Your extension's CLSID (the `[Guid("...")]` in your main .cs file) must be unique and consistent across all files
- WinGet manifests must include the `windows-commandpalette-extension` tag for CmdPal discovery
- MSIX packages require both x64 and ARM64 builds for Store submission
- WindowsAppSdk must be listed as a dependency in WinGet manifests

View File

@@ -0,0 +1,169 @@
# Microsoft Store Publishing Guide
Complete step-by-step guide for publishing your Command Palette extension to the Microsoft Store.
## Step 1: Set Up Microsoft Store
1. Go to [Partner Center](https://partner.microsoft.com/dashboard/home)
2. Navigate to **Apps and Games****New product****MSIX or PWA app**
3. Reserve your app name (e.g., `My Extension for Command Palette`)
4. Once created, go to **Product Management****Product Identity**
5. Copy these three values — you'll need them in the next step:
| Partner Center Field | Where It Goes |
|---------------------|---------------|
| **Package/Identity/Name** | `Package.appxmanifest``Identity Name` and `.csproj``AppxPackageIdentityName` |
| **Package/Identity/Publisher** | `Package.appxmanifest``Identity Publisher` and `.csproj``AppxPackagePublisher` |
| **Package/Properties/PublisherDisplayName** | `Package.appxmanifest``Properties PublisherDisplayName` |
## Step 2: Prepare the Extension
### Update `Package.appxmanifest`
Replace the placeholder identity values with your Partner Center values:
```xml
<Identity
Name="YOUR_PACKAGE_IDENTITY_NAME_HERE"
Publisher="YOUR_PACKAGE_IDENTITY_PUBLISHER_HERE"
Version="0.0.1.0" />
```
And update the publisher display name:
```xml
<Properties>
<DisplayName>Your Extension Name</DisplayName>
<PublisherDisplayName>YOUR_PUBLISHER_DISPLAY_NAME_HERE</PublisherDisplayName>
<!-- ... -->
</Properties>
```
### Update `.csproj`
Add or update the following properties in your `.csproj` file:
```xml
<PropertyGroup>
<AppxPackageIdentityName>YOUR_PACKAGE_IDENTITY_NAME_HERE</AppxPackageIdentityName>
<AppxPackagePublisher>YOUR_PACKAGE_IDENTITY_PUBLISHER_HERE</AppxPackagePublisher>
<AppxPackageVersion>0.0.1.0</AppxPackageVersion>
</PropertyGroup>
```
### Update Image Assets ItemGroup
Ensure all image assets are included in the package by updating the `ItemGroup`:
```xml
<ItemGroup>
<Content Include="Assets\**\*.png" />
</ItemGroup>
```
> **Tip:** The `Assets` folder should contain your Store logos and extension icons at the required sizes (44x44, 150x150, etc.). You can generate these from a single high-resolution image.
## Step 3: Build MSIX Packages
Build for both x64 and ARM64 architectures:
```powershell
# x64 build
dotnet build --configuration Release -p:GenerateAppxPackageOnBuild=true -p:Platform=x64 -p:AppxPackageDir="AppPackages\x64\"
# ARM64 build
dotnet build --configuration Release -p:GenerateAppxPackageOnBuild=true -p:Platform=ARM64 -p:AppxPackageDir="AppPackages\ARM64\"
```
Verify the MSIX files were created:
```powershell
dir AppPackages -Recurse -Filter "*.msix"
```
You should see two `.msix` files, one for each architecture.
## Step 4: Create MSIX Bundle
### Create the bundle mapping file
Create a file named `bundle_mapping.txt` that maps each MSIX to its architecture:
```text
[Files]
"AppPackages\x64\YourExtension_0.0.1.0_x64\YourExtension_0.0.1.0_x64.msix" "YourExtension_0.0.1.0_x64.msix"
"AppPackages\ARM64\YourExtension_0.0.1.0_ARM64\YourExtension_0.0.1.0_ARM64.msix" "YourExtension_0.0.1.0_ARM64.msix"
```
> **Note:** Update the paths and filenames to match your actual build output. Check the `AppPackages` directory structure after building.
### Run makeappx
```powershell
makeappx bundle /f bundle_mapping.txt /p YourExtension_0.0.1.0_Bundle.msixbundle
```
> **Tip:** `makeappx.exe` is included with the Windows SDK. If it's not in your PATH, find it at:
> `C:\Program Files (x86)\Windows Kits\10\bin\<version>\x64\makeappx.exe`
## Step 5: Submit to Partner Center
1. Go to [Partner Center](https://partner.microsoft.com/dashboard/home)
2. Navigate to your app → **Start a new submission**
3. In **Packages**, upload your `.msixbundle` file
4. In **Store Listings****Description**, include a note like:
> `YourExtension` integrates with the Windows Command Palette to provide [describe your extension's functionality]. Requires PowerToys with Command Palette enabled.
5. In **Notes for certification**, add testing instructions:
> This extension requires Microsoft PowerToys (available from the Microsoft Store or https://github.com/microsoft/PowerToys) with the Command Palette feature enabled. To test:
> 1. Install PowerToys and enable Command Palette
> 2. Install this extension
> 3. Open Command Palette (Win+Alt+Space by default)
> 4. Search for [your extension's commands]
6. Set **Availability** and pricing as appropriate
7. Click **Submit for certification**
Certification typically takes 13 business days.
## Validation Checklist
Before submitting, verify:
- [ ] Partner Center identity values match exactly in both `Package.appxmanifest` and `.csproj`
- [ ] `AppxPackageVersion` is set correctly and incremented from any previous submission
- [ ] Both x64 and ARM64 MSIX files are built successfully
- [ ] MSIX bundle is created without errors
- [ ] Extension installs and runs correctly from the MSIX package locally
- [ ] Store listing includes clear description mentioning Command Palette integration
- [ ] Testing instructions mention the PowerToys/Command Palette prerequisite
- [ ] All required Store logos and screenshots are provided
- [ ] Privacy policy URL is set (if your extension accesses network or user data)
## Store-Only Discovery Limitations
> **Important:** Command Palette cannot currently search for extensions published only to the Microsoft Store via its built-in browse experience. Users can find Store-published extensions through:
>
> - Direct Store link shared by the developer
> - The Store's extension tag URL:
> ```
> ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette
> ```
> - Searching the Store app directly
>
> For discoverability within Command Palette's browse experience, also publish to WinGet.
> See [winget-publishing.md](winget-publishing.md) for details.
## Updating Your Extension
To publish an update:
1. Increment the version in `.csproj` (`AppxPackageVersion`) and `Package.appxmanifest`
2. Rebuild MSIX packages for both architectures
3. Recreate the MSIX bundle with updated filenames
4. Create a new submission in Partner Center and upload the new bundle
5. Submit for certification
The Store will automatically update users who have installed your extension.

View File

@@ -0,0 +1,413 @@
# WinGet Publishing Guide
Complete step-by-step guide for publishing your Command Palette extension to WinGet for `winget install` discovery and installation.
## Why WinGet?
Publishing to WinGet enables:
- Users to install via `winget install YourPublisher.YourExtension`
- Discovery directly inside Command Palette's built-in browse experience
- Automatic update detection via WinGet manifests
## Step 1: Prepare the Project for Unpackaged Distribution
WinGet distribution uses an unpackaged (EXE-based) build instead of MSIX.
### Update `.csproj`
Remove any existing `<PublishProfile>` property and add unpackaged mode:
```xml
<PropertyGroup>
<!-- Remove or comment out this line if present: -->
<!-- <PublishProfile>win-$(Platform)</PublishProfile> -->
<!-- Add this for unpackaged distribution: -->
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup>
```
### Note Your CLSID
Find the `[Guid("...")]` attribute in your main `.cs` file (e.g., `SampleExtension.cs`):
```csharp
[Guid("YOUR-GUID-HERE")]
public sealed partial class SampleExtension : IExtension
```
You'll need this exact GUID for the installer script. It must match across all files.
## Step 2: Create Installer Scripts
### Inno Setup Script: `setup-template.iss`
Create this file in your project root. Replace all `TODO` placeholders with your values:
```iss
; Inno Setup script for Command Palette extension
#define MyAppName "TODO_YOUR_EXTENSION_NAME"
#define MyAppVersion "TODO_YOUR_VERSION"
#define MyAppPublisher "TODO_YOUR_PUBLISHER_NAME"
#define MyAppURL "TODO_YOUR_PROJECT_URL"
#define MyAppCLSID "TODO_YOUR_CLSID_WITH_BRACES"
; Example CLSID: {12345678-1234-1234-1234-123456789012}
[Setup]
AppId={#MyAppCLSID}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
OutputBaseFilename={#MyAppName}_{#MyAppVersion}_{#SetupSetting("ArchitecturesAllowed")}
Compression=lzma
SolidCompression=yes
WizardStyle=modern
PrivilegesRequired=lowest
OutputDir=Installer
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Registry]
; Register the COM server for Command Palette discovery
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppName}"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}\InprocServer32"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppName}.dll"; Flags: uninsdeletekey
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}\InprocServer32"; ValueType: string; ValueName: "ThreadingModel"; ValueData: "Both"; Flags: uninsdeletekey
[UninstallDelete]
Type: filesandordirs; Name: "{app}"
```
> **Important:** The `AppId` must use your CLSID wrapped in braces. The registry entries register your extension's COM server so Command Palette can discover it.
### Build Script: `build-exe.ps1`
Create this PowerShell script in your project root:
```powershell
<#
.SYNOPSIS
Builds EXE installers for x64 and ARM64 using dotnet publish and Inno Setup.
.DESCRIPTION
Publishes the project for both architectures, then runs Inno Setup to create
EXE installers suitable for WinGet submission.
#>
param(
[string]$Configuration = "Release",
[string]$Version = "0.0.1"
)
$ErrorActionPreference = "Stop"
$projectName = (Get-ChildItem -Filter "*.csproj" | Select-Object -First 1).BaseName
if (-not $projectName) {
Write-Error "No .csproj file found in the current directory."
exit 1
}
$architectures = @("x64", "arm64")
foreach ($arch in $architectures) {
Write-Host "`n=== Building $arch ===" -ForegroundColor Cyan
# Publish
Write-Host "Publishing for $arch..."
dotnet publish -c $Configuration -r "win-$arch" -o "publish" --self-contained=false
if ($LASTEXITCODE -ne 0) {
Write-Error "dotnet publish failed for $arch"
exit 1
}
# Create installer
Write-Host "Creating installer for $arch..."
$issFile = "setup-template.iss"
if (-not (Test-Path $issFile)) {
Write-Error "Inno Setup script not found: $issFile"
exit 1
}
$archFlag = if ($arch -eq "arm64") { "arm64" } else { "x64" }
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
/DMyAppVersion="$Version" `
/DArchitecturesAllowed="$archFlag" `
$issFile
if ($LASTEXITCODE -ne 0) {
Write-Error "Inno Setup failed for $arch"
exit 1
}
# Clean publish directory for next architecture
Remove-Item -Recurse -Force "publish" -ErrorAction SilentlyContinue
Write-Host "=== $arch complete ===" -ForegroundColor Green
}
Write-Host "`nInstallers created in the 'Installer' directory:" -ForegroundColor Cyan
Get-ChildItem -Path "Installer" -Filter "*.exe" | ForEach-Object { Write-Host " $_" }
```
## Step 3: Build EXE Installers
Run the build script from your project directory:
```powershell
.\build-exe.ps1
```
This produces two EXE files in the `Installer` directory:
```
Installer\YourExtension_0.0.1_x64.exe
Installer\YourExtension_0.0.1_arm64.exe
```
Verify both installers work by running them locally and confirming your extension appears in Command Palette.
## Step 4: Create a GitHub Release
Tag your repository with the version and create a release with the EXE files:
```powershell
# Tag the release
git tag -a v0.0.1 -m "Release v0.0.1"
git push origin v0.0.1
# Create release and upload assets (requires GitHub CLI)
gh release create v0.0.1 `
"Installer\YourExtension_0.0.1_x64.exe" `
"Installer\YourExtension_0.0.1_arm64.exe" `
--title "v0.0.1" `
--notes "Initial release of YourExtension for Command Palette."
```
After creating the release, copy the download URLs for both EXE files — you'll need them for the WinGet submission.
## Step 5: Submit to WinGet
Use `wingetcreate` to generate a WinGet manifest and submit a pull request:
```powershell
wingetcreate new "<URL_TO_x64.exe>" "<URL_TO_arm64.exe>"
```
`wingetcreate` will interactively prompt you for:
| Prompt | Example Value |
|--------|---------------|
| **PackageIdentifier** | `YourPublisher.YourExtension` |
| **PackageVersion** | `0.0.1` |
| **PackageLocale** | `en-US` |
| **Publisher** | `Your Name` |
| **PackageName** | `YourExtension for Command Palette` |
| **License** | `MIT` |
| **ShortDescription** | `A Command Palette extension that does X` |
After answering all prompts, `wingetcreate` will create a PR against the [winget-pkgs](https://github.com/microsoft/winget-pkgs) repository.
## Step 6: Add the Command Palette Tag (CRITICAL)
> **This step is required for your extension to appear in Command Palette's browse experience.**
After `wingetcreate` generates the manifest files, you **must** edit each `.locale.*.yaml` file to add the Command Palette tag.
In every locale YAML file (e.g., `YourPublisher.YourExtension.locale.en-US.yaml`), add:
```yaml
Tags:
- windows-commandpalette-extension
```
Example of a complete locale file with the tag:
```yaml
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
PackageIdentifier: YourPublisher.YourExtension
PackageVersion: 0.0.1
PackageLocale: en-US
Publisher: Your Name
PackageName: YourExtension for Command Palette
License: MIT
ShortDescription: A Command Palette extension that does X
Tags:
- windows-commandpalette-extension
ManifestType: defaultLocale
ManifestVersion: 1.6.0
```
Without this tag, Command Palette will not discover your extension in its browse experience.
## Step 7: Ensure WindowsAppSdk Dependency
Your WinGet manifest must declare a dependency on the Windows App SDK so it gets installed automatically. In the `installer.yaml` manifest file, add:
```yaml
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.WindowsAppRuntime.1.7
MinimumVersion: 7001.632.252.0
```
> **Note:** Update the version number to match the Windows App SDK version your project targets. Check your `.csproj` for the `WindowsAppSDK` package version.
## Step 8: GitHub Actions Automation (Optional)
Automate your build, release, and WinGet submission process with GitHub Actions.
### Release Workflow: `.github/workflows/release-extension.yml`
```yaml
name: Release Extension
on:
push:
tags:
- 'v*'
permissions:
contents: write
env:
PROJECT_NAME: YourExtension
DOTNET_VERSION: '9.0.x'
jobs:
build:
strategy:
matrix:
arch: [x64, arm64]
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install Inno Setup
run: choco install innosetup -y --no-progress
- name: Detect version
id: version
run: |
$tag = "${{ github.ref_name }}" -replace '^v', ''
echo "VERSION=$tag" >> $env:GITHUB_OUTPUT
- name: Publish
run: |
dotnet publish -c Release -r win-${{ matrix.arch }} -o publish --self-contained=false
- name: Create installer
run: |
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
/DMyAppVersion="${{ steps.version.outputs.VERSION }}" `
/DArchitecturesAllowed="${{ matrix.arch }}" `
setup-template.iss
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: installer-${{ matrix.arch }}
path: Installer/*.exe
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/*.exe
generate_release_notes: true
winget-update:
needs: release
runs-on: windows-latest
steps:
- name: Detect version
id: version
run: |
$tag = "${{ github.ref_name }}" -replace '^v', ''
echo "VERSION=$tag" >> $env:GITHUB_OUTPUT
- name: Update WinGet manifest
run: |
$baseUrl = "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}"
wingetcreate update YourPublisher.YourExtension `
--version ${{ steps.version.outputs.VERSION }} `
--urls "$baseUrl/${{ env.PROJECT_NAME }}_${{ steps.version.outputs.VERSION }}_x64.exe" "$baseUrl/${{ env.PROJECT_NAME }}_${{ steps.version.outputs.VERSION }}_arm64.exe" `
--submit `
--token ${{ secrets.WINGET_PAT }}
```
### Required Secrets
| Secret | Description |
|--------|-------------|
| `WINGET_PAT` | GitHub Personal Access Token with `public_repo` scope, used by `wingetcreate` to submit PRs to `microsoft/winget-pkgs` |
### How It Works
1. **Push a version tag** (e.g., `git tag v0.0.2 && git push origin v0.0.2`)
2. **Build job** runs in parallel for x64 and ARM64, creating EXE installers
3. **Release job** creates a GitHub Release and uploads the EXE files
4. **WinGet update job** automatically submits an updated manifest to `winget-pkgs`
> **Note:** The `winget-update` job uses `wingetcreate update` (not `new`) because it assumes you've already submitted your initial manifest manually. For the first submission, follow Steps 57 above.
## Validation Checklist
Before submitting to WinGet, verify:
- [ ] `.csproj` has `<WindowsPackageType>None</WindowsPackageType>` set
- [ ] CLSID in `setup-template.iss` matches the `[Guid("...")]` in your main `.cs` file
- [ ] Both x64 and ARM64 EXE installers build successfully
- [ ] Installer registers the COM server correctly (check `HKCU\Software\Classes\CLSID\{your-clsid}`)
- [ ] Extension appears in Command Palette after installing via EXE
- [ ] Extension is removed from Command Palette after uninstalling
- [ ] GitHub Release contains both EXE files with correct download URLs
- [ ] WinGet manifest includes `windows-commandpalette-extension` tag
- [ ] WinGet manifest includes `WindowsAppRuntime` dependency
- [ ] `winget validate` passes on all manifest files
## Updating Your Extension on WinGet
For subsequent releases:
```powershell
wingetcreate update YourPublisher.YourExtension `
--version "0.0.2" `
--urls "<URL_TO_NEW_x64.exe>" "<URL_TO_NEW_arm64.exe>" `
--submit
```
Or simply push a new version tag if you've set up the GitHub Actions workflow above.
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Extension not appearing in CmdPal browse | Verify the `windows-commandpalette-extension` tag is in your locale YAML |
| COM registration fails | Check that the CLSID matches exactly and registry paths are correct |
| `wingetcreate` validation errors | Run `winget validate --manifest <path>` and fix reported issues |
| Installer doesn't run silently | Add `/VERYSILENT /SUPPRESSMSGBOXES` flags for silent install support |
| Missing WindowsAppSdk at runtime | Ensure the `PackageDependencies` section is in your installer manifest |

View File

@@ -12,7 +12,7 @@ public struct InterlockedBoolean(bool initialValue = false)
private int _value = initialValue ? 1 : 0;
/// <summary>
/// Gets or sets the boolean value atomically
/// Gets or sets a value indicating whether the atomic boolean is true.
/// </summary>
public bool Value
{

View File

@@ -0,0 +1,68 @@
// 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.Extensions.Logging;
using MEL = Microsoft.Extensions.Logging;
namespace Microsoft.CmdPal.Common.Logging;
/// <summary>
/// An <see cref="ILogger"/> implementation that delegates to <see cref="ManagedCommon.Logger"/>.
/// Instances are created by <see cref="CmdPalLoggerProvider"/>.
/// </summary>
public sealed class CmdPalLogger(string categoryName) : MEL.ILogger
{
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
ArgumentNullException.ThrowIfNull(formatter);
var message = $"[{categoryName}] {formatter(state, exception)}";
switch (logLevel)
{
case LogLevel.Trace:
ManagedCommon.Logger.LogTrace(message);
break;
case LogLevel.Debug:
ManagedCommon.Logger.LogDebug(message);
break;
case LogLevel.Information:
ManagedCommon.Logger.LogInfo(message);
break;
case LogLevel.Warning:
ManagedCommon.Logger.LogWarning(message);
break;
case LogLevel.Error:
case LogLevel.Critical:
if (exception is not null)
{
ManagedCommon.Logger.LogError(message, exception);
}
else
{
ManagedCommon.Logger.LogError(message);
}
break;
case LogLevel.None:
default:
break;
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Concurrent;
using MEL = Microsoft.Extensions.Logging;
namespace Microsoft.CmdPal.Common.Logging;
/// <summary>
/// An <see cref="MEL.ILoggerProvider"/> that creates <see cref="CmdPalLogger"/> instances
/// backed by the <see cref="ManagedCommon.Logger"/> infrastructure.
/// Register via <see cref="CmdPalLoggingExtensions.AddCmdPalLogging"/>.
/// </summary>
public sealed partial class CmdPalLoggerProvider : MEL.ILoggerProvider
{
private readonly ConcurrentDictionary<string, CmdPalLogger> _loggers = new(StringComparer.OrdinalIgnoreCase);
public MEL.ILogger CreateLogger(string categoryName) =>
_loggers.GetOrAdd(categoryName, name => new CmdPalLogger(name));
public void Dispose() => _loggers.Clear();
}

View File

@@ -0,0 +1,31 @@
// 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.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using MEL = Microsoft.Extensions.Logging;
namespace Microsoft.CmdPal.Common.Logging;
public static class CmdPalLoggingExtensions
{
/// <summary>
/// Registers the Microsoft.Extensions.Logging infrastructure and adds a
/// <see cref="CmdPalLoggerProvider"/> that routes all <see cref="MEL.ILogger"/>
/// output to <see cref="ManagedCommon.Logger"/>.
/// </summary>
public static IServiceCollection AddCmdPalLogging(this IServiceCollection services)
{
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<MEL.ILoggerProvider, CmdPalLoggerProvider>());
});
return services;
}
}

View File

@@ -26,6 +26,7 @@
<ItemGroup>
<ProjectReference Include="..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
</ItemGroup>
<ItemGroup>
@@ -60,4 +61,8 @@
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<Folder Include="Logging\" />
</ItemGroup>
</Project>

View File

@@ -8,6 +8,6 @@ public sealed class PinyinFuzzyMatcherOptions
{
public PinyinMode Mode { get; init; } = PinyinMode.AutoSimplifiedChineseUi;
/// <summary>Remove IME syllable separators (') for query secondary variant.</summary>
/// <summary>Gets a value indicating whether IME syllable separators (') are removed for query secondary variant.</summary>
public bool RemoveApostrophesForQuery { get; init; } = true;
}

View File

@@ -341,25 +341,25 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
}
/// <summary>
/// Gets whether the backdrop opacity slider should be visible.
/// Gets a value indicating whether the backdrop opacity slider should be visible.
/// </summary>
public bool IsBackdropOpacityVisible =>
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether the backdrop description (for styles without options) should be visible.
/// Gets a value indicating whether the backdrop description (for styles without options) should be visible.
/// </summary>
public bool IsMicaBackdropDescriptionVisible =>
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether background/colorization settings are available.
/// Gets a value indicating whether background/colorization settings are available.
/// </summary>
public bool IsBackgroundSettingsEnabled =>
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
/// <summary>
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
/// Gets a value indicating whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
/// </summary>
public bool IsBackgroundNotAvailableVisible =>
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;

View File

@@ -36,17 +36,17 @@ public sealed record BackdropStyleConfig
public float FixedOpacity { get; init; }
/// <summary>
/// Gets whether this backdrop style supports custom colorization (tint colors).
/// Gets a value indicating whether this backdrop style supports custom colorization (tint colors).
/// </summary>
public bool SupportsColorization { get; init; } = true;
/// <summary>
/// Gets whether this backdrop style supports custom background images.
/// Gets a value indicating whether this backdrop style supports custom background images.
/// </summary>
public bool SupportsBackgroundImage { get; init; } = true;
/// <summary>
/// Gets whether this backdrop style supports opacity adjustment.
/// Gets a value indicating whether this backdrop style supports opacity adjustment.
/// </summary>
public bool SupportsOpacity { get; init; } = true;

View File

@@ -31,6 +31,7 @@ internal sealed class ExtensionTemplateService : IExtensionTemplateService
private static readonly HashSet<string> _copyAsIsTemplateExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".md",
".png",
};

View File

@@ -67,7 +67,7 @@ public sealed class ThemeSnapshot
public required float BackgroundBrightness { get; init; }
/// <summary>
/// Gets whether colorization is active (accent color, custom color, or image mode).
/// Gets a value indicating whether colorization is active (accent color, custom color, or image mode).
/// </summary>
public required bool HasColorization { get; init; }
}

View File

@@ -93,19 +93,19 @@ public record DockBandSettings
public required string CommandId { get; init; }
/// <summary>
/// Gets or sets whether titles are shown for items in this band.
/// Gets whether titles are shown for items in this band.
/// If null, falls back to dock-wide ShowLabels setting.
/// </summary>
public bool? ShowTitles { get; init; }
/// <summary>
/// Gets or sets whether subtitles are shown for items in this band.
/// Gets whether subtitles are shown for items in this band.
/// If null, falls back to dock-wide ShowLabels setting.
/// </summary>
public bool? ShowSubtitles { get; init; }
/// <summary>
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
/// Gets a value for backward compatibility. Maps to ShowTitles.
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
public bool? ShowLabels

View File

@@ -9,37 +9,37 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class WindowPosition
{
/// <summary>
/// Gets or sets left position in device pixels.
/// Gets the left position in device pixels.
/// </summary>
public int X { get; init; }
/// <summary>
/// Gets or sets top position in device pixels.
/// Gets the top position in device pixels.
/// </summary>
public int Y { get; init; }
/// <summary>
/// Gets or sets width in device pixels.
/// Gets the width in device pixels.
/// </summary>
public int Width { get; init; }
/// <summary>
/// Gets or sets height in device pixels.
/// Gets the height in device pixels.
/// </summary>
public int Height { get; init; }
/// <summary>
/// Gets or sets width of the screen in device pixels where the window is located.
/// Gets the width of the screen in device pixels where the window is located.
/// </summary>
public int ScreenWidth { get; init; }
/// <summary>
/// Gets or sets height of the screen in device pixels where the window is located.
/// Gets the height of the screen in device pixels where the window is located.
/// </summary>
public int ScreenHeight { get; init; }
/// <summary>
/// Gets or sets DPI (dots per inch) of the display where the window is located.
/// Gets the DPI (dots per inch) of the display where the window is located.
/// </summary>
public int Dpi { get; init; }

View File

@@ -5,6 +5,7 @@
using ManagedCommon;
using Microsoft.CmdPal.Common;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Logging;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Common.Text;
using Microsoft.CmdPal.Ext.Apps;
@@ -125,6 +126,8 @@ public partial class App : Application, IDisposable
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
services.AddCmdPalLogging();
AddBuiltInCommands(services, appInfoService.ConfigDirectory);
AddCoreServices(services, appInfoService);

View File

@@ -157,12 +157,6 @@ public sealed partial class SearchBar : UserControl,
{
// Clear the search box
FilterBox.Text = string.Empty;
// hack TODO GH #245
if (CurrentPageViewModel is not null)
{
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
}
}
break;
@@ -170,14 +164,6 @@ public sealed partial class SearchBar : UserControl,
e.Handled = true;
}
else if (e.Key == VirtualKey.Back)
{
// hack TODO GH #245
if (CurrentPageViewModel is not null)
{
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
}
}
}
private void FilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
@@ -332,19 +318,6 @@ public sealed partial class SearchBar : UserControl,
private void FilterBox_TextChanged(object sender, TextChangedEventArgs e)
{
// Logger.LogInfo($"FilterBox_TextChanged: {FilterBox.Text}");
// TERRIBLE HACK TODO GH #245
// There's weird wacky bugs with debounce currently. We're trying
// to get them ingested, but while we wait for the toolkit feeds to
// bubble, just manually send the first character, always
// (otherwise aliases just stop working)
if (FilterBox.Text.Length == 1)
{
DoFilterBoxUpdate();
return;
}
if (InSuggestion)
{
// Logger.LogInfo($"-- skipping, in suggestion --");

View File

@@ -18,7 +18,7 @@ namespace Microsoft.CmdPal.UI.Events;
public class CmdPalDockConfiguration : EventBase, IEvent
{
/// <summary>
/// Gets or sets whether the dock is enabled.
/// Gets or sets a value indicating whether the dock is enabled.
/// </summary>
public bool IsDockEnabled { get; set; }

View File

@@ -33,7 +33,7 @@ public class CmdPalExtensionInvoked : EventBase, IEvent
public string CommandName { get; set; }
/// <summary>
/// Gets or sets whether the command executed successfully.
/// Gets or sets a value indicating whether the command executed successfully.
/// </summary>
public bool Success { get; set; }

View File

@@ -25,7 +25,12 @@
<tkcontrols:MarkdownThemes
x:Key="DefaultMarkdownThemeConfig"
H3FontSize="12"
H3FontWeight="Normal" />
H3FontWeight="Normal"
InlineCodeBackground="{StaticResource ControlFillColorDefaultBrush}"
InlineCodeBorderBrush="{StaticResource ControlElevationBorderBrush}"
InlineCodeCornerRadius="2"
InlineCodeForeground="{StaticResource TextFillColorSecondaryBrush}"
InlineCodePadding="2,0,2,1" />
<markdownImageProviders:ImageProvider x:Key="ImageProvider" />
<tkcontrols:MarkdownConfig
x:Key="DefaultMarkdownConfig"

View File

@@ -166,7 +166,12 @@
<tkcontrols:MarkdownThemes
x:Key="DefaultMarkdownThemeConfig"
H3FontSize="12"
H3FontWeight="Normal" />
H3FontWeight="Normal"
InlineCodeBackground="{StaticResource ControlFillColorDefaultBrush}"
InlineCodeBorderBrush="{StaticResource ControlElevationBorderBrush}"
InlineCodeCornerRadius="2"
InlineCodeForeground="{StaticResource TextFillColorSecondaryBrush}"
InlineCodePadding="2,0,2,1" />
<markdownImageProviders:ImageProvider x:Key="ImageProvider" />
<tkcontrols:MarkdownConfig
x:Key="DefaultMarkdownConfig"

View File

@@ -22,7 +22,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("files");
var searchFileItem = this.Find<NavigationViewItem>("Search files");
Assert.AreEqual(searchFileItem.Name, "Search files");
Assert.AreEqual("Search files", searchFileItem.Name);
searchFileItem.DoubleClick();
SetFilesExtensionSearchBox("AppData");
@@ -36,7 +36,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("calculator");
var searchFileItem = this.Find<NavigationViewItem>("Calculator");
Assert.AreEqual(searchFileItem.Name, "Calculator");
Assert.AreEqual("Calculator", searchFileItem.Name);
searchFileItem.DoubleClick();
SetCalculatorExtensionSearchBox("1+2");
@@ -50,7 +50,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("time and date");
var searchFileItem = this.Find<NavigationViewItem>("Time and date");
Assert.AreEqual(searchFileItem.Name, "Time and date");
Assert.AreEqual("Time and date", searchFileItem.Name);
searchFileItem.DoubleClick();
SetTimeAndDaterExtensionSearchBox("year");
@@ -64,7 +64,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("Windows Terminal");
var searchFileItem = this.Find<NavigationViewItem>("Open Windows Terminal profiles");
Assert.AreEqual(searchFileItem.Name, "Open Windows Terminal profiles");
Assert.AreEqual("Open Windows Terminal profiles", searchFileItem.Name);
searchFileItem.DoubleClick();
// SetSearchBox("PowerShell");
@@ -77,7 +77,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("Windows settings");
var searchFileItem = this.Find<NavigationViewItem>("Windows settings");
Assert.AreEqual(searchFileItem.Name, "Windows settings");
Assert.AreEqual("Windows settings", searchFileItem.Name);
searchFileItem.DoubleClick();
SetSearchBox("power");
@@ -91,7 +91,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("Registry");
var searchFileItem = this.Find<NavigationViewItem>("Registry");
Assert.AreEqual(searchFileItem.Name, "Registry");
Assert.AreEqual("Registry", searchFileItem.Name);
searchFileItem.DoubleClick();
// Type the string will cause strange behavior.so comment it out for now.
@@ -105,7 +105,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("Windows Services");
var searchFileItem = this.Find<NavigationViewItem>("Windows Services");
Assert.AreEqual(searchFileItem.Name, "Windows Services");
Assert.AreEqual("Windows Services", searchFileItem.Name);
searchFileItem.DoubleClick();
SetSearchBox("hyper-v");
@@ -119,7 +119,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("Windows System Commands");
var searchFileItem = this.Find<NavigationViewItem>("Windows System Commands");
Assert.AreEqual(searchFileItem.Name, "Windows System Commands");
Assert.AreEqual("Windows System Commands", searchFileItem.Name);
searchFileItem.DoubleClick();
SetSearchBox("Sleep");

View File

@@ -45,7 +45,7 @@ public class IndexerTests : CommandPaletteTestBase
SetSearchBox("files");
var searchFileItem = this.Find<NavigationViewItem>("Search files");
Assert.AreEqual(searchFileItem.Name, "Search files");
Assert.AreEqual("Search files", searchFileItem.Name);
searchFileItem.DoubleClick();
}

View File

@@ -76,7 +76,7 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
};
/// <summary>
/// Parsed search result limit. Returns <see langword="null"/> when the caller should
/// Gets the parsed search result limit. Returns <see langword="null"/> when the caller should
/// use its own default (unrecognized value, empty, or old stored "0").
/// </summary>
public int? SearchResultLimit
@@ -146,8 +146,7 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
public AllAppsSettings()
@@ -162,7 +161,6 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
Settings.Add(_enablePathEnvironmentVariableSource);
Settings.Add(_searchResultLimitSource);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -102,8 +102,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
public SettingsManager()
@@ -117,7 +116,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_copyResultToSearchBarIfQueryEndsWithEqualSign);
Settings.Add(_autoFixQuery);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -48,8 +48,7 @@ internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{Namespace}.settings.json");
}
public SettingsManager()
@@ -60,7 +59,6 @@ internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
Settings.Add(_confirmDelete);
Settings.Add(_primaryAction);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (_, _) => SaveSettings();

View File

@@ -18,18 +18,13 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
// Add settings here when needed
// Settings.Add(setting);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -36,7 +36,7 @@ internal class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
public SettingsManager()
@@ -45,7 +45,6 @@ internal class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_predefinedConnections);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -2,8 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using Microsoft.CmdPal.Ext.Shell.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -53,8 +51,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
public SettingsManager()
@@ -64,7 +61,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_leaveShellOpen);
Settings.Add(_shellCommandExecution);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -42,8 +42,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
public bool ShowDialogToConfirmCommand() => _showDialogToConfirmCommand.Value;
@@ -65,7 +64,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_hideEmptyRecycleBin);
Settings.Add(_hideDisconnectedNetworkInfo);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -152,8 +152,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
public SettingsManager()
@@ -170,7 +169,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
_customFormats.Placeholder = CUSTOMFORMATPLACEHOLDER;
Settings.Add(_customFormats);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -12,6 +12,6 @@ public interface IBrowserInfoService
/// <summary>
/// Gets information about the system's default web browser.
/// </summary>
/// <returns></returns>
/// <returns>The default browser information, or <see langword="null"/> if it could not be determined.</returns>
BrowserInfo? GetDefaultBrowser();
}

View File

@@ -85,8 +85,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
private static string HistoryStateJsonPath()

View File

@@ -99,8 +99,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{Namespace}.settings.json");
}
public SettingsManager()
@@ -118,7 +117,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_inMruOrder);
Settings.Add(_useWindowIcon);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (_, _) => SaveSettings();

View File

@@ -65,8 +65,7 @@ public class SettingsManager : JsonSettingsManager
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
return Path.Combine(directory, $"{_namespace}.settings.json");
}
public SettingsManager()
@@ -79,7 +78,6 @@ public class SettingsManager : JsonSettingsManager
Settings.Add(_saveLastSelectedChannel);
Settings.Add(_profileSortOrder);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -28,7 +28,8 @@ public abstract class JsonSettingsManager
var filePath = FilePath;
if (!File.Exists(filePath))
{
ExtensionHost.LogMessage(new LogMessage() { Message = "The provided settings file does not exist" });
// No settings file yet: keep in-memory defaults without persisting.
// The file is created on the first user-initiated settings change.
return;
}

View File

@@ -12,7 +12,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
public static class ShellHelpers
{
/// <summary>
/// These are the executable file extensions that Windows Shell recognizes. Unlike CMD/PowerShell,
/// Gets the executable file extensions that Windows Shell recognizes. Unlike CMD/PowerShell,
/// Shell does not use PATHEXT, but has a magic fixed list.
/// </summary>
public static string[] ExecutableExtensions { get; } = [".PIF", ".COM", ".EXE", ".BAT", ".CMD"];
@@ -246,9 +246,9 @@ public static class ShellHelpers
/// <summary>
/// Mimics Windows Shell behavior to resolve an executable name to a full path.
/// </summary>
/// <param name="name"></param>
/// <param name="fullPath"></param>
/// <returns></returns>
/// <param name="name">The name of the executable to resolve.</param>
/// <param name="fullPath">When this method returns, contains the full path to the executable if found; otherwise, <see langword="null"/>.</param>
/// <returns><see langword="true"/> if the executable was resolved to a full path; otherwise, <see langword="false"/>.</returns>
public static bool TryResolveExecutableAsShell(string name, out string fullPath)
{
// First check if we can find the file in the registry

View File

@@ -0,0 +1,459 @@
// 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.Drawing;
using System.Globalization;
using ColorPicker.Helpers;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ColorPicker.UnitTests.Helpers
{
/// <summary>
/// Test class to test <see cref="ColorFormatHelper"/> conversion methods not covered
/// by the existing <see cref="ColorConverterTest"/> (which tests HSL and HSV).
/// Covers: CMYK, HSB, HSI, HWB, CIE LAB, CIE XYZ, Oklab, Oklch, sRGB-to-linear, NCol.
/// </summary>
[TestClass]
public class ColorFormatConversionTest
{
[TestMethod]
public void ConvertToCMYK_Black_Returns0_0_0_1()
{
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.Cyan);
Assert.AreEqual(0d, result.Magenta);
Assert.AreEqual(0d, result.Yellow);
Assert.AreEqual(1d, result.BlackKey);
}
[TestMethod]
public void ConvertToCMYK_White_Returns0_0_0_0()
{
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(0d, result.Cyan, 0.01);
Assert.AreEqual(0d, result.Magenta, 0.01);
Assert.AreEqual(0d, result.Yellow, 0.01);
Assert.AreEqual(0d, result.BlackKey, 0.01);
}
[TestMethod]
public void ConvertToCMYK_Red_Returns0_1_1_0()
{
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 255, 0, 0));
Assert.AreEqual(0d, result.Cyan, 0.01);
Assert.AreEqual(1d, result.Magenta, 0.01);
Assert.AreEqual(1d, result.Yellow, 0.01);
Assert.AreEqual(0d, result.BlackKey, 0.01);
}
[TestMethod]
public void ConvertToCMYK_Green_Returns1_0_1_0()
{
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 0, 255, 0));
Assert.AreEqual(1d, result.Cyan, 0.01);
Assert.AreEqual(0d, result.Magenta, 0.01);
Assert.AreEqual(1d, result.Yellow, 0.01);
Assert.AreEqual(0d, result.BlackKey, 0.01);
}
[TestMethod]
public void ConvertToCMYK_Blue_Returns1_1_0_0()
{
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 0, 0, 255));
Assert.AreEqual(1d, result.Cyan, 0.01);
Assert.AreEqual(1d, result.Magenta, 0.01);
Assert.AreEqual(0d, result.Yellow, 0.01);
Assert.AreEqual(0d, result.BlackKey, 0.01);
}
[TestMethod]
public void ConvertToCMYK_MidGray_Returns0_0_0_Half()
{
// RGB(128, 128, 128) should give roughly K ≈ 0.498
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 128, 128, 128));
Assert.AreEqual(0d, result.Cyan, 0.01);
Assert.AreEqual(0d, result.Magenta, 0.01);
Assert.AreEqual(0d, result.Yellow, 0.01);
Assert.AreEqual(128d / 255d, result.BlackKey, 0.01);
}
[TestMethod]
public void ConvertToHSB_Black_Returns0_0_0()
{
var result = ColorFormatHelper.ConvertToHSBColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(0d, result.Saturation, 0.01);
Assert.AreEqual(0d, result.Brightness, 0.01);
}
[TestMethod]
public void ConvertToHSB_White_Returns0_0_1()
{
var result = ColorFormatHelper.ConvertToHSBColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(0d, result.Saturation, 0.01);
Assert.AreEqual(1d, result.Brightness, 0.01);
}
[TestMethod]
public void ConvertToHSB_Red_Returns0_1_1()
{
var result = ColorFormatHelper.ConvertToHSBColor(Color.FromArgb(255, 255, 0, 0));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(1d, result.Saturation, 0.01);
Assert.AreEqual(1d, result.Brightness, 0.01);
}
[TestMethod]
public void ConvertToHSI_Black_Returns0_0_0()
{
var result = ColorFormatHelper.ConvertToHSIColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(0d, result.Saturation, 0.01);
Assert.AreEqual(0d, result.Intensity, 0.01);
}
[TestMethod]
public void ConvertToHSI_White_Returns0_0_1()
{
var result = ColorFormatHelper.ConvertToHSIColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(0d, result.Saturation, 0.01);
Assert.AreEqual(1d, result.Intensity, 0.01);
}
[TestMethod]
public void ConvertToHSI_Red_Returns0_1_Third()
{
// Pure red: intensity = (255+0+0)/(3*255) = 1/3
var result = ColorFormatHelper.ConvertToHSIColor(Color.FromArgb(255, 255, 0, 0));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(1d, result.Saturation, 0.01);
Assert.AreEqual(1d / 3d, result.Intensity, 0.01);
}
[TestMethod]
public void ConvertToHWB_Black_Returns0_0_1()
{
var result = ColorFormatHelper.ConvertToHWBColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(0d, result.Whiteness, 0.01);
Assert.AreEqual(1d, result.Blackness, 0.01);
}
[TestMethod]
public void ConvertToHWB_White_Returns0_1_0()
{
var result = ColorFormatHelper.ConvertToHWBColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(1d, result.Whiteness, 0.01);
Assert.AreEqual(0d, result.Blackness, 0.01);
}
[TestMethod]
public void ConvertToHWB_Red_Returns0_0_0()
{
var result = ColorFormatHelper.ConvertToHWBColor(Color.FromArgb(255, 255, 0, 0));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(0d, result.Whiteness, 0.01);
Assert.AreEqual(0d, result.Blackness, 0.01);
}
[TestMethod]
public void ConvertToHWB_MidGray_Returns0_Half_Half()
{
var result = ColorFormatHelper.ConvertToHWBColor(Color.FromArgb(255, 128, 128, 128));
Assert.AreEqual(0d, result.Hue, 0.5);
Assert.AreEqual(128d / 255d, result.Whiteness, 0.01);
Assert.AreEqual(1d - (128d / 255d), result.Blackness, 0.01);
}
[TestMethod]
public void ConvertToCIEXYZ_Black_Returns0_0_0()
{
var result = ColorFormatHelper.ConvertToCIEXYZColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.X, 0.001);
Assert.AreEqual(0d, result.Y, 0.001);
Assert.AreEqual(0d, result.Z, 0.001);
}
[TestMethod]
public void ConvertToCIEXYZ_White_ReturnsD65Illuminant()
{
// White should be close to D65 illuminant: X≈0.9505, Y≈1.0, Z≈1.089
var result = ColorFormatHelper.ConvertToCIEXYZColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(0.9505d, result.X, 0.02);
Assert.AreEqual(1.0d, result.Y, 0.02);
Assert.AreEqual(1.089d, result.Z, 0.02);
}
[TestMethod]
public void ConvertToCIEXYZ_Red_HasExpectedValues()
{
// Pure red: X≈0.4124, Y≈0.2126, Z≈0.0193
var result = ColorFormatHelper.ConvertToCIEXYZColor(Color.FromArgb(255, 255, 0, 0));
Assert.AreEqual(0.4124d, result.X, 0.02);
Assert.AreEqual(0.2126d, result.Y, 0.02);
Assert.AreEqual(0.0193d, result.Z, 0.02);
}
[TestMethod]
public void ConvertToCIELAB_Black_Returns0_0_0()
{
var result = ColorFormatHelper.ConvertToCIELABColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.Lightness, 0.5);
Assert.AreEqual(0d, result.ChromaticityA, 0.5);
Assert.AreEqual(0d, result.ChromaticityB, 0.5);
}
[TestMethod]
public void ConvertToCIELAB_White_Returns100_0_0()
{
var result = ColorFormatHelper.ConvertToCIELABColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(100d, result.Lightness, 1.0);
Assert.AreEqual(0d, result.ChromaticityA, 1.0);
Assert.AreEqual(0d, result.ChromaticityB, 1.0);
}
[TestMethod]
public void ConvertToCIELAB_Red_HasPositiveA()
{
// Red is in the +a* direction
var result = ColorFormatHelper.ConvertToCIELABColor(Color.FromArgb(255, 255, 0, 0));
Assert.IsTrue(result.ChromaticityA > 0, "Red should have positive a* in CIE LAB");
}
[TestMethod]
public void ConvertToCIELAB_Blue_HasNegativeB()
{
// Blue is in the -b* direction
var result = ColorFormatHelper.ConvertToCIELABColor(Color.FromArgb(255, 0, 0, 255));
Assert.IsTrue(result.ChromaticityB < 0, "Blue should have negative b* in CIE LAB");
}
[TestMethod]
public void ConvertToOklab_Black_Returns0_0_0()
{
var result = ColorFormatHelper.ConvertToOklabColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.Lightness, 0.01);
Assert.AreEqual(0d, result.ChromaticityA, 0.01);
Assert.AreEqual(0d, result.ChromaticityB, 0.01);
}
[TestMethod]
public void ConvertToOklab_White_Returns1_0_0()
{
var result = ColorFormatHelper.ConvertToOklabColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(1d, result.Lightness, 0.02);
Assert.AreEqual(0d, result.ChromaticityA, 0.02);
Assert.AreEqual(0d, result.ChromaticityB, 0.02);
}
[TestMethod]
public void ConvertToOklch_Black_Returns0_0_0()
{
var result = ColorFormatHelper.ConvertToOklchColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.Lightness, 0.01);
Assert.AreEqual(0d, result.Chroma, 0.01);
}
[TestMethod]
public void ConvertToOklch_White_Returns1_0_Any()
{
var result = ColorFormatHelper.ConvertToOklchColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(1d, result.Lightness, 0.02);
Assert.AreEqual(0d, result.Chroma, 0.02);
// Hue is undefined for achromatic colors, so we don't assert it
}
[TestMethod]
public void ConvertToOklch_Chroma_IsNonNegative()
{
// Chroma should always be non-negative
var colors = new[] { Color.Red, Color.Green, Color.Blue, Color.Yellow, Color.Cyan, Color.Magenta };
foreach (var color in colors)
{
var result = ColorFormatHelper.ConvertToOklchColor(color);
Assert.IsTrue(result.Chroma >= 0, $"Chroma should be non-negative for {color.Name}");
}
}
[TestMethod]
public void ConvertSRGBToLinear_Zero_ReturnsZero()
{
var result = ColorFormatHelper.ConvertSRGBToLinearRGB(0, 0, 0);
Assert.AreEqual(0d, result.R, 0.001);
Assert.AreEqual(0d, result.G, 0.001);
Assert.AreEqual(0d, result.B, 0.001);
}
[TestMethod]
public void ConvertSRGBToLinear_One_ReturnsOne()
{
var result = ColorFormatHelper.ConvertSRGBToLinearRGB(1, 1, 1);
Assert.AreEqual(1d, result.R, 0.001);
Assert.AreEqual(1d, result.G, 0.001);
Assert.AreEqual(1d, result.B, 0.001);
}
[TestMethod]
public void ConvertSRGBToLinear_SmallValues_UsesLinearPath()
{
// For small values (≤ 0.04045), the formula is linear: value / 12.92
var result = ColorFormatHelper.ConvertSRGBToLinearRGB(0.04, 0.04, 0.04);
Assert.AreEqual(0.04 / 12.92, result.R, 0.001);
}
[TestMethod]
public void ConvertSRGBToLinear_LargeValues_UsesGammaPath()
{
// For larger values, the gamma function is applied
// sRGB 0.5 should map to ~0.214
var result = ColorFormatHelper.ConvertSRGBToLinearRGB(0.5, 0.5, 0.5);
Assert.AreEqual(0.214, result.R, 0.01);
}
[TestMethod]
public void ConvertToNaturalColor_Red_ReturnsR0()
{
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 255, 0, 0));
Assert.AreEqual("R0", result.Hue);
Assert.AreEqual(0d, result.Whiteness, 0.01);
Assert.AreEqual(0d, result.Blackness, 0.01);
}
[TestMethod]
public void ConvertToNaturalColor_Green_HueStartsWithG()
{
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 0, 128, 0));
Assert.IsTrue(result.Hue.StartsWith('G'), $"Green should start with G, got {result.Hue}");
}
[TestMethod]
public void ConvertToNaturalColor_Blue_HueStartsWithB()
{
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 0, 0, 255));
Assert.IsTrue(result.Hue.StartsWith('B'), $"Blue should start with B, got {result.Hue}");
}
[TestMethod]
public void ConvertToNaturalColor_Black_Returns0_0_100()
{
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 0, 0, 0));
Assert.AreEqual(0d, result.Whiteness, 0.01);
Assert.AreEqual(1d, result.Blackness, 0.01);
}
[TestMethod]
public void ConvertToNaturalColor_White_Returns0_100_0()
{
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 255, 255, 255));
Assert.AreEqual(1d, result.Whiteness, 0.01);
Assert.AreEqual(0d, result.Blackness, 0.01);
}
[TestMethod]
[DataRow("CMYK", "cmyk(0%, 100%, 100%, 0%)")]
[DataRow("HEX", "ff0000")]
[DataRow("RGB", "rgb(255, 0, 0)")]
[DataRow("HSL", "hsl(0, 100%, 50%)")]
[DataRow("HSV", "hsv(0, 100%, 100%)")]
[DataRow("HSB", "hsb(0, 100%, 100%)")]
[DataRow("HSI", "hsi(0, 100%, 33%)")]
[DataRow("HWB", "hwb(0, 0%, 0%)")]
[DataRow("Decimal", "255")]
[DataRow("HEX Int", "0xFFFF0000")]
[DataRow("VEC4", "(1f, 0f, 0f, 1f)")]
public void GetStringRepresentation_Red(string type, string expected)
{
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 255, 0, 0), type, ColorFormatHelper.GetDefaultFormat(type));
Assert.AreEqual(expected, result);
}
[TestMethod]
[DataRow("CMYK", "cmyk(0%, 0%, 0%, 0%)")]
[DataRow("HEX", "ffffff")]
[DataRow("RGB", "rgb(255, 255, 255)")]
[DataRow("HSL", "hsl(0, 0%, 100%)")]
[DataRow("HSV", "hsv(0, 0%, 100%)")]
[DataRow("Decimal", "16777215")]
[DataRow("HEX Int", "0xFFFFFFFF")]
[DataRow("VEC4", "(1f, 1f, 1f, 1f)")]
public void GetStringRepresentation_White(string type, string expected)
{
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 255, 255, 255), type, ColorFormatHelper.GetDefaultFormat(type));
Assert.AreEqual(expected, result);
}
[TestMethod]
[DataRow("CMYK", "cmyk(100%, 0%, 100%, 0%)")]
[DataRow("HEX", "00ff00")]
[DataRow("RGB", "rgb(0, 255, 0)")]
[DataRow("HSL", "hsl(120, 100%, 50%)")]
[DataRow("HSV", "hsv(120, 100%, 100%)")]
[DataRow("Decimal", "65280")]
[DataRow("HEX Int", "0xFF00FF00")]
public void GetStringRepresentation_Green(string type, string expected)
{
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 0, 255, 0), type, ColorFormatHelper.GetDefaultFormat(type));
Assert.AreEqual(expected, result);
}
[TestMethod]
[DataRow("CMYK", "cmyk(100%, 100%, 0%, 0%)")]
[DataRow("HEX", "0000ff")]
[DataRow("RGB", "rgb(0, 0, 255)")]
[DataRow("HSL", "hsl(240, 100%, 50%)")]
[DataRow("HSV", "hsv(240, 100%, 100%)")]
[DataRow("Decimal", "16711680")]
[DataRow("HEX Int", "0xFF0000FF")]
public void GetStringRepresentation_Blue(string type, string expected)
{
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 0, 0, 255), type, ColorFormatHelper.GetDefaultFormat(type));
Assert.AreEqual(expected, result);
}
[TestMethod]
public void GetStringRepresentation_EmptyFormat_ReturnsHex()
{
// When colorFormat is null or empty, should return hex
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 255, 0, 0), "RGB", string.Empty);
Assert.AreEqual("ff0000", result);
}
[TestMethod]
public void GetStringRepresentation_NullFormat_ReturnsHex()
{
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 255, 0, 0), "RGB", null);
Assert.AreEqual("ff0000", result);
}
[TestMethod]
[DataRow("RGB")]
[DataRow("HEX")]
[DataRow("CMYK")]
[DataRow("HSL")]
[DataRow("HSV")]
[DataRow("HSB")]
[DataRow("HSI")]
[DataRow("HWB")]
[DataRow("NCol")]
[DataRow("CIEXYZ")]
[DataRow("CIELAB")]
[DataRow("Oklab")]
[DataRow("Oklch")]
[DataRow("VEC4")]
[DataRow("Decimal")]
[DataRow("HEX Int")]
public void GetDefaultFormat_KnownTypes_ReturnsNonEmptyString(string formatName)
{
var result = ColorFormatHelper.GetDefaultFormat(formatName);
Assert.IsFalse(string.IsNullOrEmpty(result), $"Default format for {formatName} should not be empty");
}
}
}

View File

@@ -34,7 +34,7 @@ namespace ColorPicker.Helpers
public void GetStringRepresentationTest(string type, string expected)
{
var result = ColorRepresentationHelper.GetStringRepresentation(Color.Black, type, ColorFormatHelper.GetDefaultFormat(type));
Assert.AreEqual(result, expected);
Assert.AreEqual(expected, result);
}
}
}

View File

@@ -121,6 +121,14 @@ namespace ImageResizer.Models
{
encoder.BitmapTransform.Bounds = cropBounds.Value;
}
// Apply codec-specific properties (e.g., JPEG quality).
// Must be set after transforms since re-encoding will occur.
var encoderProps = GetEncoderPropertySet(encoderGuid);
if (encoderProps != null)
{
await encoder.BitmapProperties.SetPropertiesAsync(encoderProps);
}
}
}
else
@@ -515,6 +523,25 @@ namespace ImageResizer.Models
};
}
if (encoderGuid == BitmapEncoder.PngEncoderId)
{
// Only override when explicitly set; Default lets the WIC encoder decide.
if (_settings.PngInterlaceOption == PngInterlaceOption.On)
{
return new BitmapPropertySet
{
{ "InterlaceOption", new BitmapTypedValue(true, PropertyType.Boolean) },
};
}
else if (_settings.PngInterlaceOption == PngInterlaceOption.Off)
{
return new BitmapPropertySet
{
{ "InterlaceOption", new BitmapTypedValue(false, PropertyType.Boolean) },
};
}
}
if (encoderGuid == BitmapEncoder.TiffEncoderId)
{
var compressionMethod = MapTiffCompression(_settings.TiffCompressOption);

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace KeyboardManagerEditorUI.Controls
{
public class KeyChangedEventArgs : EventArgs
{
public string OldKeyName { get; }
public string NewKeyName { get; }
public int NewKeyCode { get; }
public KeyChangedEventArgs(string oldKeyName, string newKeyName, int newKeyCode)
{
OldKeyName = oldKeyName;
NewKeyName = newKeyName;
NewKeyCode = newKeyCode;
}
}
}

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="KeyboardManagerEditorUI.Controls.KeyDropDownButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<DropDownButton
x:Name="KeyButton"
MinWidth="48"
MinHeight="36"
Padding="8"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
CornerRadius="{StaticResource OverlayCornerRadius}"
FontSize="16"
Style="{StaticResource DefaultKeyVisualDropDownButtonStyle}">
<DropDownButton.Flyout>
<Flyout
x:Name="KeyListFlyout"
Closed="KeyListFlyout_Closed"
Opening="KeyListFlyout_Opening"
Placement="Bottom">
<ListView
x:Name="KeyListView"
MinWidth="200"
MaxHeight="320"
IsItemClickEnabled="True"
ItemClick="KeyListView_ItemClick"
SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Flyout>
</DropDownButton.Flyout>
<commoncontrols:KeyCharPresenter x:Name="KeyNamePresenter" Content="{x:Bind KeyName, Mode=OneWay}" />
</DropDownButton>
</UserControl>

View File

@@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using KeyboardManagerEditorUI.Interop;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace KeyboardManagerEditorUI.Controls
{
public sealed partial class KeyDropDownButton : UserControl
{
private static List<KeyNameEntry>? _cachedKeyList;
private static List<KeyNameEntry>? _cachedShortcutKeyList;
public static readonly DependencyProperty KeyNameProperty =
DependencyProperty.Register(
nameof(KeyName),
typeof(string),
typeof(KeyDropDownButton),
new PropertyMetadata(string.Empty));
public static readonly DependencyProperty IsShortcutProperty =
DependencyProperty.Register(
nameof(IsShortcut),
typeof(bool),
typeof(KeyDropDownButton),
new PropertyMetadata(true));
public static readonly DependencyProperty UseAccentStyleProperty =
DependencyProperty.Register(
nameof(UseAccentStyle),
typeof(bool),
typeof(KeyDropDownButton),
new PropertyMetadata(false));
public string KeyName
{
get => (string)GetValue(KeyNameProperty);
set => SetValue(KeyNameProperty, value);
}
public bool IsShortcut
{
get => (bool)GetValue(IsShortcutProperty);
set => SetValue(IsShortcutProperty, value);
}
public bool UseAccentStyle
{
get => (bool)GetValue(UseAccentStyleProperty);
set => SetValue(UseAccentStyleProperty, value);
}
public event EventHandler<KeyChangedEventArgs>? KeyChanged;
public KeyDropDownButton()
{
this.InitializeComponent();
this.Loaded += (_, _) =>
{
if (UseAccentStyle)
{
KeyButton.Style = (Style)Application.Current.Resources["AccentKeyVisualDropDownButtonStyle"];
}
};
}
private void KeyListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is KeyNameEntry entry)
{
string oldKeyName = KeyName;
KeyListFlyout.Hide();
KeyChanged?.Invoke(this, new KeyChangedEventArgs(oldKeyName, entry.DisplayName, entry.KeyCode));
}
}
private void KeyListFlyout_Closed(object sender, object e)
{
// Clear selection when flyout closes
KeyListView.SelectedItem = null;
}
private void KeyListFlyout_Opening(object sender, object e)
{
RefreshKeyList();
}
private List<KeyNameEntry> GetKeyList()
{
bool isShortcut = IsShortcut;
ref var cached = ref (isShortcut ? ref _cachedShortcutKeyList : ref _cachedKeyList);
if (cached == null)
{
try
{
using (var service = new KeyboardMappingService())
{
var list = service.GetKeyboardKeysList(isShortcut);
// Filter out the synthetic "None" entry (keycode 0) that the native layer
// injects for shortcut lists; selecting it would store an invalid key code.
cached = list.Where(e => e.KeyCode != 0).ToList();
}
}
catch
{
cached = new List<KeyNameEntry>();
}
}
return cached;
}
internal void RefreshKeyList()
{
KeyListView.ItemsSource = GetKeyList();
// Scroll to current key if possible
var list = GetKeyList();
for (int i = 0; i < list.Count; i++)
{
if (string.Equals(list[i].DisplayName, KeyName, StringComparison.Ordinal))
{
KeyListView.SelectedIndex = i;
KeyListView.ScrollIntoView(list[i]);
break;
}
}
}
}
}

View File

@@ -6,6 +6,7 @@
xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
@@ -80,28 +81,29 @@
Style="{StaticResource CustomShortcutToggleButtonStyle}"
Unchecked="TriggerKeyToggleBtn_Unchecked">
<ToggleButton.Content>
<ItemsControl x:Name="TriggerKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
HorizontalSpacing="4"
Orientation="Horizontal"
VerticalSpacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual
Padding="8"
Background="{ThemeResource ControlFillColorDefaultBrush}"
BorderThickness="1"
Content="{Binding}"
CornerRadius="{StaticResource OverlayCornerRadius}"
FontSize="16"
Style="{StaticResource DefaultKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid>
<TextBlock
x:Name="TriggerKeyPlaceholder"
x:Uid="TriggerKeyPlaceholder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorDisabledBrush}" />
<ItemsControl x:Name="TriggerKeys" IsTabStop="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
HorizontalSpacing="4"
Orientation="Horizontal"
VerticalSpacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyDropDownButton KeyName="{Binding}" Loaded="TriggerKeyDropDown_Loaded" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ToggleButton.Content>
</ToggleButton>
<CheckBox
@@ -204,6 +206,12 @@
<TextBlock x:Uid="ActionType_OpenApp_Text" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem x:Uid="ActionType_Disable" Tag="Disable">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE711;" />
<TextBlock x:Uid="ActionType_Disable_Text" />
</StackPanel>
</ComboBoxItem>
<!--
<ComboBoxItem x:Uid="ActionType_MouseClick" Tag="MouseClick">
<StackPanel Orientation="Horizontal" Spacing="8">
@@ -235,29 +243,32 @@
Style="{StaticResource CustomShortcutToggleButtonStyle}"
Unchecked="ActionKeyToggleBtn_Unchecked">
<ToggleButton.Content>
<ItemsControl x:Name="ActionKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
HorizontalSpacing="4"
Orientation="Horizontal"
VerticalSpacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual
Padding="8"
Background="{ThemeResource CustomAccentBackgroundBrush}"
BorderThickness="0"
Content="{Binding}"
CornerRadius="{StaticResource OverlayCornerRadius}"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{StaticResource DefaultKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid>
<TextBlock
x:Name="ActionKeyPlaceholder"
x:Uid="ActionKeyPlaceholder"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorDisabledBrush}" />
<ItemsControl x:Name="ActionKeys" IsTabStop="False">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
HorizontalSpacing="4"
Orientation="Horizontal"
VerticalSpacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyDropDownButton
KeyName="{Binding}"
Loaded="ActionKeyDropDown_Loaded"
UseAccentStyle="True" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ToggleButton.Content>
</ToggleButton>
</tkcontrols:Case>
@@ -288,7 +299,7 @@
<!-- Open App Action -->
<tkcontrols:Case Value="OpenApp">
<StackPanel Orientation="Vertical" Spacing="16">
<Grid ColumnSpacing="8">
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
@@ -307,13 +318,17 @@
Click="ProgramPathSelectButton_Click"
Content="{ui:FontIcon Glyph=&#xE8DA;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
Style="{StaticResource SubtleButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="ProgramPathSelectButtonTooltip" />
</ToolTipService.ToolTip>
</Button>
</Grid>
<TextBox
x:Name="ProgramArgsInput"
x:Uid="ProgramArgsInput"
GotFocus="ProgramArgsInput_GotFocus" />
<Grid ColumnSpacing="8">
<Grid ColumnSpacing="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
@@ -331,7 +346,11 @@
Click="StartInSelectButton_Click"
Content="{ui:FontIcon Glyph=&#xE8DA;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
Style="{StaticResource SubtleButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="StartInSelectButtonTooltip" />
</ToolTipService.ToolTip>
</Button>
</Grid>
<ComboBox
x:Name="ElevationComboBox"
@@ -372,6 +391,13 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</tkcontrols:Case>
<!-- Disable Action -->
<tkcontrols:Case Value="Disable">
<TextBlock
x:Uid="DisableDescription"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</tkcontrols:Case>
</tkcontrols:SwitchPresenter>
</StackPanel>
<!-- Validation InfoBar spanning all columns -->

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Storage;
@@ -77,6 +78,7 @@ namespace KeyboardManagerEditorUI.Controls
OpenUrl,
OpenApp,
MouseClick,
Disable,
}
/// <summary>
@@ -129,6 +131,7 @@ namespace KeyboardManagerEditorUI.Controls
"OpenUrl" => ActionType.OpenUrl,
"OpenApp" => ActionType.OpenApp,
"MouseClick" => ActionType.MouseClick,
"Disable" => ActionType.Disable,
_ => ActionType.KeyOrShortcut,
};
}
@@ -148,8 +151,17 @@ namespace KeyboardManagerEditorUI.Controls
TriggerKeys.ItemsSource = _triggerKeys;
ActionKeys.ItemsSource = _actionKeys;
_triggerKeys.CollectionChanged += (_, _) => RaiseValidationStateChanged();
_actionKeys.CollectionChanged += (_, _) => RaiseValidationStateChanged();
_triggerKeys.CollectionChanged += (_, _) =>
{
UpdatePlaceholderVisibility();
RaiseValidationStateChanged();
};
_actionKeys.CollectionChanged += (_, _) =>
{
UpdatePlaceholderVisibility();
RaiseValidationStateChanged();
};
this.Unloaded += UnifiedMappingControl_Unloaded;
}
@@ -209,6 +221,9 @@ namespace KeyboardManagerEditorUI.Controls
ActionKeyToggleBtn.IsChecked = false;
}
// Disable dropdowns during recording
SetDropDownsEnabled(TriggerKeys, false);
KeyboardHookHelper.Instance.ActivateHook(this);
}
}
@@ -219,6 +234,8 @@ namespace KeyboardManagerEditorUI.Controls
{
CleanupKeyboardHook();
}
SetDropDownsEnabled(TriggerKeys, true);
}
#endregion
@@ -262,6 +279,9 @@ namespace KeyboardManagerEditorUI.Controls
TriggerKeyToggleBtn.IsChecked = false;
}
// Disable dropdowns during recording
SetDropDownsEnabled(ActionKeys, false);
KeyboardHookHelper.Instance.ActivateHook(this);
}
}
@@ -272,6 +292,238 @@ namespace KeyboardManagerEditorUI.Controls
{
CleanupKeyboardHook();
}
SetDropDownsEnabled(ActionKeys, true);
}
#endregion
#region Key Dropdown Handling
private void TriggerKeyDropDown_Loaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
// Ensure we do not accumulate multiple subscriptions when Loaded fires repeatedly.
dropDown.KeyChanged -= TriggerKeyDropDown_KeyChanged;
dropDown.KeyChanged += TriggerKeyDropDown_KeyChanged;
// Use a named Unloaded handler so we can detach it and avoid accumulating handlers.
dropDown.Unloaded -= TriggerKeyDropDown_Unloaded;
dropDown.Unloaded += TriggerKeyDropDown_Unloaded;
}
}
private void TriggerKeyDropDown_Unloaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
dropDown.KeyChanged -= TriggerKeyDropDown_KeyChanged;
dropDown.Unloaded -= TriggerKeyDropDown_Unloaded;
}
}
private void ActionKeyDropDown_Loaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
// Ensure we do not accumulate multiple subscriptions when Loaded fires repeatedly.
dropDown.KeyChanged -= ActionKeyDropDown_KeyChanged;
dropDown.KeyChanged += ActionKeyDropDown_KeyChanged;
// Use a named Unloaded handler so we can detach it and avoid accumulating handlers.
dropDown.Unloaded -= ActionKeyDropDown_Unloaded;
dropDown.Unloaded += ActionKeyDropDown_Unloaded;
}
}
private void ActionKeyDropDown_Unloaded(object sender, RoutedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
dropDown.KeyChanged -= ActionKeyDropDown_KeyChanged;
dropDown.Unloaded -= ActionKeyDropDown_Unloaded;
}
}
private void TriggerKeyDropDown_KeyChanged(object? sender, KeyChangedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
int index = GetDropDownIndex(TriggerKeys, dropDown);
if (index >= 0 && index < _triggerKeys.Count)
{
// KeyCode 0 means "None" — treat as invalid selection and do not update.
if (e.NewKeyCode == 0)
{
RevertKeySelection(_triggerKeys, index);
return;
}
string? validationError = ValidateDropDownSelection(_triggerKeys, index, e.NewKeyCode, e.NewKeyName);
if (validationError != null)
{
RevertKeySelection(_triggerKeys, index);
ShowNotificationTip(validationError);
return;
}
_triggerKeys[index] = e.NewKeyName;
HandleAutoGrowShrink(_triggerKeys, index, e.NewKeyCode);
}
}
}
private void ActionKeyDropDown_KeyChanged(object? sender, KeyChangedEventArgs e)
{
if (sender is KeyDropDownButton dropDown)
{
int index = GetDropDownIndex(ActionKeys, dropDown);
if (index >= 0 && index < _actionKeys.Count)
{
// KeyCode 0 means "None" — treat as invalid selection and do not update.
if (e.NewKeyCode == 0)
{
RevertKeySelection(_actionKeys, index);
return;
}
string? validationError = ValidateDropDownSelection(_actionKeys, index, e.NewKeyCode, e.NewKeyName);
if (validationError != null)
{
RevertKeySelection(_actionKeys, index);
ShowNotificationTip(validationError);
return;
}
_actionKeys[index] = e.NewKeyName;
HandleAutoGrowShrink(_actionKeys, index, e.NewKeyCode);
}
}
}
/// <summary>
/// Reverts a key selection by re-inserting the current value via the bound ObservableCollection,
/// which forces the binding to refresh without breaking the binding expression.
/// </summary>
private static void RevertKeySelection(ObservableCollection<string> keys, int index)
{
string current = keys[index];
keys.RemoveAt(index);
keys.Insert(index, current);
}
private static int GetDropDownIndex(ItemsControl itemsControl, KeyDropDownButton dropDown)
{
for (int i = 0; i < itemsControl.Items.Count; i++)
{
var container = itemsControl.ContainerFromIndex(i) as ContentPresenter;
if (container != null)
{
// Walk the visual tree to find the KeyDropDownButton
var child = FindChild<KeyDropDownButton>(container);
if (child == dropDown)
{
return i;
}
}
}
return -1;
}
private static T? FindChild<T>(Microsoft.UI.Xaml.DependencyObject parent)
where T : Microsoft.UI.Xaml.DependencyObject
{
int childCount = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childCount; i++)
{
var child = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(parent, i);
if (child is T result)
{
return result;
}
var descendant = FindChild<T>(child);
if (descendant != null)
{
return descendant;
}
}
return null;
}
/// <summary>
/// Validates a key selection from a dropdown before it is applied.
/// Returns null if valid, or an error message string if invalid.
/// </summary>
private static string? ValidateDropDownSelection(ObservableCollection<string> keys, int changedIndex, int newKeyCode, string newKeyName)
{
const int maxShortcutSize = 5;
// KeyType: 0=Win, 1=Ctrl, 2=Alt, 3=Shift, 4=Action
int newKeyType = KeyboardManagerInterop.GetKeyType(newKeyCode);
bool isModifier = newKeyType < 4;
// Count only non-empty (real) entries to determine effective shortcut size.
int nonEmptyCount = keys.Count(k => !string.IsNullOrEmpty(k));
// Rule: action key at position 0 in multi-key shortcut (shortcut must start with modifier)
if (!isModifier && changedIndex == 0 && nonEmptyCount > 1)
{
return ResourceHelper.GetString("Warning_ShortcutStartWithModifier");
}
// Rule: no repeated modifier types (skip empty placeholder slots)
if (isModifier)
{
for (int i = 0; i < keys.Count; i++)
{
if (i == changedIndex || string.IsNullOrEmpty(keys[i]))
{
continue;
}
int existingKeyCode = KeyboardManagerInterop.GetKeyCodeFromName(keys[i]);
int existingKeyType = KeyboardManagerInterop.GetKeyType(existingKeyCode);
if (existingKeyType == newKeyType)
{
return ResourceHelper.GetString("Warning_RepeatedModifier");
}
}
}
// Rule: modifier at last position when already at max size
if (isModifier && changedIndex == keys.Count - 1 && nonEmptyCount >= maxShortcutSize)
{
return ResourceHelper.GetString("Warning_MaxShortcutSize");
}
return null;
}
private void HandleAutoGrowShrink(ObservableCollection<string> keys, int changedIndex, int newKeyCode)
{
const int maxShortcutSize = 5;
int keyType = KeyboardManagerInterop.GetKeyType(newKeyCode);
bool isModifier = keyType < 4;
if (isModifier && changedIndex == keys.Count - 1 && keys.Count < maxShortcutSize)
{
// Modifier at last position — auto-grow: add placeholder for next key
keys.Add(string.Empty);
}
else if (!isModifier)
{
// Action key — trim any trailing entries after this one
while (keys.Count > changedIndex + 1)
{
keys.RemoveAt(keys.Count - 1);
}
}
}
#endregion
@@ -453,7 +705,7 @@ namespace KeyboardManagerEditorUI.Controls
public void OnInputLimitReached()
{
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
ShowNotificationTip(ResourceHelper.GetString("Warning_InputLimitReached"));
}
#endregion
@@ -463,12 +715,12 @@ namespace KeyboardManagerEditorUI.Controls
/// <summary>
/// Gets the trigger keys.
/// </summary>
public List<string> GetTriggerKeys() => _triggerKeys.ToList();
public List<string> GetTriggerKeys() => _triggerKeys.Where(k => !string.IsNullOrEmpty(k)).ToList();
/// <summary>
/// Gets the action keys (for Key/Shortcut action type).
/// </summary>
public List<string> GetActionKeys() => _actionKeys.ToList();
public List<string> GetActionKeys() => _actionKeys.Where(k => !string.IsNullOrEmpty(k)).ToList();
/// <summary>
/// Gets the selected mouse trigger.
@@ -567,6 +819,7 @@ namespace KeyboardManagerEditorUI.Controls
ActionType.Text => !string.IsNullOrEmpty(TextContentBox?.Text),
ActionType.OpenUrl => !string.IsNullOrWhiteSpace(UrlPathInput?.Text),
ActionType.OpenApp => !string.IsNullOrWhiteSpace(ProgramPathInput?.Text),
ActionType.Disable => true,
_ => false,
};
}
@@ -612,18 +865,28 @@ namespace KeyboardManagerEditorUI.Controls
/// </summary>
public void SetActionType(ActionType actionType)
{
int index = actionType switch
if (ActionTypeComboBox == null)
{
ActionType.Text => 1,
ActionType.OpenUrl => 2,
ActionType.OpenApp => 3,
ActionType.MouseClick => 4,
_ => 0,
return;
}
string tag = actionType switch
{
ActionType.Text => "Text",
ActionType.OpenUrl => "OpenUrl",
ActionType.OpenApp => "OpenApp",
ActionType.Disable => "Disable",
ActionType.MouseClick => "MouseClick",
_ => "KeyOrShortcut",
};
if (ActionTypeComboBox != null)
foreach (var item in ActionTypeComboBox.Items)
{
ActionTypeComboBox.SelectedIndex = index;
if (item is ComboBoxItem comboBoxItem && comboBoxItem.Tag is string itemTag && itemTag == tag)
{
ActionTypeComboBox.SelectedItem = comboBoxItem;
return;
}
}
}
@@ -748,11 +1011,44 @@ namespace KeyboardManagerEditorUI.Controls
}
}
private static void SetDropDownsEnabled(ItemsControl itemsControl, bool enabled)
{
for (int i = 0; i < itemsControl.Items.Count; i++)
{
var container = itemsControl.ContainerFromIndex(i) as ContentPresenter;
if (container != null)
{
var dropDown = FindChild<KeyDropDownButton>(container);
if (dropDown != null)
{
dropDown.IsEnabled = enabled;
}
}
}
}
private void CleanupKeyboardHook()
{
KeyboardHookHelper.Instance.CleanupHook();
}
private void UpdatePlaceholderVisibility()
{
if (TriggerKeyPlaceholder != null)
{
TriggerKeyPlaceholder.Visibility = _triggerKeys.Count == 0
? Microsoft.UI.Xaml.Visibility.Visible
: Microsoft.UI.Xaml.Visibility.Collapsed;
}
if (ActionKeyPlaceholder != null)
{
ActionKeyPlaceholder.Visibility = _actionKeys.Count == 0
? Microsoft.UI.Xaml.Visibility.Visible
: Microsoft.UI.Xaml.Visibility.Collapsed;
}
}
private void RaiseValidationStateChanged()
{
UpdateInlineValidation();
@@ -910,7 +1206,7 @@ namespace KeyboardManagerEditorUI.Controls
/// </summary>
public void ShowNotificationTip(string message)
{
ShowValidationMessage("Warning", message, InfoBarSeverity.Warning);
ShowValidationMessage(ResourceHelper.GetString("Warning_Title"), message, InfoBarSeverity.Warning);
}
/// <summary>
@@ -932,7 +1228,7 @@ namespace KeyboardManagerEditorUI.Controls
}
else
{
ShowValidationError("Validation Error", "An unknown validation error occurred.");
ShowValidationError(ResourceHelper.GetString("Error_UnknownValidation_Title"), ResourceHelper.GetString("Error_UnknownValidation_Message"));
}
}

View File

@@ -8,6 +8,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Interop;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Windows.System;
@@ -19,7 +20,7 @@ namespace KeyboardManagerEditorUI.Helpers
public static KeyboardHookHelper Instance => _instance ??= new KeyboardHookHelper();
private KeyboardMappingService _mappingService;
private KeyboardMappingService? _mappingService;
private HotkeySettingsControlHook? _keyboardHook;
@@ -34,7 +35,14 @@ namespace KeyboardManagerEditorUI.Helpers
// Singleton to make sure only one instance of the hook is active
private KeyboardHookHelper()
{
_mappingService = new KeyboardMappingService();
try
{
_mappingService = new KeyboardMappingService();
}
catch (Exception ex)
{
Logger.LogWarning($"Native KBM library unavailable for keyboard hook: {ex.Message}");
}
}
public void ActivateHook(IKeyboardHookTarget target)
@@ -46,11 +54,18 @@ namespace KeyboardManagerEditorUI.Helpers
_currentlyPressedKeys.Clear();
_keyPressOrder.Clear();
_keyboardHook = new HotkeySettingsControlHook(
KeyDown,
KeyUp,
() => true,
(key, extraInfo) => true);
try
{
_keyboardHook = new HotkeySettingsControlHook(
KeyDown,
KeyUp,
() => true,
(key, extraInfo) => true);
}
catch (Exception ex)
{
Logger.LogWarning($"Keyboard hook unavailable: {ex.Message}");
}
}
public void CleanupHook()
@@ -110,6 +125,17 @@ namespace KeyboardManagerEditorUI.Helpers
{
_keyPressOrder.Add(virtualKey);
// When building chords, cap at 2 action keys: if a third action key arrives,
// remove the oldest (shift behavior matching old editor).
if (_activeTarget.AllowChords && !RemappingHelper.IsModifierKey(virtualKey))
{
var actionKeysInOrder = _keyPressOrder.Where(k => !RemappingHelper.IsModifierKey(k)).ToList();
if (actionKeysInOrder.Count > 2)
{
_keyPressOrder.Remove(actionKeysInOrder[0]);
}
}
// Notify the target page
_activeTarget.OnKeyDown(virtualKey, GetFormattedKeyList());
}
@@ -126,7 +152,13 @@ namespace KeyboardManagerEditorUI.Helpers
if (_currentlyPressedKeys.Remove(virtualKey))
{
_keyPressOrder.Remove(virtualKey);
// When building chords, preserve released action keys in _keyPressOrder
// so the next action key press is recognized as the chord's second key.
// Only modifier releases clear from _keyPressOrder (matching old editor behavior).
if (!_activeTarget.AllowChords || RemappingHelper.IsModifierKey(virtualKey))
{
_keyPressOrder.Remove(virtualKey);
}
_activeTarget.OnKeyUp(virtualKey, GetFormattedKeyList());
}
@@ -140,6 +172,11 @@ namespace KeyboardManagerEditorUI.Helpers
return new List<string>();
}
if (_mappingService is null)
{
return new List<string>();
}
List<string> keyList = new List<string>();
List<VirtualKey> modifierKeys = new List<VirtualKey>();
VirtualKey? actionKey = null;
@@ -147,9 +184,15 @@ namespace KeyboardManagerEditorUI.Helpers
foreach (var key in _keyPressOrder)
{
// For modifiers, only include if currently pressed.
// For action keys when building chords, also include released keys
// so the chord's first key stays visible while waiting for the second.
if (!_currentlyPressedKeys.Contains(key))
{
continue;
if (RemappingHelper.IsModifierKey(key) || !_activeTarget.AllowChords)
{
continue;
}
}
if (RemappingHelper.IsModifierKey(key))
@@ -189,6 +232,11 @@ namespace KeyboardManagerEditorUI.Helpers
private void RemoveExistingModifierVariant(VirtualKey key)
{
if (_mappingService is null)
{
return;
}
KeyType keyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)key);
// No need to remove if the key is an action key

View File

@@ -0,0 +1,19 @@
// 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.Windows.ApplicationModel.Resources;
namespace KeyboardManagerEditorUI.Helpers
{
public static class ResourceHelper
{
private static ResourceLoader? _resourceLoader;
public static string GetString(string resourceKey)
{
_resourceLoader ??= new ResourceLoader();
return _resourceLoader.GetString(resourceKey);
}
}
}

View File

@@ -0,0 +1,52 @@
// 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.Diagnostics;
namespace KeyboardManagerEditorUI.Helpers
{
public static class ServiceStatusHelper
{
private const string KeyboardManagerEngineProcessName = "PowerToys.KeyboardManagerEngine";
public static bool IsKeyboardManagerServiceRunning()
{
try
{
var processes = Process.GetProcessesByName(KeyboardManagerEngineProcessName);
bool running = processes.Length > 0;
foreach (var process in processes)
{
process.Dispose();
}
return running;
}
catch (Exception)
{
return false;
}
}
public static bool IsPowerToysRunning()
{
try
{
var processes = Process.GetProcessesByName("PowerToys");
bool running = processes.Length > 0;
foreach (var process in processes)
{
process.Dispose();
}
return running;
}
catch (Exception)
{
return false;
}
}
}
}

View File

@@ -19,6 +19,7 @@ namespace KeyboardManagerEditorUI.Helpers
EmptyAppName,
IllegalShortcut,
DuplicateMapping,
ConflictingModifier,
SelfMapping,
EmptyTargetText,
EmptyUrl,

View File

@@ -16,17 +16,18 @@ namespace KeyboardManagerEditorUI.Helpers
{
public static readonly Dictionary<ValidationErrorType, (string Title, string Message)> ValidationMessages = new()
{
{ ValidationErrorType.EmptyOriginalKeys, ("Missing Original Keys", "Please enter at least one original key to create a remapping.") },
{ ValidationErrorType.EmptyRemappedKeys, ("Missing Target Keys", "Please enter at least one target key to create a remapping.") },
{ ValidationErrorType.ModifierOnly, ("Invalid Shortcut", "Shortcuts must contain at least one action key in addition to modifier keys (Ctrl, Alt, Shift, Win).") },
{ ValidationErrorType.EmptyAppName, ("Missing Application Name", "You've selected app-specific remapping but haven't specified an application name. Please enter the application name.") },
{ ValidationErrorType.IllegalShortcut, ("Reserved System Shortcut", "Win+L and Ctrl+Alt+Delete are reserved system shortcuts and cannot be remapped.") },
{ ValidationErrorType.DuplicateMapping, ("Duplicate Remapping", "This key or shortcut is already remapped.") },
{ ValidationErrorType.SelfMapping, ("Invalid Remapping", "A key or shortcut cannot be remapped to itself. Please choose a different target.") },
{ ValidationErrorType.EmptyTargetText, ("Missing Target Text", "Please enter the text to be inserted when the shortcut is pressed.") },
{ ValidationErrorType.EmptyUrl, ("Missing URL", "Please enter the URL to open when the shortcut is pressed.") },
{ ValidationErrorType.EmptyProgramPath, ("Missing Program Path", "Please enter the program path to launch when the shortcut is pressed.") },
{ ValidationErrorType.OneKeyMapping, ("Invalid Remapping", "A single key cannot be remapped to a Program or URL shortcut. Please choose a combination of keys.") },
{ ValidationErrorType.EmptyOriginalKeys, (ResourceHelper.GetString("Validation_EmptyOriginalKeys_Title"), ResourceHelper.GetString("Validation_EmptyOriginalKeys_Message")) },
{ ValidationErrorType.EmptyRemappedKeys, (ResourceHelper.GetString("Validation_EmptyRemappedKeys_Title"), ResourceHelper.GetString("Validation_EmptyRemappedKeys_Message")) },
{ ValidationErrorType.ModifierOnly, (ResourceHelper.GetString("Validation_ModifierOnly_Title"), ResourceHelper.GetString("Validation_ModifierOnly_Message")) },
{ ValidationErrorType.EmptyAppName, (ResourceHelper.GetString("Validation_EmptyAppName_Title"), ResourceHelper.GetString("Validation_EmptyAppName_Message")) },
{ ValidationErrorType.IllegalShortcut, (ResourceHelper.GetString("Validation_IllegalShortcut_Title"), ResourceHelper.GetString("Validation_IllegalShortcut_Message")) },
{ ValidationErrorType.DuplicateMapping, (ResourceHelper.GetString("Validation_DuplicateMapping_Title"), ResourceHelper.GetString("Validation_DuplicateMapping_Message")) },
{ ValidationErrorType.ConflictingModifier, (ResourceHelper.GetString("Validation_ConflictingModifier_Title"), ResourceHelper.GetString("Validation_ConflictingModifier_Message")) },
{ ValidationErrorType.SelfMapping, (ResourceHelper.GetString("Validation_SelfMapping_Title"), ResourceHelper.GetString("Validation_SelfMapping_Message")) },
{ ValidationErrorType.EmptyTargetText, (ResourceHelper.GetString("Validation_EmptyTargetText_Title"), ResourceHelper.GetString("Validation_EmptyTargetText_Message")) },
{ ValidationErrorType.EmptyUrl, (ResourceHelper.GetString("Validation_EmptyUrl_Title"), ResourceHelper.GetString("Validation_EmptyUrl_Message")) },
{ ValidationErrorType.EmptyProgramPath, (ResourceHelper.GetString("Validation_EmptyProgramPath_Title"), ResourceHelper.GetString("Validation_EmptyProgramPath_Message")) },
{ ValidationErrorType.OneKeyMapping, (ResourceHelper.GetString("Validation_OneKeyMapping_Title"), ResourceHelper.GetString("Validation_OneKeyMapping_Message")) },
};
public static ValidationErrorType ValidateKeyMapping(
@@ -69,6 +70,11 @@ namespace KeyboardManagerEditorUI.Helpers
return ValidationErrorType.DuplicateMapping;
}
if (originalKeys.Count == 1 && HasConflictingModifierMapping(originalKeys[0], isEditMode, mappingService))
{
return ValidationErrorType.ConflictingModifier;
}
if (IsSelfMapping(originalKeys, remappedKeys, mappingService))
{
return ValidationErrorType.SelfMapping;
@@ -77,6 +83,47 @@ namespace KeyboardManagerEditorUI.Helpers
return ValidationErrorType.NoError;
}
public static ValidationErrorType ValidateDisableMapping(
List<string> originalKeys,
bool isAppSpecific,
string appName,
KeyboardMappingService mappingService,
bool isEditMode = false,
Remapping? editingRemapping = null)
{
if (originalKeys == null || originalKeys.Count == 0)
{
return ValidationErrorType.EmptyOriginalKeys;
}
if (originalKeys.Count > 1 && ContainsOnlyModifierKeys(originalKeys))
{
return ValidationErrorType.ModifierOnly;
}
if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
{
return ValidationErrorType.EmptyAppName;
}
if (originalKeys.Count > 1 && IsIllegalShortcut(originalKeys, mappingService))
{
return ValidationErrorType.IllegalShortcut;
}
if (IsDuplicateMapping(originalKeys, isEditMode, mappingService, appName))
{
return ValidationErrorType.DuplicateMapping;
}
if (originalKeys.Count == 1 && HasConflictingModifierMapping(originalKeys[0], isEditMode, mappingService))
{
return ValidationErrorType.ConflictingModifier;
}
return ValidationErrorType.NoError;
}
public static ValidationErrorType ValidateTextMapping(
List<string> keys,
string textContent,
@@ -239,6 +286,58 @@ namespace KeyboardManagerEditorUI.Helpers
return KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString);
}
/// <summary>
/// Checks if a single key conflicts with existing single-key mappings via modifier variants.
/// E.g., remapping LCtrl when Ctrl is already mapped, or vice versa.
/// </summary>
private static bool HasConflictingModifierMapping(string keyName, bool isEditMode, KeyboardMappingService mappingService)
{
int keyCode = KeyboardManagerInterop.GetKeyCodeFromName(keyName);
int keyType = KeyboardManagerInterop.GetKeyType(keyCode);
// Only modifier keys can conflict with their variants
if (keyType >= 4)
{
return false;
}
int upperLimit = isEditMode ? 1 : 0;
int conflictCount = 0;
foreach (var settings in SettingsManager.EditorSettings.ShortcutSettingsDictionary.Values)
{
string existingOriginal = settings.Shortcut.OriginalKeys;
// Only check single-key mappings (no semicolons)
if (string.IsNullOrEmpty(existingOriginal) || existingOriginal.Contains(';'))
{
continue;
}
if (int.TryParse(existingOriginal, out int existingKeyCode))
{
if (existingKeyCode == keyCode)
{
continue; // Exact match handled by DuplicateMapping
}
int existingKeyType = KeyboardManagerInterop.GetKeyType(existingKeyCode);
// Same modifier type (e.g., Ctrl and LCtrl) = conflict
if (existingKeyType == keyType)
{
conflictCount++;
if (conflictCount > upperLimit)
{
return true;
}
}
}
}
return false;
}
private static string BuildKeyCodeString(List<string> keys, KeyboardMappingService mappingService)
{
return string.Join(";", keys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));

View File

@@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace KeyboardManagerEditorUI.Interop
{
public record KeyNameEntry(int KeyCode, string DisplayName);
}

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