Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
bb34f71b62 Merge remote-tracking branch 'origin/main' into dev/duhowett/ws2025
# Conflicts:
#	.pipelines/v2/oneFuzz.yml
#	.pipelines/v2/release.yml
#	.pipelines/v2/templates/pipeline-ci-build.yml
#	.pipelines/v2/templates/pipeline-ui-tests-full-build.yml

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-04-01 09:55:36 +00:00
Dustin L. Howett
6fdff7a10f this is easier than mucking around 2025-09-30 12:14:54 -05:00
Dustin L. Howett
b78fa62ba3 Move to Windows Server 2025 build agent images 2025-09-30 12:12:48 -05:00
186 changed files with 4557 additions and 8792 deletions

View File

@@ -19,7 +19,6 @@ OLIVEGREEN
PALEBLUE
PArgb
Pbgra
SRGBTo
WHITEONBLACK
@@ -49,6 +48,7 @@ nupkg
petabyte
resw
resx
runtimeconfig
srt
Stereolithography
terabyte
@@ -332,7 +332,6 @@ REGSTR
INVOKEIDLIST
MEMORYSTATUSEX
ABE
Mdt
HTCAPTION
POSCHANGED
QUERYPOS
@@ -342,29 +341,6 @@ 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,9 +178,7 @@ Taras
TBM
Teutsch
tilovell
traies
Triet
udit
urnotdfs
vednig
waaverecords

View File

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

View File

@@ -191,6 +191,15 @@ 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,30 +233,6 @@ 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@v3
uses: azure/login@v2
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@v3
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |-

View File

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

View File

@@ -217,11 +217,7 @@
"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

@@ -15,6 +15,11 @@ parameters:
type: boolean
default: false
- name: codeSign
displayName: "Enable Code Signing"
type: boolean
default: true
- name: versionNumber
displayName: "Version Number"
type: string
@@ -89,7 +94,7 @@ extends:
versionNumber: ${{ parameters.versionNumber }}
publishArtifacts: false # 1ES PT handles publication for us.
official: true
codeSign: true
codeSign: ${{ parameters.codeSign }}
runTests: false
buildTests: false
signingIdentity:
@@ -130,7 +135,7 @@ extends:
name: SHINE-INT-L
os: windows
official: true
codeSign: true
codeSign: ${{ parameters.codeSign }}
signingIdentity:
serviceName: $(SigningServiceName)
appId: $(SigningAppId)

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,15 +26,13 @@ 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
@@ -46,7 +44,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 |
|------|---------|
@@ -54,7 +52,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`)
@@ -64,10 +62,9 @@ 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
@@ -76,18 +73,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 |
|------|--------------|-------|
@@ -95,13 +92,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
@@ -110,14 +107,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 |
|------|---------|-----------|
@@ -126,7 +123,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
@@ -146,27 +143,23 @@ 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,109 +1,84 @@
# Community
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.
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.
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 the New+ utility
### [@cgaarden](https://github.com/cgaarden) - [Christian Gaarden Gaardmark](https://www.onegreatworld.com)
Christian contributed 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 multiple new features to Keyboard Manager, such as key chord support and launching apps. He also contributed multiple features/fixes to PowerToys.
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.
### [@snickler](https://github.com/snickler) - [Jeremy Sinclair](http://sinclairinat0r.com)
Jeremy has helped drive substantial ARM64 support within PowerToys.
Jeremy has helped drive large sums of the ARM64 support inside 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 with triaging, discussing issues as well as fixing bugs and building features for Text Extractor.
Joe has helped 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 contribute features to PowerToys Run, such as the unit converter plugin
ThiefZero has helped out contributing a 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
@@ -119,8 +94,7 @@ 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
@@ -128,7 +102,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
@@ -151,7 +125,6 @@ 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
@@ -162,48 +135,46 @@ 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 everyone's time:
Please follow this rule to help eliminate wasted effort and frustration, and to ensure an efficient and effective use of everyones 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 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.
* 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.
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!
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!
### 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 or features
## Contributing Fixes/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.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.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.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.250325.1"/>
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.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,7 +106,8 @@ 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
@@ -116,7 +117,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
@@ -143,7 +144,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
@@ -153,7 +154,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
@@ -177,13 +178,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)
@@ -207,9 +208,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.
@@ -239,9 +240,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
@@ -715,9 +716,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)
@@ -743,10 +744,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)
@@ -769,9 +770,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)
@@ -785,9 +786,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
@@ -815,7 +816,7 @@ SOFTWARE.
### spdlog
**Source**: <https://github.com/gabime/spdlog>
**Source**: https://github.com/gabime/spdlog
The MIT License (MIT)
@@ -840,12 +841,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
@@ -873,7 +874,7 @@ DEALINGS IN THE SOFTWARE.
### zip
**Source**: <https://github.com/kuba--/zip>
**Source**: https://github.com/kuba--/zip
All Rights Reserved.
@@ -901,7 +902,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
@@ -924,9 +925,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)
@@ -950,11 +951,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
@@ -978,13 +979,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
@@ -1462,9 +1463,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
@@ -1491,11 +1492,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
@@ -1525,7 +1526,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,7 +57,6 @@
<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/">
@@ -710,19 +709,17 @@
</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,13 +19,14 @@
<span> · </span>
<a href="#-whats-new">Release notes</a>
</h3>
<br/><br/>
## 🔨 Utilities
PowerToys includes over 30 utilities to help you customize and optimize your Windows experience:
PowerToys includes over 25 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) |
@@ -37,27 +38,28 @@ PowerToys includes over 30 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
For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
## 📋 Installation
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 the .exe file from GitHub</strong></summary>
<summary><strong>Download .exe from GitHub</strong></summary>
<br/>
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_.
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.
<!-- 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] |
@@ -81,16 +83,14 @@ You can easily install PowerToys from the Microsoft Store:
<details>
<summary><strong>WinGet</strong></summary>
<br/>
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)
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:
*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 [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.
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.
</details>
## ✨ What's new?
@@ -108,26 +108,28 @@ There are [community driven install methods](https://learn.microsoft.com/windows
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
## 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.
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.
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
## Code of conduct
## 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).
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
[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

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 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).
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).
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,21 +1,24 @@
# Support
## How to use Microsoft PowerToys
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 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]!
## 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 visit our documentation and [Contributor's Guide][contributor] if you want to contribute to PowerToys.
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.
## 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
[gh-feature]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=feature_request.md
[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
[contributor]: https://github.com/microsoft/PowerToys/blob/main/CONTRIBUTING.md
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs

View File

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

View File

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

View File

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

View File

@@ -212,10 +212,6 @@ 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,8 +14,6 @@
#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>
@@ -23,8 +21,6 @@
#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>
@@ -42,16 +38,15 @@ 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() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
if (error)
{
return std::nullopt;
}
return dst_path;
return std::move(dst_path);
}
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
@@ -62,9 +57,34 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
auto state = UpdateState::read();
// 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)
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)
{
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
if (fs::is_regular_file(installer))
@@ -77,44 +97,12 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
return std::nullopt;
}
}
if (state.state == UpdateState::upToDate)
else 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;
}
@@ -128,29 +116,13 @@ 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
}
}
}
// 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));
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
arguments += L" \"";
arguments += installer.c_str();
arguments += L"\"";
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = copy_in_temp->c_str();
@@ -218,16 +190,9 @@ 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());
@@ -236,10 +201,6 @@ 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();
@@ -256,12 +217,6 @@ 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)
@@ -272,37 +227,6 @@ 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.250325.1" targetFramework="native" />
</packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@@ -1,679 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#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

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<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

@@ -1,5 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "pch.h"

View File

@@ -1,17 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#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

@@ -1,170 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#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

@@ -1,47 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#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>The formatted resource string.</returns>
/// <returns></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">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>
/// <typeparam name="T"></typeparam>
/// <param name="args"></param>
/// <returns></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>The JSON schema string for the settings resource object.</returns>
/// <returns></returns>
public string Schema();
}

View File

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

View File

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

View File

@@ -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>
<!-- <string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string> --><!-- TEMPORARILY_DISABLED: PowerDisplay -->
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>

View File

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

View File

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

View File

@@ -1,180 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.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.250325.1" targetFramework="native" />
</packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

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

View File

@@ -1,48 +0,0 @@
# 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

@@ -1,353 +0,0 @@
---
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

@@ -1,145 +0,0 @@
---
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

@@ -1,536 +0,0 @@
# 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

@@ -1,149 +0,0 @@
---
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

@@ -1,202 +0,0 @@
---
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

@@ -1,164 +0,0 @@
---
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

@@ -1,66 +0,0 @@
---
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

@@ -1,169 +0,0 @@
# 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

@@ -1,413 +0,0 @@
# 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 a value indicating whether the atomic boolean is true.
/// Gets or sets the boolean value atomically
/// </summary>
public bool Value
{

View File

@@ -1,68 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.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

@@ -1,24 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.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

@@ -1,31 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.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,7 +26,6 @@
<ItemGroup>
<ProjectReference Include="..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
</ItemGroup>
<ItemGroup>
@@ -61,8 +60,4 @@
</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>Gets a value indicating whether IME syllable separators (') are removed for query secondary variant.</summary>
/// <summary>Remove IME syllable separators (') 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 a value indicating whether the backdrop opacity slider should be visible.
/// Gets whether the backdrop opacity slider should be visible.
/// </summary>
public bool IsBackdropOpacityVisible =>
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets a value indicating whether the backdrop description (for styles without options) should be visible.
/// Gets whether the backdrop description (for styles without options) should be visible.
/// </summary>
public bool IsMicaBackdropDescriptionVisible =>
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets a value indicating whether background/colorization settings are available.
/// Gets whether background/colorization settings are available.
/// </summary>
public bool IsBackgroundSettingsEnabled =>
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
/// <summary>
/// Gets a value indicating whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
/// Gets 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 a value indicating whether this backdrop style supports custom colorization (tint colors).
/// Gets whether this backdrop style supports custom colorization (tint colors).
/// </summary>
public bool SupportsColorization { get; init; } = true;
/// <summary>
/// Gets a value indicating whether this backdrop style supports custom background images.
/// Gets whether this backdrop style supports custom background images.
/// </summary>
public bool SupportsBackgroundImage { get; init; } = true;
/// <summary>
/// Gets a value indicating whether this backdrop style supports opacity adjustment.
/// Gets whether this backdrop style supports opacity adjustment.
/// </summary>
public bool SupportsOpacity { get; init; } = true;

View File

@@ -31,7 +31,6 @@ 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 a value indicating whether colorization is active (accent color, custom color, or image mode).
/// Gets 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 whether titles are shown for items in this band.
/// Gets or sets 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 whether subtitles are shown for items in this band.
/// Gets or sets 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 a value for backward compatibility. Maps to ShowTitles.
/// Gets or sets 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 the left position in device pixels.
/// Gets or sets left position in device pixels.
/// </summary>
public int X { get; init; }
/// <summary>
/// Gets the top position in device pixels.
/// Gets or sets top position in device pixels.
/// </summary>
public int Y { get; init; }
/// <summary>
/// Gets the width in device pixels.
/// Gets or sets width in device pixels.
/// </summary>
public int Width { get; init; }
/// <summary>
/// Gets the height in device pixels.
/// Gets or sets height in device pixels.
/// </summary>
public int Height { get; init; }
/// <summary>
/// Gets the width of the screen in device pixels where the window is located.
/// Gets or sets width of the screen in device pixels where the window is located.
/// </summary>
public int ScreenWidth { get; init; }
/// <summary>
/// Gets the height of the screen in device pixels where the window is located.
/// Gets or sets height of the screen in device pixels where the window is located.
/// </summary>
public int ScreenHeight { get; init; }
/// <summary>
/// Gets the DPI (dots per inch) of the display where the window is located.
/// Gets or sets DPI (dots per inch) of the display where the window is located.
/// </summary>
public int Dpi { get; init; }

View File

@@ -5,7 +5,6 @@
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;
@@ -126,8 +125,6 @@ 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,6 +157,12 @@ 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;
@@ -164,6 +170,14 @@ 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)
@@ -318,6 +332,19 @@ 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 a value indicating whether the dock is enabled.
/// Gets or sets 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 a value indicating whether the command executed successfully.
/// Gets or sets whether the command executed successfully.
/// </summary>
public bool Success { get; set; }

View File

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

View File

@@ -166,12 +166,7 @@
<tkcontrols:MarkdownThemes
x:Key="DefaultMarkdownThemeConfig"
H3FontSize="12"
H3FontWeight="Normal"
InlineCodeBackground="{StaticResource ControlFillColorDefaultBrush}"
InlineCodeBorderBrush="{StaticResource ControlElevationBorderBrush}"
InlineCodeCornerRadius="2"
InlineCodeForeground="{StaticResource TextFillColorSecondaryBrush}"
InlineCodePadding="2,0,2,1" />
H3FontWeight="Normal" />
<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("Search files", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Search files");
searchFileItem.DoubleClick();
SetFilesExtensionSearchBox("AppData");
@@ -36,7 +36,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("calculator");
var searchFileItem = this.Find<NavigationViewItem>("Calculator");
Assert.AreEqual("Calculator", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Calculator");
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("Time and date", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Time and date");
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("Open Windows Terminal profiles", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Open Windows Terminal profiles");
searchFileItem.DoubleClick();
// SetSearchBox("PowerShell");
@@ -77,7 +77,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("Windows settings");
var searchFileItem = this.Find<NavigationViewItem>("Windows settings");
Assert.AreEqual("Windows settings", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Windows settings");
searchFileItem.DoubleClick();
SetSearchBox("power");
@@ -91,7 +91,7 @@ public class BasicTests : CommandPaletteTestBase
SetSearchBox("Registry");
var searchFileItem = this.Find<NavigationViewItem>("Registry");
Assert.AreEqual("Registry", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Registry");
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("Windows Services", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Windows Services");
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("Windows System Commands", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Windows System Commands");
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("Search files", searchFileItem.Name);
Assert.AreEqual(searchFileItem.Name, "Search files");
searchFileItem.DoubleClick();
}

View File

@@ -76,7 +76,7 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
};
/// <summary>
/// Gets the parsed search result limit. Returns <see langword="null"/> when the caller should
/// 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,7 +146,8 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"{_namespace}.settings.json");
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public AllAppsSettings()
@@ -161,6 +162,7 @@ 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,7 +102,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"{_namespace}.settings.json");
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
@@ -116,6 +117,7 @@ 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,7 +48,8 @@ internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"{Namespace}.settings.json");
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
@@ -59,6 +60,7 @@ 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,13 +18,18 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"{_namespace}.settings.json");
// now, the state is just next to the exe
return Path.Combine(directory, "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, $"{_namespace}.settings.json");
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
@@ -45,6 +45,7 @@ internal class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_predefinedConnections);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();

View File

@@ -2,6 +2,8 @@
// 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;
@@ -51,7 +53,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"{_namespace}.settings.json");
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
@@ -61,6 +64,7 @@ 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,7 +42,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"{_namespace}.settings.json");
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public bool ShowDialogToConfirmCommand() => _showDialogToConfirmCommand.Value;
@@ -64,6 +65,7 @@ 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,7 +152,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"{_namespace}.settings.json");
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
@@ -169,6 +170,7 @@ 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>The default browser information, or <see langword="null"/> if it could not be determined.</returns>
/// <returns></returns>
BrowserInfo? GetDefaultBrowser();
}

View File

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

View File

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

View File

@@ -65,7 +65,8 @@ public class SettingsManager : JsonSettingsManager
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, $"{_namespace}.settings.json");
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
@@ -78,6 +79,7 @@ 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,8 +28,7 @@ public abstract class JsonSettingsManager
var filePath = FilePath;
if (!File.Exists(filePath))
{
// No settings file yet: keep in-memory defaults without persisting.
// The file is created on the first user-initiated settings change.
ExtensionHost.LogMessage(new LogMessage() { Message = "The provided settings file does not exist" });
return;
}

View File

@@ -12,7 +12,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
public static class ShellHelpers
{
/// <summary>
/// Gets the executable file extensions that Windows Shell recognizes. Unlike CMD/PowerShell,
/// These are 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">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>
/// <param name="name"></param>
/// <param name="fullPath"></param>
/// <returns></returns>
public static bool TryResolveExecutableAsShell(string name, out string fullPath)
{
// First check if we can find the file in the registry

View File

@@ -1,459 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.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(expected, result);
Assert.AreEqual(result, expected);
}
}
}

View File

@@ -121,14 +121,6 @@ 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
@@ -523,25 +515,6 @@ 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

@@ -1,24 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
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

@@ -1,46 +0,0 @@
<?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

@@ -1,137 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using 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,7 +6,6 @@
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"
@@ -81,29 +80,28 @@
Style="{StaticResource CustomShortcutToggleButtonStyle}"
Unchecked="TriggerKeyToggleBtn_Unchecked">
<ToggleButton.Content>
<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>
<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>
</ToggleButton.Content>
</ToggleButton>
<CheckBox
@@ -206,12 +204,6 @@
<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">
@@ -243,32 +235,29 @@
Style="{StaticResource CustomShortcutToggleButtonStyle}"
Unchecked="ActionKeyToggleBtn_Unchecked">
<ToggleButton.Content>
<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>
<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>
</ToggleButton.Content>
</ToggleButton>
</tkcontrols:Case>
@@ -299,7 +288,7 @@
<!-- Open App Action -->
<tkcontrols:Case Value="OpenApp">
<StackPanel Orientation="Vertical" Spacing="16">
<Grid ColumnSpacing="4">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
@@ -318,17 +307,13 @@
Click="ProgramPathSelectButton_Click"
Content="{ui:FontIcon Glyph=&#xE8DA;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="ProgramPathSelectButtonTooltip" />
</ToolTipService.ToolTip>
</Button>
Style="{StaticResource SubtleButtonStyle}" />
</Grid>
<TextBox
x:Name="ProgramArgsInput"
x:Uid="ProgramArgsInput"
GotFocus="ProgramArgsInput_GotFocus" />
<Grid ColumnSpacing="4">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
@@ -346,11 +331,7 @@
Click="StartInSelectButton_Click"
Content="{ui:FontIcon Glyph=&#xE8DA;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="StartInSelectButtonTooltip" />
</ToolTipService.ToolTip>
</Button>
Style="{StaticResource SubtleButtonStyle}" />
</Grid>
<ComboBox
x:Name="ElevationComboBox"
@@ -391,13 +372,6 @@
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,7 +7,6 @@ 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;
@@ -78,7 +77,6 @@ namespace KeyboardManagerEditorUI.Controls
OpenUrl,
OpenApp,
MouseClick,
Disable,
}
/// <summary>
@@ -131,7 +129,6 @@ namespace KeyboardManagerEditorUI.Controls
"OpenUrl" => ActionType.OpenUrl,
"OpenApp" => ActionType.OpenApp,
"MouseClick" => ActionType.MouseClick,
"Disable" => ActionType.Disable,
_ => ActionType.KeyOrShortcut,
};
}
@@ -151,17 +148,8 @@ namespace KeyboardManagerEditorUI.Controls
TriggerKeys.ItemsSource = _triggerKeys;
ActionKeys.ItemsSource = _actionKeys;
_triggerKeys.CollectionChanged += (_, _) =>
{
UpdatePlaceholderVisibility();
RaiseValidationStateChanged();
};
_actionKeys.CollectionChanged += (_, _) =>
{
UpdatePlaceholderVisibility();
RaiseValidationStateChanged();
};
_triggerKeys.CollectionChanged += (_, _) => RaiseValidationStateChanged();
_actionKeys.CollectionChanged += (_, _) => RaiseValidationStateChanged();
this.Unloaded += UnifiedMappingControl_Unloaded;
}
@@ -221,9 +209,6 @@ namespace KeyboardManagerEditorUI.Controls
ActionKeyToggleBtn.IsChecked = false;
}
// Disable dropdowns during recording
SetDropDownsEnabled(TriggerKeys, false);
KeyboardHookHelper.Instance.ActivateHook(this);
}
}
@@ -234,8 +219,6 @@ namespace KeyboardManagerEditorUI.Controls
{
CleanupKeyboardHook();
}
SetDropDownsEnabled(TriggerKeys, true);
}
#endregion
@@ -279,9 +262,6 @@ namespace KeyboardManagerEditorUI.Controls
TriggerKeyToggleBtn.IsChecked = false;
}
// Disable dropdowns during recording
SetDropDownsEnabled(ActionKeys, false);
KeyboardHookHelper.Instance.ActivateHook(this);
}
}
@@ -292,238 +272,6 @@ 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
@@ -705,7 +453,7 @@ namespace KeyboardManagerEditorUI.Controls
public void OnInputLimitReached()
{
ShowNotificationTip(ResourceHelper.GetString("Warning_InputLimitReached"));
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
}
#endregion
@@ -715,12 +463,12 @@ namespace KeyboardManagerEditorUI.Controls
/// <summary>
/// Gets the trigger keys.
/// </summary>
public List<string> GetTriggerKeys() => _triggerKeys.Where(k => !string.IsNullOrEmpty(k)).ToList();
public List<string> GetTriggerKeys() => _triggerKeys.ToList();
/// <summary>
/// Gets the action keys (for Key/Shortcut action type).
/// </summary>
public List<string> GetActionKeys() => _actionKeys.Where(k => !string.IsNullOrEmpty(k)).ToList();
public List<string> GetActionKeys() => _actionKeys.ToList();
/// <summary>
/// Gets the selected mouse trigger.
@@ -819,7 +567,6 @@ 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,
};
}
@@ -865,28 +612,18 @@ namespace KeyboardManagerEditorUI.Controls
/// </summary>
public void SetActionType(ActionType actionType)
{
if (ActionTypeComboBox == null)
int index = actionType switch
{
return;
}
string tag = actionType switch
{
ActionType.Text => "Text",
ActionType.OpenUrl => "OpenUrl",
ActionType.OpenApp => "OpenApp",
ActionType.Disable => "Disable",
ActionType.MouseClick => "MouseClick",
_ => "KeyOrShortcut",
ActionType.Text => 1,
ActionType.OpenUrl => 2,
ActionType.OpenApp => 3,
ActionType.MouseClick => 4,
_ => 0,
};
foreach (var item in ActionTypeComboBox.Items)
if (ActionTypeComboBox != null)
{
if (item is ComboBoxItem comboBoxItem && comboBoxItem.Tag is string itemTag && itemTag == tag)
{
ActionTypeComboBox.SelectedItem = comboBoxItem;
return;
}
ActionTypeComboBox.SelectedIndex = index;
}
}
@@ -1011,44 +748,11 @@ 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();
@@ -1206,7 +910,7 @@ namespace KeyboardManagerEditorUI.Controls
/// </summary>
public void ShowNotificationTip(string message)
{
ShowValidationMessage(ResourceHelper.GetString("Warning_Title"), message, InfoBarSeverity.Warning);
ShowValidationMessage("Warning", message, InfoBarSeverity.Warning);
}
/// <summary>
@@ -1228,7 +932,7 @@ namespace KeyboardManagerEditorUI.Controls
}
else
{
ShowValidationError(ResourceHelper.GetString("Error_UnknownValidation_Title"), ResourceHelper.GetString("Error_UnknownValidation_Message"));
ShowValidationError("Validation Error", "An unknown validation error occurred.");
}
}

View File

@@ -8,7 +8,6 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Interop;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Windows.System;
@@ -20,7 +19,7 @@ namespace KeyboardManagerEditorUI.Helpers
public static KeyboardHookHelper Instance => _instance ??= new KeyboardHookHelper();
private KeyboardMappingService? _mappingService;
private KeyboardMappingService _mappingService;
private HotkeySettingsControlHook? _keyboardHook;
@@ -35,14 +34,7 @@ namespace KeyboardManagerEditorUI.Helpers
// Singleton to make sure only one instance of the hook is active
private KeyboardHookHelper()
{
try
{
_mappingService = new KeyboardMappingService();
}
catch (Exception ex)
{
Logger.LogWarning($"Native KBM library unavailable for keyboard hook: {ex.Message}");
}
_mappingService = new KeyboardMappingService();
}
public void ActivateHook(IKeyboardHookTarget target)
@@ -54,18 +46,11 @@ namespace KeyboardManagerEditorUI.Helpers
_currentlyPressedKeys.Clear();
_keyPressOrder.Clear();
try
{
_keyboardHook = new HotkeySettingsControlHook(
KeyDown,
KeyUp,
() => true,
(key, extraInfo) => true);
}
catch (Exception ex)
{
Logger.LogWarning($"Keyboard hook unavailable: {ex.Message}");
}
_keyboardHook = new HotkeySettingsControlHook(
KeyDown,
KeyUp,
() => true,
(key, extraInfo) => true);
}
public void CleanupHook()
@@ -125,17 +110,6 @@ 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());
}
@@ -152,13 +126,7 @@ namespace KeyboardManagerEditorUI.Helpers
if (_currentlyPressedKeys.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);
}
_keyPressOrder.Remove(virtualKey);
_activeTarget.OnKeyUp(virtualKey, GetFormattedKeyList());
}
@@ -172,11 +140,6 @@ 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;
@@ -184,15 +147,9 @@ 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))
{
if (RemappingHelper.IsModifierKey(key) || !_activeTarget.AllowChords)
{
continue;
}
continue;
}
if (RemappingHelper.IsModifierKey(key))
@@ -232,11 +189,6 @@ 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

@@ -1,19 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.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

@@ -1,52 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.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,7 +19,6 @@ namespace KeyboardManagerEditorUI.Helpers
EmptyAppName,
IllegalShortcut,
DuplicateMapping,
ConflictingModifier,
SelfMapping,
EmptyTargetText,
EmptyUrl,

View File

@@ -16,18 +16,17 @@ namespace KeyboardManagerEditorUI.Helpers
{
public static readonly Dictionary<ValidationErrorType, (string Title, string Message)> ValidationMessages = new()
{
{ 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")) },
{ 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.") },
};
public static ValidationErrorType ValidateKeyMapping(
@@ -70,11 +69,6 @@ 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;
@@ -83,47 +77,6 @@ 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,
@@ -286,58 +239,6 @@ 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)));

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