mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-02 00:19:16 +02:00
Compare commits
41 Commits
dev/duhowe
...
autoUpgrad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df1d6ea7e0 | ||
|
|
31e30280de | ||
|
|
5074588c9a | ||
|
|
df7a41b457 | ||
|
|
b0b073f088 | ||
|
|
c56258d2d3 | ||
|
|
236c241107 | ||
|
|
d6f61c8439 | ||
|
|
b387bbffdd | ||
|
|
5003c6c758 | ||
|
|
b2ef4c85bc | ||
|
|
24d56f0524 | ||
|
|
0089de33bd | ||
|
|
3e2914a0b2 | ||
|
|
3554f0884b | ||
|
|
ad60090096 | ||
|
|
de6a609d16 | ||
|
|
243255ecea | ||
|
|
1e1bd07087 | ||
|
|
32b4080007 | ||
|
|
47c1fb5418 | ||
|
|
8ee3d64667 | ||
|
|
51c9bc4930 | ||
|
|
ddff66c088 | ||
|
|
e28ed8a566 | ||
|
|
4f693778f2 | ||
|
|
e1ad13ab34 | ||
|
|
cea0497bb9 | ||
|
|
fd5be6d04e | ||
|
|
4dfdf46e0d | ||
|
|
1c4ecc23c6 | ||
|
|
5888f6eb7f | ||
|
|
152f64151b | ||
|
|
76b773b016 | ||
|
|
0da5602f68 | ||
|
|
565094abbe | ||
|
|
1314f68602 | ||
|
|
fbad0dce9c | ||
|
|
36a5b77e6c | ||
|
|
4ce451edd0 | ||
|
|
42924e71c7 |
26
.github/actions/spell-check/allow/code.txt
vendored
26
.github/actions/spell-check/allow/code.txt
vendored
@@ -19,6 +19,7 @@ OLIVEGREEN
|
||||
PALEBLUE
|
||||
PArgb
|
||||
Pbgra
|
||||
SRGBTo
|
||||
WHITEONBLACK
|
||||
|
||||
|
||||
@@ -48,7 +49,6 @@ nupkg
|
||||
petabyte
|
||||
resw
|
||||
resx
|
||||
runtimeconfig
|
||||
srt
|
||||
Stereolithography
|
||||
terabyte
|
||||
@@ -332,6 +332,7 @@ REGSTR
|
||||
INVOKEIDLIST
|
||||
MEMORYSTATUSEX
|
||||
ABE
|
||||
Mdt
|
||||
HTCAPTION
|
||||
POSCHANGED
|
||||
QUERYPOS
|
||||
@@ -341,6 +342,29 @@ WINEVENTPROC
|
||||
WORKERW
|
||||
FULLSCREENAPP
|
||||
|
||||
# COM/WinRT interface prefixes and type fragments
|
||||
BAlt
|
||||
BShift
|
||||
Cmanifest
|
||||
Cmodule
|
||||
Cuuid
|
||||
Dng
|
||||
IApplication
|
||||
IDisposable
|
||||
IEnum
|
||||
IFolder
|
||||
IInitialize
|
||||
IMemory
|
||||
IOle
|
||||
ipreview
|
||||
IProperty
|
||||
IShell
|
||||
ithumbnail
|
||||
IVirtual
|
||||
|
||||
# Test frameworks
|
||||
MSTEST
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
FFF
|
||||
|
||||
2
.github/actions/spell-check/allow/names.txt
vendored
2
.github/actions/spell-check/allow/names.txt
vendored
@@ -178,7 +178,9 @@ Taras
|
||||
TBM
|
||||
Teutsch
|
||||
tilovell
|
||||
traies
|
||||
Triet
|
||||
udit
|
||||
urnotdfs
|
||||
vednig
|
||||
waaverecords
|
||||
|
||||
2
.github/actions/spell-check/excludes.txt
vendored
2
.github/actions/spell-check/excludes.txt
vendored
@@ -140,8 +140,6 @@
|
||||
^tools/project_template/ModuleTemplate/resource\.h$
|
||||
^tools/Verification scripts/Check preview handler registration\.ps1$
|
||||
ignore$
|
||||
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
|
||||
^src/common/CalculatorEngineCommon/exprtk\.hpp$
|
||||
src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage.cs
|
||||
^src/modules/powerrename/unittests/testdata/avif_test\.avif$
|
||||
^src/modules/powerrename/unittests/testdata/heif_test\.heic$
|
||||
|
||||
23
.github/actions/spell-check/expect.txt
vendored
23
.github/actions/spell-check/expect.txt
vendored
@@ -87,6 +87,7 @@ AUTOCHECKBOX
|
||||
AUTOHIDE
|
||||
AUTOHSCROLL
|
||||
AUTOMATIONPROPERTIES
|
||||
autopf
|
||||
AUTORADIOBUTTON
|
||||
Autorun
|
||||
AUTOTICKS
|
||||
@@ -241,6 +242,7 @@ CPower
|
||||
cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
createallsubdirs
|
||||
createdump
|
||||
CREATEPROCESS
|
||||
CREATESCHEDULEDTASK
|
||||
@@ -277,6 +279,7 @@ CYSMICON
|
||||
CYVIRTUALSCREEN
|
||||
Dac
|
||||
dacl
|
||||
DArchitectures
|
||||
datareader
|
||||
datatracker
|
||||
Dayof
|
||||
@@ -353,6 +356,7 @@ dlib
|
||||
dllhost
|
||||
dllmain
|
||||
Dmdo
|
||||
DMy
|
||||
DNLEN
|
||||
DONOTROUND
|
||||
DONTVALIDATEPATH
|
||||
@@ -472,6 +476,7 @@ FILEMUSTEXIST
|
||||
FILEOP
|
||||
FILEOPENDIALOGOPTIONS
|
||||
FILEOS
|
||||
filesandordirs
|
||||
FILESUBTYPE
|
||||
FILESYSPATH
|
||||
Filetime
|
||||
@@ -655,6 +660,7 @@ IEXPLORE
|
||||
IFACEMETHOD
|
||||
IFACEMETHODIMP
|
||||
IGNOREUNKNOWN
|
||||
ignoreversion
|
||||
IGo
|
||||
iid
|
||||
Iindex
|
||||
@@ -705,6 +711,8 @@ ipcmanager
|
||||
IPREVIEW
|
||||
irprops
|
||||
isbi
|
||||
ISCC
|
||||
isdl
|
||||
iss
|
||||
issecret
|
||||
ISSEPARATOR
|
||||
@@ -719,6 +727,7 @@ jobject
|
||||
JOBOBJECT
|
||||
jpe
|
||||
jpnime
|
||||
jrsoftware
|
||||
Jsons
|
||||
jsonval
|
||||
jxr
|
||||
@@ -939,6 +948,8 @@ muxc
|
||||
mvvm
|
||||
MVVMTK
|
||||
MWBEx
|
||||
mycompany
|
||||
myextension
|
||||
MYICON
|
||||
myorg
|
||||
myrepo
|
||||
@@ -1026,9 +1037,7 @@ NORMALDISPLAY
|
||||
NORMALUSER
|
||||
NOSEARCH
|
||||
NOSENDCHANGING
|
||||
NOSIZE
|
||||
notdefault
|
||||
Nosize
|
||||
NOTHOUSANDS
|
||||
NOTICKS
|
||||
NOTIFICATIONSDLL
|
||||
@@ -1260,6 +1269,7 @@ Quarternary
|
||||
QUERYENDSESSION
|
||||
QUERYOPEN
|
||||
QUEUESYNC
|
||||
quicklinks
|
||||
QUNS
|
||||
RAII
|
||||
RAlt
|
||||
@@ -1280,6 +1290,7 @@ recents
|
||||
RECTDESTINATION
|
||||
rectp
|
||||
RECTSOURCE
|
||||
recursesubdirs
|
||||
recyclebin
|
||||
Redist
|
||||
Reencode
|
||||
@@ -1540,6 +1551,7 @@ suntimes
|
||||
swp
|
||||
sug
|
||||
Superbar
|
||||
SUPPRESSMSGBOXES
|
||||
sut
|
||||
svchost
|
||||
SVGIn
|
||||
@@ -1570,6 +1582,7 @@ sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
systemroot
|
||||
SYSTEMTIME
|
||||
TARGETAPPHEADER
|
||||
targetentrypoint
|
||||
@@ -1663,6 +1676,7 @@ uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
UNICODETEXT
|
||||
uninsdeletekey
|
||||
uninstalls
|
||||
Uniquifies
|
||||
unitconverter
|
||||
@@ -1678,6 +1692,7 @@ UOI
|
||||
UPDATENOW
|
||||
updown
|
||||
UPGRADINGPRODUCTCODE
|
||||
upserts
|
||||
Uptool
|
||||
urld
|
||||
Usb
|
||||
@@ -1708,6 +1723,7 @@ VERIFYCONTEXT
|
||||
VERSIONINFO
|
||||
VERTRES
|
||||
VERTSIZE
|
||||
VERYSILENT
|
||||
VFT
|
||||
vget
|
||||
vgetq
|
||||
@@ -1761,7 +1777,6 @@ webpage
|
||||
websites
|
||||
wekyb
|
||||
wgpocpl
|
||||
WIC
|
||||
wic
|
||||
wifi
|
||||
winapi
|
||||
@@ -2159,7 +2174,7 @@ nodiscard
|
||||
nologo
|
||||
nomove
|
||||
nosize
|
||||
notopmost
|
||||
NOTOPMOST
|
||||
Notupdated
|
||||
notwindows
|
||||
nowarn
|
||||
|
||||
9
.github/actions/spell-check/patterns.txt
vendored
9
.github/actions/spell-check/patterns.txt
vendored
@@ -191,15 +191,6 @@ aka\.ms/[a-zA-Z0-9]+
|
||||
# #pragma lib
|
||||
^\s*#pragma comment\(lib, ".*?"\)
|
||||
|
||||
# UnitTests
|
||||
\[DataRow\(.*\)\]
|
||||
|
||||
# AdditionalDependencies
|
||||
<AdditionalDependencies>.*<
|
||||
|
||||
# the last line of mimetype="application/x-microsoft.net.object.bytearray.base64" things in .resx files
|
||||
^\s*[-a-zA-Z=;:/0-9+]*[-a-zA-Z;:/0-9+][-a-zA-Z=;:/0-9+]*=$
|
||||
|
||||
RegExp\(@?([`'"]).*?\g{-1}\)|(?:escapes|regEx):\s*(?:/.*/|([`'"]).*?\g{-1})|return/.*?/
|
||||
|
||||
# Questionably acceptable forms of `in to`
|
||||
|
||||
24
.github/policies/resourceManagement.yml
vendored
24
.github/policies/resourceManagement.yml
vendored
@@ -233,6 +233,30 @@ configuration:
|
||||
- addReply:
|
||||
reply: Hi! Thanks for making us aware of the problem. We raised the issue with our internal localization team. This issue should be fixed hopefully in the next version of PowerToys.
|
||||
description:
|
||||
- if:
|
||||
- payloadType: Issue_Comment
|
||||
- commentContains:
|
||||
pattern: '\/need-monitor-info'
|
||||
isRegex: True
|
||||
- hasLabel:
|
||||
label: Product-Cursor Wrap
|
||||
- or:
|
||||
- activitySenderHasAssociation:
|
||||
association: Owner
|
||||
- activitySenderHasAssociation:
|
||||
association: Member
|
||||
- activitySenderHasAssociation:
|
||||
association: Collaborator
|
||||
then:
|
||||
- removeLabel:
|
||||
label: Needs-Triage
|
||||
- removeLabel:
|
||||
label: Needs-Team-Response
|
||||
- addLabel:
|
||||
label: Needs-Author-Feedback
|
||||
- addReply:
|
||||
reply: "To help debug your layout, please run [this script](https://github.com/microsoft/PowerToys/blob/main/src/modules/MouseUtils/CursorWrap/CursorWrapTests/Capture-MonitorLayout.ps1) and attach the generated JSON output to this thread.\n\nThis allows us to better understand the issue and investigate potential fixes."
|
||||
description:
|
||||
- if:
|
||||
- payloadType: Issue_Comment
|
||||
- commentContains:
|
||||
|
||||
4
.github/workflows/msstore-submissions.yml
vendored
4
.github/workflows/msstore-submissions.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh)
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: azure/login@v2
|
||||
uses: azure/login@v3
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
- uses: microsoft/setup-msstore-cli@v1
|
||||
|
||||
- name: Fetch Store Credential
|
||||
uses: azure/cli@v2
|
||||
uses: azure/cli@v3
|
||||
with:
|
||||
azcliversion: latest
|
||||
inlineScript: |-
|
||||
|
||||
13
.github/workflows/spelling2.yml
vendored
13
.github/workflows/spelling2.yml
vendored
@@ -93,7 +93,7 @@ jobs:
|
||||
steps:
|
||||
- name: check-spelling
|
||||
id: spelling
|
||||
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
|
||||
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
|
||||
with:
|
||||
config: .github/actions/spell-check
|
||||
suppress_push_for_open_pull_request: ${{ github.actor != 'dependabot[bot]' && 1 }}
|
||||
@@ -135,6 +135,7 @@ jobs:
|
||||
cspell:cpp/compiler-msvc.txt
|
||||
cspell:python/common/extra.txt
|
||||
cspell:scala/scala.txt
|
||||
ignored: ignored-expect-variant
|
||||
|
||||
comment-push:
|
||||
name: Report (Push)
|
||||
@@ -147,10 +148,8 @@ jobs:
|
||||
if: (success() || failure()) && needs.spelling.outputs.followup && github.event_name == 'push'
|
||||
steps:
|
||||
- name: comment
|
||||
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
|
||||
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
|
||||
with:
|
||||
config: .github/actions/spell-check
|
||||
checkout: true
|
||||
spell_check_this: microsoft/PowerToys@main
|
||||
task: ${{ needs.spelling.outputs.followup }}
|
||||
|
||||
@@ -166,10 +165,8 @@ jobs:
|
||||
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
|
||||
steps:
|
||||
- name: comment
|
||||
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
|
||||
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
|
||||
with:
|
||||
config: .github/actions/spell-check
|
||||
checkout: true
|
||||
spell_check_this: check-spelling/spell-check-this@prerelease
|
||||
task: ${{ needs.spelling.outputs.followup }}
|
||||
experimental_apply_changes_via_bot: ${{ github.repository_owner != 'microsoft' && 1 }}
|
||||
@@ -193,7 +190,7 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: apply spelling updates
|
||||
uses: check-spelling/check-spelling@c635c2f3f714eec2fcf27b643a1919b9a811ef2e # v0.0.25
|
||||
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
|
||||
with:
|
||||
experimental_apply_changes_via_bot: ${{ github.repository_owner != 'microsoft' && 1 }}
|
||||
checkout: true
|
||||
|
||||
@@ -217,7 +217,11 @@
|
||||
"PowerToys.PowerAccentModuleInterface.dll",
|
||||
"PowerToys.PowerAccentKeyboardService.dll",
|
||||
|
||||
"PowerToys.PowerDisplayModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerDisplay.exe",
|
||||
"PowerDisplay.Lib.dll",
|
||||
"PowerDisplay.Models.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.PowerRenameExt.dll",
|
||||
"WinUI3Apps\\PowerToys.PowerRename.exe",
|
||||
|
||||
39
AGENTS.md
39
AGENTS.md
@@ -3,7 +3,7 @@ description: 'Top-level AI contributor guidance for developing PowerToys - a col
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
# PowerToys – AI Contributor Guide
|
||||
# PowerToys – AI contributor guide
|
||||
|
||||
This is the top-level guidance for AI contributions to PowerToys. Keep changes atomic, follow existing patterns, and cite exact paths in PRs.
|
||||
|
||||
@@ -26,13 +26,15 @@ For architecture details and module types, see [Architecture Overview](doc/devdo
|
||||
## Conventions
|
||||
|
||||
For detailed coding conventions, see:
|
||||
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md) – Dependencies, testing, PR management
|
||||
- [Coding Style](doc/devdocs/development/style.md) – Formatting, C++/C#/XAML style rules
|
||||
- [Logging](doc/devdocs/development/logging.md) – C++ spdlog and C# Logger usage
|
||||
|
||||
### Component-Specific Instructions
|
||||
### Component-specific instructions
|
||||
|
||||
These instruction files are automatically applied when working in their respective areas:
|
||||
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md) – IPC contracts, schema migrations
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md) – ABI stability, shared code guidelines
|
||||
|
||||
@@ -44,7 +46,7 @@ These instruction files are automatically applied when working in their respecti
|
||||
- Windows 10 1803+ (April 2018 Update or newer)
|
||||
- Initialize submodules once: `git submodule update --init --recursive`
|
||||
|
||||
### Build Commands
|
||||
### Build commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
@@ -52,7 +54,7 @@ These instruction files are automatically applied when working in their respecti
|
||||
| Build current folder | `tools\build\build.cmd` |
|
||||
| Build with options | `build.ps1 -Platform x64 -Configuration Release` |
|
||||
|
||||
### Build Discipline
|
||||
### Build discipline
|
||||
|
||||
1. One terminal per operation (build → test). Do not switch or open new ones mid-flow
|
||||
2. After making changes, `cd` to the project folder that changed (`.csproj`/`.vcxproj`)
|
||||
@@ -62,9 +64,10 @@ These instruction files are automatically applied when working in their respecti
|
||||
6. On failure, read the errors log: `build.<config>.<platform>.errors.log`
|
||||
7. Do not start tests or launch Runner until the build succeeds
|
||||
|
||||
### Build Logs
|
||||
### Build logs
|
||||
|
||||
Located next to the solution/project being built:
|
||||
|
||||
- `build.<configuration>.<platform>.errors.log` – errors only (check this first)
|
||||
- `build.<configuration>.<platform>.all.log` – full log
|
||||
- `build.<configuration>.<platform>.trace.binlog` – for MSBuild Structured Log Viewer
|
||||
@@ -73,18 +76,18 @@ For complete details, see [Build Guidelines](tools/build/BUILD-GUIDELINES.md).
|
||||
|
||||
## Tests
|
||||
|
||||
### Test Discovery
|
||||
### Test discovery
|
||||
|
||||
- Find test projects by product code prefix (e.g., `FancyZones`, `AdvancedPaste`)
|
||||
- Look for sibling folders or 1-2 levels up named `<Product>*UnitTests` or `<Product>*UITests`
|
||||
|
||||
### Running Tests
|
||||
### Running tests
|
||||
|
||||
1. **Build the test project first**, wait for exit code 0
|
||||
2. Run via VS Test Explorer (`Ctrl+E, T`) or `vstest.console.exe` with filters
|
||||
3. **Avoid `dotnet test`** in this repo – use VS Test Explorer or vstest.console.exe
|
||||
|
||||
### Test Types
|
||||
### Test types
|
||||
|
||||
| Type | Requirements | Setup |
|
||||
|------|--------------|-------|
|
||||
@@ -92,13 +95,13 @@ For complete details, see [Build Guidelines](tools/build/BUILD-GUIDELINES.md).
|
||||
| UI Tests | WinAppDriver v1.2.1, Developer Mode | Install from [WinAppDriver releases](https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1) |
|
||||
| Fuzz Tests | OneFuzz, .NET 8 | See [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md) |
|
||||
|
||||
### Test Discipline
|
||||
### Test discipline
|
||||
|
||||
1. Add or adjust tests when changing behavior
|
||||
2. If tests skipped, state why (e.g., comment-only change, string rename)
|
||||
3. New modules handling file I/O or user input **must** implement fuzzing tests
|
||||
|
||||
### Special Requirements
|
||||
### Special requirements
|
||||
|
||||
- **Mouse Without Borders**: Requires 2+ physical computers (not VMs)
|
||||
- **Multi-monitor utilities**: Test with 2+ monitors, different DPI settings
|
||||
@@ -107,14 +110,14 @@ For UI test setup details, see [UI Tests](doc/devdocs/development/ui-tests.md).
|
||||
|
||||
## Boundaries
|
||||
|
||||
### Ask for Clarification When
|
||||
### Ask for clarification when
|
||||
|
||||
- Ambiguous spec after scanning relevant docs
|
||||
- Cross-module impact (shared enum/struct) is unclear
|
||||
- Security, elevation, or installer changes involved
|
||||
- GPO or policy handling modifications needed
|
||||
|
||||
### Areas Requiring Extra Care
|
||||
### Areas requiring extra care
|
||||
|
||||
| Area | Concern | Reference |
|
||||
|------|---------|-----------|
|
||||
@@ -123,7 +126,7 @@ For UI test setup details, see [UI Tests](doc/devdocs/development/ui-tests.md).
|
||||
| Installer files | Release impact | Careful review required |
|
||||
| Elevation/GPO logic | Security | Confirm no regression in policy handling |
|
||||
|
||||
### What NOT to Do
|
||||
### What not to do
|
||||
|
||||
- Don't merge incomplete features into main (use feature branches)
|
||||
- Don't break IPC/JSON contracts without updating both runner and settings-ui
|
||||
@@ -143,23 +146,27 @@ Before finishing, verify:
|
||||
|
||||
## Documentation Index
|
||||
|
||||
### Core Architecture
|
||||
### Core architecture
|
||||
|
||||
- [Architecture Overview](doc/devdocs/core/architecture.md)
|
||||
- [Runner](doc/devdocs/core/runner.md)
|
||||
- [Settings System](doc/devdocs/core/settings/readme.md)
|
||||
- [Module Interface](doc/devdocs/modules/interface.md)
|
||||
|
||||
### Development
|
||||
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md)
|
||||
- [Coding Style](doc/devdocs/development/style.md)
|
||||
- [Logging](doc/devdocs/development/logging.md)
|
||||
- [UI Tests](doc/devdocs/development/ui-tests.md)
|
||||
- [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md)
|
||||
|
||||
### Build & Tools
|
||||
### Build & tools
|
||||
|
||||
- [Build Guidelines](tools/build/BUILD-GUIDELINES.md)
|
||||
- [Tools Overview](doc/devdocs/tools/readme.md)
|
||||
|
||||
### Instructions (Auto-Applied)
|
||||
### Instructions (auto-applied)
|
||||
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
|
||||
121
COMMUNITY.md
121
COMMUNITY.md
@@ -1,84 +1,109 @@
|
||||
# Community
|
||||
|
||||
The PowerToys team is extremely grateful to have the support of an amazing active community. The work you do is incredibly important. PowerToys 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 wouldn't be near what it is without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thanks and to recognize your work. This is a living document dedicated to highlighting the high impact community members and their contributions.
|
||||
|
||||
Names are in alphabetical order based on first name.
|
||||
Names are in alphabetical order, based on first name.
|
||||
|
||||
## High impact community members
|
||||
|
||||
### [@cgaarden](https://github.com/cgaarden) - [Christian Gaarden Gaardmark](https://www.onegreatworld.com)
|
||||
Christian contributed New+ utility
|
||||
### [@cgaarden](https://github.com/cgaarden) - [Christian Gaarden Gaardmark](https://www.onegreatworld.com)
|
||||
|
||||
Christian contributed the New+ utility
|
||||
|
||||
### [@CleanCodeDeveloper](https://github.com/CleanCodeDeveloper)
|
||||
|
||||
CleanCodeDeveloper helped do massive amounts of code stability and image resizer work.
|
||||
|
||||
### [@plante-msft](https://github.com/plante-msft) - Connor Plante
|
||||
|
||||
Connor was the creator of Workspaces and helped create Command Palette (PowerToys Run v2)
|
||||
|
||||
### [@damienleroy](https://github.com/damienleroy) - [Damien Leroy](https://www.linkedin.com/in/Damien-Leroy-b2734416a/)
|
||||
|
||||
Damien has helped out by developing and contributing the Quick Accent utility.
|
||||
|
||||
### [@daverayment](https://github.com/daverayment) - [David Rayment](https://www.linkedin.com/in/david-rayment-168b5251/)
|
||||
|
||||
Dave has helped improve the experience inside of Peek by adding in new features and fixing bugs.
|
||||
|
||||
### [@davidegiacometti](https://github.com/davidegiacometti) - [Davide Giacometti](https://www.linkedin.com/in/davidegiacometti/)
|
||||
|
||||
Davide has helped fix multiple bugs, added new utilities, features, as well as help us with the ARM64 effort by porting applications to .NET Core.
|
||||
|
||||
### [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang
|
||||
|
||||
Ethan helped run PowerToys and worked on improving and prototyping out next generation PowerToys
|
||||
|
||||
### [@franky920920](https://github.com/franky920920) - [Franky Chen](https://frankychen.net)
|
||||
|
||||
Franky has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes to PowerToys.
|
||||
|
||||
### [@htcfreek](https://github.com/htcfreek) - Heiko
|
||||
|
||||
Heiko has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes to PowerToys.
|
||||
|
||||
### [@Jay-o-Way](https://github.com/Jay-o-Way) - Jay
|
||||
|
||||
Jay has helped triaging, discussing, creating a substantial number of issues and PRs.
|
||||
|
||||
### [@jefflord](https://github.com/Jjefflord) - Jeff Lord
|
||||
Jeff added in multiple new features into Keyboard manager, such as key chord support and launching apps. He also contributed multiple features/fixes to PowerToys.
|
||||
|
||||
Jeff added multiple new features to Keyboard Manager, such as key chord support and launching apps. He also contributed multiple features/fixes to PowerToys.
|
||||
|
||||
### [@snickler](https://github.com/snickler) - [Jeremy Sinclair](http://sinclairinat0r.com)
|
||||
Jeremy has helped drive large sums of the ARM64 support inside PowerToys
|
||||
|
||||
Jeremy has helped drive substantial ARM64 support within PowerToys.
|
||||
|
||||
### [@jiripolasek](https://github.com/jiripolasek) - [Jiří Polášek](https://github.com/jiripolasek)
|
||||
|
||||
Jiří has contributed a massive number of features and improvements to Command Palette, including drag & drop support, custom themes, Web Search enhancements, Remote Desktop extension fixes, and many UX improvements.
|
||||
|
||||
### [@TheJoeFin](https://github.com/TheJoeFin) - [Joe Finney](https://joefinapps.com)
|
||||
Joe has helped triaging, discussing, issues as well as fixing bugs and building features for Text Extractor.
|
||||
|
||||
Joe has helped with triaging, discussing issues as well as fixing bugs and building features for Text Extractor.
|
||||
|
||||
### [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie
|
||||
|
||||
Jordi helped innovate amazing new features into Advanced Paste and helped create Command Palette (PowerToys Run v2)
|
||||
|
||||
|
||||
### [@jsoref](https://github.com/jsoref) - [Josh Soref](https://check-spelling.dev/)
|
||||
|
||||
Helping keep our spelling correct :)
|
||||
|
||||
### [@martinchrzan](https://github.com/martinchrzan/) - Martin Chrzan
|
||||
|
||||
Color Picker is from Martin.
|
||||
|
||||
### [@mikeclayton](https://github.com/mikeclayton) - [Michael Clayton](https://michael-clayton.com)
|
||||
|
||||
Michael contributed the [initial version](https://github.com/microsoft/PowerToys/issues/23216) of the Mouse Jump tool and [a number of updates](https://github.com/microsoft/PowerToys/pulls?q=is%3Apr+author%3Amikeclayton) based on his FancyMouse utility.
|
||||
|
||||
### [@Noraa-Junker](https://github.com/Noraa-Junker) - [Noraa Junker](https://noraajunker.ch)
|
||||
|
||||
Noraa has helped triaging, discussing, and creating a substantial number of issues and contributed features/fixes. Noraa was the primary person for helping build the File Explorer preview pane handler for developer files.
|
||||
|
||||
### [@pedrolamas](https://github.com/pedrolamas/) - Pedro Lamas
|
||||
Pedro helped create the thumbnail and File Explorer previewers for 3D files like STL and GCode. If you like 3D printing, these are very helpful.
|
||||
|
||||
Pedro helped create the thumbnail and File Explorer previewers for 3D files like STL and GCode. If you like 3D printing, these are very helpful.
|
||||
|
||||
### [@PesBandi](https://github.com/PesBandi/) - PesBandi
|
||||
|
||||
PesBandi has helped do massive amounts of Quick Accent and bug fixes.
|
||||
|
||||
### [@riverar](https://github.com/riverar) - [Rafael Rivera](https://withinrafael.com/)
|
||||
Rafael has helped do the [upgrade from CppWinRT 1.x to 2.0](https://github.com/microsoft/PowerToys/issues/1907). He directly provided feedback to the CppWinRT team for bugs from this migration as well.
|
||||
|
||||
Rafael has helped do the [upgrade from CppWinRT 1.x to 2.0](https://github.com/microsoft/PowerToys/issues/1907). He directly provided feedback to the CppWinRT team for bugs from this migration as well.
|
||||
|
||||
### [@royvou](https://github.com/royvou)
|
||||
|
||||
Roy has helped out contributing multiple features to PowerToys Run
|
||||
|
||||
### [@ThiefZero](https://github.com/ThiefZero)
|
||||
ThiefZero has helped out contributing a features to PowerToys Run such as the unit converter plugin
|
||||
|
||||
ThiefZero has helped contribute features to PowerToys Run, such as the unit converter plugin
|
||||
|
||||
### [@TobiasSekan](https://github.com/TobiasSekan) - Tobias Sekan
|
||||
|
||||
Tobias Sekan has helped out contributing features to PowerToys Run such as Settings plugin, Registry plugin
|
||||
|
||||
## Open source projects
|
||||
@@ -94,7 +119,8 @@ Their fork of Wox was the base of PowerToys Run.
|
||||
Initial base of jjw24's fork, which makes it the base of PowerToys Run.
|
||||
|
||||
### [Text-Grab](https://github.com/TheJoeFin/Text-Grab) - Joseph Finney
|
||||
Joe helped develop and contribute to the Text Extractor utility. It is directly based on his Text Grab application.
|
||||
|
||||
Joe helped develop and contribute to the Text Extractor utility. It is directly based on his Text Grab application.
|
||||
|
||||
## Microsoft community members
|
||||
|
||||
@@ -102,7 +128,7 @@ We would like to also directly call out some extremely helpful Microsoft employe
|
||||
|
||||
### [@betsegaw](https://github.com/betsegaw/) - [Betsegaw Tadele](http://www.dreamsofameaningfullife.com/)
|
||||
|
||||
Window Walker, inside PowerToys Run, is from Beta.
|
||||
Window Walker, inside PowerToys Run, is from Beta.
|
||||
|
||||
### [@TheMrJukes](https://github.com/TheMrJukes/) - Bret Anderson
|
||||
|
||||
@@ -125,6 +151,7 @@ PowerToys Awake is a tool to keep your computer awake.
|
||||
Randy contributed Registry Preview and some very early conversations about keyboard remapping.
|
||||
|
||||
### [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon
|
||||
|
||||
Kayla was a former lead for PowerToys and helped create multiple utilities, maintained the GitHub repo, and collaborated with the community to improve the overall product
|
||||
|
||||
### [@oldnewthing](https://github.com/oldnewthing) - Raymond Chen
|
||||
@@ -135,46 +162,48 @@ Find My Mouse is based on Raymond Chen's SuperSonar.
|
||||
|
||||
Crop And Lock is based on the original work of Robert Mikhayelyan, with Program Manager support from [@kevinguo305](https://github.com/kevinguo305) - Kevin Guo.
|
||||
|
||||
ZoomIt's Video Recording Session code is based on Robert Mikhayelyan's https://github.com/robmikh/capturevideosample code.
|
||||
ZoomIt's Video Recording Session code is based on Robert Mikhayelyan's <https://github.com/robmikh/capturevideosample> code.
|
||||
|
||||
### Microsoft InVEST team
|
||||
|
||||
This amazing team helped PowerToys develop PowerToys Run and Keyboard manager as well as update our Settings to v2. @alekhyareddy28, @arjunbalgovind, @jyuwono @laviusmotileng-ms, @ryanbodrug-microsoft, @saahmedm, @somil55, @traies, @udit3333
|
||||
This amazing team helped PowerToys develop PowerToys Run and Keyboard manager as well as update our Settings to v2. @alekhyareddy28, @arjunbalgovind, @jyuwono @laviusmotileng-ms, @ryanbodrug-microsoft, @saahmedm, @somil55, @traies, @udit3333
|
||||
|
||||
## Mouse Without Borders original contributors
|
||||
*Project creator: Truong Do (Đỗ Đức Trường)*
|
||||
|
||||
Project creator: Truong Do (Đỗ Đức Trường)
|
||||
|
||||
Other contributors:
|
||||
* Microsoft Garage: Quinn Hawkins, Michael Low, Joe Coplen, Nino Yuniardi, Gwyneth Marshall, David Andrews, Karen Luecking
|
||||
* Peter Hauge - Visual Studio
|
||||
* Bruce Dawson - Windows Fundamentals
|
||||
* Alan Myrvold - Office Security
|
||||
* Adrian Garside - WEX
|
||||
* Scott Bradner - Surface
|
||||
* Aleks Gershaft - Windows Azure
|
||||
* Chinh Huynh - Windows Azure
|
||||
* Long Nguyen - Data Center
|
||||
* Triet Le - Cloud Engineering
|
||||
* Luke Schoen - Excel
|
||||
* Bao Nguyen - Bing
|
||||
* Ross Nichols - Windows
|
||||
* Ryan Baltazar - Windows
|
||||
* Ed Essey - The Garage
|
||||
* Mario Madden - The Garage
|
||||
* Karthick Mahalingam - ACE
|
||||
* Pooja Kamra - ACE
|
||||
* Justin White - SA
|
||||
* Chris Ransom - SA
|
||||
* Mike Ricks - Red Team
|
||||
* Randy Santossio - Surface
|
||||
* Ashish Sen Jaswal - Device Health
|
||||
* Zoltan Harmath - Security Tools
|
||||
* Luciano Krigun - Security Products
|
||||
* Jo Hemmerlein - Red Team
|
||||
* Chris Johnson - Surface Hub
|
||||
* Loren Ponten - Surface Hub
|
||||
* Paul Schmitt - WWL
|
||||
* And many other Users!
|
||||
|
||||
- Microsoft Garage: Quinn Hawkins, Michael Low, Joe Coplen, Nino Yuniardi, Gwyneth Marshall, David Andrews, Karen Luecking
|
||||
- Peter Hauge - Visual Studio
|
||||
- Bruce Dawson - Windows Fundamentals
|
||||
- Alan Myrvold - Office Security
|
||||
- Adrian Garside - WEX
|
||||
- Scott Bradner - Surface
|
||||
- Aleks Gershaft - Windows Azure
|
||||
- Chinh Huynh - Windows Azure
|
||||
- Long Nguyen - Data Center
|
||||
- Triet Le - Cloud Engineering
|
||||
- Luke Schoen - Excel
|
||||
- Bao Nguyen - Bing
|
||||
- Ross Nichols - Windows
|
||||
- Ryan Baltazar - Windows
|
||||
- Ed Essey - The Garage
|
||||
- Mario Madden - The Garage
|
||||
- Karthick Mahalingam - ACE
|
||||
- Pooja Kamra - ACE
|
||||
- Justin White - SA
|
||||
- Chris Ransom - SA
|
||||
- Mike Ricks - Red Team
|
||||
- Randy Santossio - Surface
|
||||
- Ashish Sen Jaswal - Device Health
|
||||
- Zoltan Harmath - Security Tools
|
||||
- Luciano Krigun - Security Products
|
||||
- Jo Hemmerlein - Red Team
|
||||
- Chris Johnson - Surface Hub
|
||||
- Loren Ponten - Surface Hub
|
||||
- Paul Schmitt - WWL
|
||||
- And many other Users!
|
||||
|
||||
## ZoomIt original contributors
|
||||
|
||||
|
||||
@@ -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 everyone's time:
|
||||
|
||||
> 👉 If you have a question, think you've discovered an issue, or would like to propose a new feature, please find/file an issue **BEFORE** starting work to fix/implement it.
|
||||
|
||||
When requesting new features or enhancements, providing additional evidence, data, tweets, blog posts, or research is extremely helpful. This information gives context to the scenario that may otherwise be lost.
|
||||
|
||||
* Unsure whether 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 it's an issue or feature request? File an issue.
|
||||
- Have a question that isn't answered in the docs, videos, etc.? File an issue.
|
||||
- Want to know if we're planning a particular feature? File an issue.
|
||||
- Got a great idea for a new utility or feature? File an issue/request/idea.
|
||||
- Don't understand how to do something? File an issue/Community Guidance Request.
|
||||
- Found an existing issue that describes yours? Great! Upvote and add additional commentary, info, or repro steps.
|
||||
|
||||
A quick search before filing an issue could be helpful. 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. It's likely someone else has found the same problem, and they may even be working on or have already contributed a fix!
|
||||
|
||||
### Indicating Interest in Issues
|
||||
### Indicating interest in issues
|
||||
|
||||
To let the team know which issues are important, upvote by clicking the [+😊] button and the 👍 icon on the original issue post. Avoid comments like "+1" or "me too" as they clutter the discussion and make it harder to prioritize requests.
|
||||
|
||||
---
|
||||
|
||||
## Contributing Fixes/Features
|
||||
## Contributing fixes or features
|
||||
|
||||
Please comment on our ["Would you like to contribute to PowerToys?"](https://github.com/microsoft/PowerToys/issues/28769) thread to let us know you're interested in working on something before you start. This helps avoid multiple people unexpectedly working on the same thing and ensures everyone is clear on what should be done. It's less work for everyone to establish this up front.
|
||||
Please comment on our [Would you like to contribute to PowerToys?](https://github.com/microsoft/PowerToys/issues/28769) thread to let us know you're interested in working on something before you start. This helps avoid multiple people unexpectedly working on the same thing and ensures everyone is clear on what should be done. It's less work for everyone to establish this up front.
|
||||
|
||||
### Localization Issues
|
||||
### Localization issues
|
||||
|
||||
For localization issues, please file an issue to notify our internal localization team, as community PRs for localization aren't accepted. Localization is handled exclusively by the internal Microsoft team.
|
||||
|
||||
### To Spec or Not to Spec
|
||||
### To spec or not to spec
|
||||
|
||||
A key point is for everyone to understand the approach that will be taken. We want to be sure that any work done will be accepted. Larger-scope items will require a spec to outline the approach and allow for discussion. Specs help collaborators consider different solutions, describe feature behavior, and plan for errors. Achieving agreement in a spec before writing code often results in simpler code and less wasted effort.
|
||||
|
||||
Once a team member has agreed with your approach, proceed to the "Development" section below. Team members are happy to help review specs and guide them to completion.
|
||||
|
||||
### Help Wanted
|
||||
### Help wanted
|
||||
|
||||
Once the team has approved an issue/spec approach, development can proceed. If no developers are immediately available, the spec may be parked and labeled "Help Wanted," ready for a developer to get started. For development opportunities, visit [Issues labeled Help Wanted](https://github.com/microsoft/PowerToys/labels/Help%20Wanted).
|
||||
|
||||
@@ -55,18 +55,18 @@ Once the team has approved an issue/spec approach, development can proceed. If n
|
||||
|
||||
Follow the [development guidelines](https://github.com/microsoft/PowerToys/blob/main/doc/devdocs/readme.md).
|
||||
|
||||
### Naming Features and Functionality
|
||||
### Naming features and functionality
|
||||
|
||||
Names should be descriptive and straightforward, clearly reflecting functionality and usefulness.
|
||||
|
||||
### Becoming a Collaborator on the PowerToys Team
|
||||
### Becoming a collaborator on the PowerToys team
|
||||
|
||||
Be an active community member! Make helpful contributions by filing bugs, offering suggestions, developing fixes and features, conducting code reviews, and updating documentation.
|
||||
Be an active community member! Make helpful contributions by filing bugs, offering suggestions, developing fixes and features, conducting code reviews, and updating documentation.
|
||||
|
||||
When the time comes, Microsoft will reach out to you about becoming a formal team member. Just make sure they have a way to contact you. 😊
|
||||
|
||||
---
|
||||
|
||||
## Thank You
|
||||
## Thank you
|
||||
|
||||
Thank you in advance for your contribution! We appreciate your help in making PowerToys a better tool for everyone.
|
||||
|
||||
1710
DATA_AND_PRIVACY.md
1710
DATA_AND_PRIVACY.md
File diff suppressed because it is too large
Load Diff
@@ -18,15 +18,15 @@
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.12" />
|
||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Collections" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Collections" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Primitives" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
@@ -75,7 +75,7 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.231216.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260209005" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.260203002" />
|
||||
|
||||
87
NOTICE.md
87
NOTICE.md
@@ -17,7 +17,7 @@ This software incorporates material from third parties.
|
||||
|
||||
### Martin Chrzan's Color Picker
|
||||
|
||||
**Source**: https://github.com/martinchrzan/ColorPicker
|
||||
**Source**: <https://github.com/martinchrzan/ColorPicker>
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -49,7 +49,7 @@ We use the WyHash NuGet package for calculating stable hashes for strings.
|
||||
|
||||
**Source**: [https://github.com/wangyi-fudan/wyhash](https://github.com/wangyi-fudan/wyhash)
|
||||
|
||||
```
|
||||
```text
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
@@ -82,7 +82,7 @@ We use the ToolGood.Words.Pinyin NuGet package for converting Chinese characters
|
||||
|
||||
**Source**: [https://github.com/toolgood/ToolGood.Words.Pinyin](https://github.com/toolgood/ToolGood.Words.Pinyin)
|
||||
|
||||
```
|
||||
```text
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 ToolGood
|
||||
@@ -106,8 +106,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
|
||||
## Utility: Command Palette Built-in Extensions
|
||||
## Utility: Command palette built-in extensions
|
||||
|
||||
### Calculator
|
||||
|
||||
@@ -117,7 +116,7 @@ We use the exprtk library (exprtk.hpp) to evaluate mathematical expressions.
|
||||
|
||||
**Source**: [https://github.com/ArashPartow/exprtk](https://github.com/ArashPartow/exprtk)
|
||||
|
||||
```
|
||||
```text
|
||||
MIT License
|
||||
|
||||
Copyright (c) 1999-2024 Arash Partow
|
||||
@@ -144,7 +143,7 @@ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
```
|
||||
|
||||
## Utility: PowerToys Run Built-in Extensions
|
||||
## Utility: PowerToys Run built-in extensions
|
||||
|
||||
### Calculator
|
||||
|
||||
@@ -154,7 +153,7 @@ We use the Mages NuGet package for calculating the result of expression.
|
||||
|
||||
**Source**: [https://github.com/FlorianRappl/Mages](https://github.com/FlorianRappl/Mages)
|
||||
|
||||
```
|
||||
```text
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 - 2025 Florian Rappl
|
||||
@@ -178,13 +177,13 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## Utility: File Explorer Add-ins
|
||||
## Utility: File Explorer add-ins
|
||||
|
||||
### Monaco Editor
|
||||
|
||||
**Source**: https://github.com/Microsoft/monaco-editor
|
||||
**Source**: <https://github.com/Microsoft/monaco-editor>
|
||||
|
||||
**Additional third party notifications:** https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt
|
||||
**Additional third party notifications:** <https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt>
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -208,9 +207,9 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
### The Quite OK Image Format reference decoder
|
||||
### The Quite OK image format reference decoder
|
||||
|
||||
**Source**: https://github.com/phoboslab/qoi
|
||||
**Source**: <https://github.com/phoboslab/qoi>
|
||||
|
||||
**Note**: [@pedrolamas](https://github.com/pedrolamas) translated and adapted the reference decoder code to C# that is in PowerToys from the original C++ implementation.
|
||||
|
||||
@@ -240,9 +239,9 @@ SOFTWARE.
|
||||
|
||||
We use the UTF.Unknown NuGet package for detecting encoding in text/code files.
|
||||
|
||||
**Source**: https://github.com/CharsetDetector/UTF-unknown
|
||||
**Source**: <https://github.com/CharsetDetector/UTF-unknown>
|
||||
|
||||
```
|
||||
```text
|
||||
MOZILLA PUBLIC LICENSE
|
||||
Version 1.1
|
||||
|
||||
@@ -716,9 +715,9 @@ EXHIBIT A -Mozilla Public License.
|
||||
|
||||
## Utility: ImageResizer
|
||||
|
||||
### Brice Lams's Image Resizer License
|
||||
### Brice Lams's Image Resizer license
|
||||
|
||||
**Source**: https://github.com/bricelam/ImageResizer/
|
||||
**Source**: <https://github.com/bricelam/ImageResizer/>
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -744,10 +743,10 @@ THE SOFTWARE.
|
||||
|
||||
## Utility: PowerToys Run
|
||||
|
||||
### Wox License
|
||||
### Wox license
|
||||
|
||||
**Fork project source**: https://github.com/jjw24/Wox/
|
||||
**Base project source**: https://github.com/Wox-launcher/Wox
|
||||
**Fork project source**: <https://github.com/jjw24/Wox/>
|
||||
**Base project source**: <https://github.com/Wox-launcher/Wox>
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -770,9 +769,9 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
### Beta Tadele's Window Walker License
|
||||
### Beta Tadele's Window Walker license
|
||||
|
||||
**Source**: https://github.com/betsegaw/windowwalker
|
||||
**Source**: <https://github.com/betsegaw/windowwalker>
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -786,9 +785,9 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
## Utility: PowerRename
|
||||
|
||||
### Chris Davis's SmartRename License
|
||||
### Chris Davis's SmartRename license
|
||||
|
||||
**Source**: https://github.com/chrdavis/SmartRename
|
||||
**Source**: <https://github.com/chrdavis/SmartRename>
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -816,7 +815,7 @@ SOFTWARE.
|
||||
|
||||
### spdlog
|
||||
|
||||
**Source**: https://github.com/gabime/spdlog
|
||||
**Source**: <https://github.com/gabime/spdlog>
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -841,12 +840,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-- NOTE: Third party dependency used by this software --
|
||||
This software depends on the fmt lib (MIT License),
|
||||
and users must comply to its license: https://github.com/fmtlib/fmt/blob/master/LICENSE.rst
|
||||
This software depends on the fmt lib (MIT License), and users must comply to its license:
|
||||
<https://github.com/fmtlib/fmt/blob/master/LICENSE.rst>
|
||||
|
||||
### expected-lite
|
||||
|
||||
**Source**: https://github.com/martinmoene/expected-lite
|
||||
**Source**: <https://github.com/martinmoene/expected-lite>
|
||||
|
||||
Boost Software License - Version 1.0 - August 17th, 2003
|
||||
|
||||
@@ -874,7 +873,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
### zip
|
||||
|
||||
**Source**: https://github.com/kuba--/zip
|
||||
**Source**: <https://github.com/kuba--/zip>
|
||||
|
||||
All Rights Reserved.
|
||||
|
||||
@@ -902,7 +901,7 @@ THE SOFTWARE.
|
||||
|
||||
We adopted some functions from it.
|
||||
|
||||
**Source**: https://github.com/DLTcollab/sse2neon
|
||||
**Source**: <https://github.com/DLTcollab/sse2neon>
|
||||
|
||||
sse2neon is freely redistributable under the MIT License.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
@@ -925,9 +924,9 @@ SOFTWARE.
|
||||
|
||||
### Monaco Editor
|
||||
|
||||
**Source**: https://github.com/Microsoft/monaco-editor
|
||||
**Source**: <https://github.com/Microsoft/monaco-editor>
|
||||
|
||||
**Additional third party notifications:** https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt
|
||||
**Additional third party notifications:** <https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt>
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -951,11 +950,11 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
### The Quite OK Image Format reference decoder
|
||||
### The Quite OK image format reference decoder
|
||||
|
||||
**Source**: https://github.com/phoboslab/qoi
|
||||
**Source**: <https://github.com/phoboslab/qoi>
|
||||
|
||||
**Note**: [@pedrolamas](https://github.com/pedrolamas) translated and adapted the reference decoder code to C# that is in PowerToys from the original C++ implementation.
|
||||
**Note**: [@pedrolamas](https://github.com/pedrolamas) translated and adapted the reference decoder code to C# that is in PowerToys, from the original C++ implementation.
|
||||
|
||||
MIT License
|
||||
|
||||
@@ -979,13 +978,13 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
### UTF Unknown
|
||||
### UTF unknown
|
||||
|
||||
We use the UTF.Unknown NuGet package for detecting encoding in text/code files.
|
||||
|
||||
**Source**: https://github.com/CharsetDetector/UTF-unknown
|
||||
**Source**: <https://github.com/CharsetDetector/UTF-unknown>
|
||||
|
||||
```
|
||||
```text
|
||||
MOZILLA PUBLIC LICENSE
|
||||
Version 1.1
|
||||
|
||||
@@ -1463,9 +1462,9 @@ EXHIBIT A -Mozilla Public License.
|
||||
|
||||
We use HexBox.WinUI to show a preview of binary values.
|
||||
|
||||
**Source**: https://github.com/hotkidfamily/HexBox.WinUI
|
||||
**Source**: <https://github.com/hotkidfamily/HexBox.WinUI>
|
||||
|
||||
```
|
||||
```text
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Filip Jeremic
|
||||
@@ -1492,11 +1491,11 @@ SOFTWARE.
|
||||
|
||||
### Monaco Editor
|
||||
|
||||
**Source**: https://github.com/Microsoft/monaco-editor
|
||||
**Source**: <https://github.com/Microsoft/monaco-editor>
|
||||
|
||||
**Additional third party notifications:** https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt
|
||||
**Additional third party notifications:** <https://github.com/microsoft/monaco-editor/blob/main/ThirdPartyNotices.txt>
|
||||
|
||||
```
|
||||
```text
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 - present Microsoft Corporation
|
||||
@@ -1526,7 +1525,7 @@ SOFTWARE.
|
||||
|
||||
PowerDisplay's DDC/CI implementation references techniques from Twinkle Tray.
|
||||
|
||||
**Source**: https://github.com/xanderfrangos/twinkle-tray
|
||||
**Source**: <https://github.com/xanderfrangos/twinkle-tray>
|
||||
|
||||
MIT License
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
|
||||
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
|
||||
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
|
||||
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
|
||||
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
|
||||
</Folder>
|
||||
<Folder Name="/common/interop/">
|
||||
@@ -709,17 +710,19 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Models/PowerDisplay.Models.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib/PowerDisplay.Lib.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<!-- TEMPORARILY_DISABLED: PowerDisplay
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay/PowerDisplay.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplayModuleInterface/PowerDisplayModuleInterface.vcxproj" Id="d1234567-8901-2345-6789-abcdef012345" />
|
||||
-->
|
||||
</Folder>
|
||||
<Folder Name="/modules/PowerDisplay/Tests/">
|
||||
<Project Path="src/modules/powerdisplay/PowerDisplay.Lib.UnitTests/PowerDisplay.Lib.UnitTests.csproj">
|
||||
|
||||
70
README.md
70
README.md
@@ -19,14 +19,13 @@
|
||||
<span> · </span>
|
||||
<a href="#-whats-new">Release notes</a>
|
||||
</h3>
|
||||
<br/><br/>
|
||||
|
||||
## 🔨 Utilities
|
||||
|
||||
PowerToys includes over 25 utilities to help you customize and optimize your Windows experience:
|
||||
PowerToys includes over 30 utilities to help you customize and optimize your Windows experience:
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| [<img src="doc/images/icons/AdvancedPaste.png" alt="Advanced Paste icon" height="16"> Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [<img src="doc/images/icons/Always%20On%20Top.png" alt="Always on Top icon" height="16"> Always on Top](https://aka.ms/PowerToysOverview_AoT) | [<img src="doc/images/icons/Awake.png" alt="Awake icon" height="16"> Awake](https://aka.ms/PowerToysOverview_Awake) |
|
||||
| [<img src="doc/images/icons/Color%20Picker.png" alt="Color Picker icon" height="16"> Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [<img src="doc/images/icons/Command%20Not%20Found.png" alt="Command Not Found icon" height="16"> Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [<img src="doc/images/icons/Command Palette.png" alt="Command Palette icon" height="16"> Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
|
||||
| [<img src="doc/images/icons/Crop%20And%20Lock.png" alt="Crop and Lock icon" height="16"> Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [<img src="doc/images/icons/Environment%20Manager.png" alt="Environment Variables icon" height="16"> Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [<img src="doc/images/icons/FancyZones.png" alt="FancyZones icon" height="16"> FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
|
||||
@@ -38,28 +37,27 @@ PowerToys includes over 25 utilities to help you customize and optimize your Win
|
||||
| [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
|
||||
| [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) | | |
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
## 📋 Installation
|
||||
|
||||
For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
|
||||
For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
|
||||
|
||||
But to get started quickly, choose one of the installation methods below:
|
||||
<br/><br/>
|
||||
<details open>
|
||||
<summary><strong>Download .exe from GitHub</strong></summary>
|
||||
<summary><strong>Download the .exe file from GitHub</strong></summary>
|
||||
<br/>
|
||||
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
Go to the [PowerToys GitHub releases](https://aka.ms/installPowerToys), select **Assets** to reveal the installation files, and choose the one that matches your architecture and install scope. For most devices, that would be _x64 per-user_.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.99%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.98%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
|
||||
| Description | Filename |
|
||||
| --- | --- |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.98.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.98.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.98.1-x64.exe][ptMachineX64] |
|
||||
@@ -83,14 +81,16 @@ You can easily install PowerToys from the Microsoft Store:
|
||||
<details>
|
||||
<summary><strong>WinGet</strong></summary>
|
||||
<br/>
|
||||
Download PowerToys from <a href="https://github.com/microsoft/winget-cli#installing-the-client">WinGet</a>. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
Download PowerToys from [WinGet](https://github.com/microsoft/winget-cli#installing-the-client). Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
- User scope installer (default)
|
||||
|
||||
*User scope installer [default]*
|
||||
```powershell
|
||||
winget install Microsoft.PowerToys -s winget
|
||||
```
|
||||
|
||||
*Machine-wide scope installer*
|
||||
- Machine-wide scope installer
|
||||
|
||||
```powershell
|
||||
winget install --scope machine Microsoft.PowerToys -s winget
|
||||
```
|
||||
@@ -99,7 +99,7 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
<details>
|
||||
<summary><strong>Other methods</strong></summary>
|
||||
<br/>
|
||||
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
There are [community driven install methods](https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
## ✨ What's new?
|
||||
@@ -108,28 +108,26 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
|
||||
|
||||
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.1).
|
||||
|
||||
## 🛣️ Roadmap
|
||||
## 🛣️ Roadmap
|
||||
|
||||
We are planning some nice new features and improvements for the next releases – PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.99][github-next-release-work]!
|
||||
|
||||
## ❤️ PowerToys Community
|
||||
## ❤️ PowerToys Community
|
||||
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
|
||||
|
||||
## Contributing
|
||||
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so. For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
|
||||
## Contributing
|
||||
|
||||
## Code of Conduct
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
|
||||
This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows. We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort. Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so. For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
|
||||
|
||||
## Privacy Statement
|
||||
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
|
||||
## Code of conduct
|
||||
|
||||
[oss-CLA]: https://cla.opensource.microsoft.com
|
||||
[oss-conduct-code]: CODE_OF_CONDUCT.md
|
||||
[community-link]: COMMUNITY.md
|
||||
[github-release-link]: https://aka.ms/installPowerToys
|
||||
[microsoft-store-link]: https://aka.ms/getPowertoys
|
||||
[winget-link]: https://github.com/microsoft/winget-cli#installing-the-client
|
||||
[roadmap]: https://github.com/microsoft/PowerToys/wiki/Roadmap
|
||||
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
|
||||
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
|
||||
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct][oss-conduct-code].
|
||||
|
||||
## Privacy statement
|
||||
|
||||
The application logs basic diagnostic data (telemetry). For more privacy information and what we collect, see our [PowerToys Data and Privacy documentation](https://aka.ms/powertoys-data-and-privacy-documentation).
|
||||
|
||||
[oss-CLA]: https://cla.opensource.microsoft.com
|
||||
[oss-conduct-code]: CODE_OF_CONDUCT.md
|
||||
[community-link]: COMMUNITY.md
|
||||
|
||||
24
SECURITY.md
24
SECURITY.md
@@ -1,36 +1,36 @@
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
|
||||
|
||||
## Security
|
||||
# Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
## Reporting security issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
|
||||
If you prefer to submit without logging in, send an email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
- Full paths of source file(s) related to the manifestation of the issue
|
||||
- The location of the affected source code (tag/branch/commit or direct URL)
|
||||
- Any special configuration required to reproduce the issue
|
||||
- Step-by-step instructions to reproduce the issue
|
||||
- Proof-of-concept or exploit code (if possible)
|
||||
- Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
## Preferred languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
|
||||
17
SUPPORT.md
17
SUPPORT.md
@@ -1,24 +1,21 @@
|
||||
# Support
|
||||
|
||||
## How to use Microsoft PowerToys
|
||||
|
||||
## How to use Microsoft PowerToys
|
||||
|
||||
For more info on [PowerToys overviews and how to use the utilities][usingPowerToys-docs-link], or any other tools and resources for [Windows development environments](https://learn.microsoft.com/windows/dev-environment/overview), head over to [learn.microsoft.com][usingPowerToys-docs-link]!
|
||||
For more information about PowerToys overviews, how to use the utilities, and other tools and resources for [Windows development environments](https://learn.microsoft.com/windows/dev-environment/overview), visit [learn.microsoft.com][usingPowerToys-docs-link].
|
||||
|
||||
## How to file issues and get help
|
||||
|
||||
This project uses [GitHub Issues][gh-issue] to [track bugs][gh-bug] and [feature requests][gh-feature]. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or
|
||||
feature request as a new Issue.
|
||||
This project uses [GitHub Issues][gh-issue] to [track bugs][gh-bug] and [feature requests][gh-feature]. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue.
|
||||
|
||||
For help and questions about using this project, please look at our Wiki for using PowerToys and our [Contributor's Guide][contributor] if you want to work on PowerToys.
|
||||
For help and questions about using this project, please visit our documentation and [Contributor's Guide][contributor] if you want to contribute to PowerToys.
|
||||
|
||||
## Microsoft Support Policy
|
||||
## Microsoft support policy
|
||||
|
||||
Support for PowerToys is limited to the resources listed above.
|
||||
|
||||
[gh-issue]: https://github.com/microsoft/PowerToys/issues/new/choose
|
||||
[gh-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=Issue-Bug&template=bug_report.md&title=
|
||||
[gh-feature]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=feature_request.md&title=
|
||||
[wiki]: https://github.com/microsoft/PowerToys/wiki
|
||||
[gh-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=Issue-Bug&template=bug_report.md
|
||||
[gh-feature]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=feature_request.md
|
||||
[contributor]: https://github.com/microsoft/PowerToys/blob/main/CONTRIBUTING.md
|
||||
[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
|
||||
|
||||
@@ -1594,6 +1594,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.PowerRename.exe",
|
||||
L"PowerToys.ImageResizer.exe",
|
||||
L"PowerToys.LightSwitchService.exe",
|
||||
L"PowerToys.PowerDisplay.exe",
|
||||
L"PowerToys.GcodeThumbnailProvider.exe",
|
||||
L"PowerToys.BgcodeThumbnailProvider.exe",
|
||||
L"PowerToys.PdfThumbnailProvider.exe",
|
||||
|
||||
@@ -47,6 +47,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
call move /Y ..\..\..\NewPlus.wxs.bk ..\..\..\NewPlus.wxs
|
||||
call move /Y ..\..\..\Peek.wxs.bk ..\..\..\Peek.wxs
|
||||
call move /Y ..\..\..\PowerRename.wxs.bk ..\..\..\PowerRename.wxs
|
||||
call move /Y ..\..\..\PowerDisplay.wxs.bk ..\..\..\PowerDisplay.wxs
|
||||
call move /Y ..\..\..\Product.wxs.bk ..\..\..\Product.wxs
|
||||
call move /Y ..\..\..\RegistryPreview.wxs.bk ..\..\..\RegistryPreview.wxs
|
||||
call move /Y ..\..\..\Resources.wxs.bk ..\..\..\Resources.wxs
|
||||
@@ -123,6 +124,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
|
||||
<Compile Include="KeyboardManager.wxs" />
|
||||
<Compile Include="Peek.wxs" />
|
||||
<Compile Include="PowerRename.wxs" />
|
||||
<Compile Include="PowerDisplay.wxs" />
|
||||
<Compile Include="DscResources.wxs" />
|
||||
<Compile Include="RegistryPreview.wxs" />
|
||||
<Compile Include="Run.wxs" />
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
<ComponentGroupRef Id="LightSwitchComponentGroup" />
|
||||
<ComponentGroupRef Id="PeekComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerRenameComponentGroup" />
|
||||
<ComponentGroupRef Id="PowerDisplayComponentGroup" />
|
||||
<ComponentGroupRef Id="RegistryPreviewComponentGroup" />
|
||||
<ComponentGroupRef Id="RunComponentGroup" />
|
||||
<ComponentGroupRef Id="SettingsComponentGroup" />
|
||||
|
||||
@@ -212,6 +212,10 @@ Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRo
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\"
|
||||
Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs
|
||||
|
||||
#PowerDisplay
|
||||
Generate-FileList -fileDepsJson "" -fileListName PowerDisplayAssetsFiles -wxsFilePath $PSScriptRoot\PowerDisplay.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerDisplay\"
|
||||
Generate-FileComponents -fileListName "PowerDisplayAssetsFiles" -wxsFilePath $PSScriptRoot\PowerDisplay.wxs
|
||||
|
||||
#RegistryPreview
|
||||
Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\"
|
||||
Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <common/updating/updating.h>
|
||||
#include <common/updating/updateState.h>
|
||||
#include <common/updating/installer.h>
|
||||
#include <common/updating/configBackup.h>
|
||||
#include <common/updating/updateLifecycle.h>
|
||||
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/utils/HttpClient.h>
|
||||
@@ -21,6 +23,8 @@
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/timeutil.h>
|
||||
|
||||
#include <wil/resource.h>
|
||||
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
@@ -38,15 +42,16 @@ namespace fs = std::filesystem;
|
||||
|
||||
std::optional<fs::path> CopySelfToTempDir()
|
||||
{
|
||||
// D5 fix: Use unique temp path with PID to avoid collision on concurrent updates
|
||||
std::error_code error;
|
||||
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
|
||||
auto dst_path = fs::temp_directory_path() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
|
||||
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
|
||||
if (error)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::move(dst_path);
|
||||
return dst_path;
|
||||
}
|
||||
|
||||
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
@@ -57,34 +62,9 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
|
||||
auto state = UpdateState::read();
|
||||
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
Logger::error("Invoked with -update_now argument, but no update was available");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
|
||||
{
|
||||
if (!new_version_info)
|
||||
{
|
||||
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
}
|
||||
|
||||
return downloaded_installer;
|
||||
}
|
||||
else if (state.state == UpdateState::readyToInstall)
|
||||
// Handle readyToInstall first — the installer is already on disk,
|
||||
// so we don't need a GitHub API call (which may fail if offline).
|
||||
if (state.state == UpdateState::readyToInstall)
|
||||
{
|
||||
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
|
||||
if (fs::is_regular_file(installer))
|
||||
@@ -97,12 +77,44 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
else if (state.state == UpdateState::upToDate)
|
||||
|
||||
if (state.state == UpdateState::upToDate)
|
||||
{
|
||||
isUpToDate = true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto new_version_info = std::move(get_github_version_info_async()).get();
|
||||
|
||||
// Check for error BEFORE dereferencing — the old code crashed here
|
||||
// when GitHub API was unreachable (new_version_info held an error string).
|
||||
if (!new_version_info)
|
||||
{
|
||||
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (std::holds_alternative<version_up_to_date>(*new_version_info))
|
||||
{
|
||||
isUpToDate = true;
|
||||
Logger::error("Invoked with -update_now argument, but no update was available");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
|
||||
{
|
||||
// Cleanup old updates before downloading the latest
|
||||
updating::cleanup_updates();
|
||||
|
||||
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
|
||||
if (!downloaded_installer)
|
||||
{
|
||||
Logger::error("Couldn't download new installer");
|
||||
}
|
||||
|
||||
return downloaded_installer;
|
||||
}
|
||||
|
||||
Logger::error("Invoked with -update_now argument, but update state was invalid");
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -116,13 +128,29 @@ bool InstallNewVersionStage1(fs::path installer)
|
||||
|
||||
if (pt_main_window != nullptr)
|
||||
{
|
||||
// Get the process that owns the tray window so we can wait for it to exit
|
||||
DWORD ptProcessId = 0;
|
||||
GetWindowThreadProcessId(pt_main_window, &ptProcessId);
|
||||
|
||||
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
|
||||
|
||||
// D4 fix: Wait for PT to actually exit before launching installer.
|
||||
// Without this, the installer may find PT files locked.
|
||||
if (ptProcessId != 0)
|
||||
{
|
||||
wil::unique_handle ptProcess{ OpenProcess(SYNCHRONIZE, FALSE, ptProcessId) };
|
||||
if (ptProcess)
|
||||
{
|
||||
WaitForSingleObject(ptProcess.get(), 10000); // 10 second timeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
|
||||
arguments += L" \"";
|
||||
arguments += installer.c_str();
|
||||
arguments += L"\"";
|
||||
// Pass the install directory so Stage 2 can relaunch PowerToys after install
|
||||
const std::wstring installDir = get_module_folderpath();
|
||||
|
||||
std::wstring arguments = updating::BuildStage2Arguments(
|
||||
UPDATE_NOW_LAUNCH_STAGE2, installer, fs::path(installDir));
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
|
||||
sei.lpFile = copy_in_temp->c_str();
|
||||
@@ -190,9 +218,16 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
|
||||
if (!args || nArgs < 2)
|
||||
{
|
||||
if (args)
|
||||
{
|
||||
LocalFree(args);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// D3 fix: ensure args is freed on all exit paths
|
||||
auto freeArgs = wil::scope_exit([&] { LocalFree(args); });
|
||||
|
||||
std::wstring_view action{ args[1] };
|
||||
|
||||
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
|
||||
@@ -201,6 +236,10 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
|
||||
if (action == UPDATE_NOW_LAUNCH_STAGE1)
|
||||
{
|
||||
// Backup config files before the update to protect against corruption
|
||||
Logger::info("Backing up config files before update");
|
||||
updating::BackupConfigFiles(fs::path(PTSettingsHelper::get_root_save_folder_location()));
|
||||
|
||||
bool isUpToDate = false;
|
||||
auto installerPath = ObtainInstaller(isUpToDate);
|
||||
bool failed = !installerPath.has_value();
|
||||
@@ -217,6 +256,12 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
}
|
||||
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
|
||||
{
|
||||
if (nArgs < 3)
|
||||
{
|
||||
Logger::error("Stage 2 invoked without installer path argument");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
const bool failed = !InstallNewVersionStage2(args[2]);
|
||||
if (failed)
|
||||
@@ -227,6 +272,37 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
state.state = UpdateState::errorDownloading;
|
||||
});
|
||||
}
|
||||
|
||||
// D7 fix: Always check for corrupted configs after Stage 2, regardless
|
||||
// of install success/failure. A failed install may still corrupt configs.
|
||||
Logger::info("Checking for corrupted config files after update");
|
||||
updating::RestoreCorruptedConfigs(fs::path(PTSettingsHelper::get_root_save_folder_location()));
|
||||
|
||||
if (!failed)
|
||||
{
|
||||
// Relaunch PowerToys from the install directory
|
||||
if (updating::CanRelaunchAfterUpdate(nArgs))
|
||||
{
|
||||
std::wstring ptExePath = updating::BuildPowerToysExePath(args[3]);
|
||||
|
||||
Logger::info(L"Relaunching PowerToys after update: {}", ptExePath);
|
||||
|
||||
SHELLEXECUTEINFOW sei{ sizeof(sei) };
|
||||
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
|
||||
sei.lpFile = ptExePath.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
sei.lpParameters = UPDATE_REPORT_SUCCESS;
|
||||
|
||||
if (!ShellExecuteExW(&sei))
|
||||
{
|
||||
Logger::error(L"Failed to relaunch PowerToys after update");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn("Install directory not provided to Stage 2 - cannot relaunch PowerToys");
|
||||
}
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
|
||||
679
src/common/updating/UnitTests/UpdatingTests.cpp
Normal file
679
src/common/updating/UnitTests/UpdatingTests.cpp
Normal file
@@ -0,0 +1,679 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <common/updating/configBackup.h>
|
||||
#include <common/updating/updateLifecycle.h>
|
||||
|
||||
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace UpdatingUnitTests
|
||||
{
|
||||
// Helper to create a temp directory for test isolation.
|
||||
// Each instance gets a unique subdirectory to prevent test interference.
|
||||
class TempDir
|
||||
{
|
||||
public:
|
||||
TempDir()
|
||||
{
|
||||
wchar_t tempPath[MAX_PATH + 1];
|
||||
GetTempPathW(MAX_PATH, tempPath);
|
||||
static std::atomic<int> counter{0};
|
||||
m_path = fs::path(tempPath) / (L"PowerToysUpdateTests_" + std::to_wstring(counter++));
|
||||
|
||||
// Ensure clean state
|
||||
std::error_code ec;
|
||||
fs::remove_all(m_path, ec);
|
||||
fs::create_directories(m_path, ec);
|
||||
}
|
||||
|
||||
~TempDir()
|
||||
{
|
||||
std::error_code ec;
|
||||
fs::remove_all(m_path, ec);
|
||||
}
|
||||
|
||||
const fs::path& path() const { return m_path; }
|
||||
|
||||
// Write a file with the given content
|
||||
void WriteFile(const fs::path& relativePath, const std::string& content)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
fs::create_directories(fullPath.parent_path());
|
||||
std::ofstream file(fullPath, std::ios::binary);
|
||||
file.write(content.data(), content.size());
|
||||
}
|
||||
|
||||
// Write a file with raw bytes (including null bytes for corruption testing)
|
||||
void WriteFileBytes(const fs::path& relativePath, const std::vector<char>& bytes)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
fs::create_directories(fullPath.parent_path());
|
||||
std::ofstream file(fullPath, std::ios::binary);
|
||||
file.write(bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
// Read file content as string
|
||||
std::string ReadFile(const fs::path& relativePath)
|
||||
{
|
||||
auto fullPath = m_path / relativePath;
|
||||
std::ifstream file(fullPath, std::ios::binary);
|
||||
return std::string(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
bool FileExists(const fs::path& relativePath)
|
||||
{
|
||||
return fs::exists(m_path / relativePath);
|
||||
}
|
||||
|
||||
private:
|
||||
fs::path m_path;
|
||||
};
|
||||
|
||||
TEST_CLASS(IsJsonFileCorruptedTests)
|
||||
{
|
||||
public:
|
||||
// Tests IsJsonFileCorrupted: valid JSON with no null bytes returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — happy path, full file scan.
|
||||
TEST_METHOD(CleanJsonFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark","startup":true})");
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: zero-length file returns false (empty is not corrupted).
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — file.read returns 0 bytes immediately.
|
||||
TEST_METHOD(EmptyFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"empty.json", "");
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"empty.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file containing embedded null bytes returns true.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — null byte detection within buffer.
|
||||
TEST_METHOD(FileWithNullBytesIsCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::vector<char> corrupted = { '{', '"', 'a', '"', ':', '\0', '\0', '\0', '}' };
|
||||
dir.WriteFileBytes(L"corrupted.json", corrupted);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"corrupted.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file entirely filled with 0x00 bytes returns true.
|
||||
// Reproduces the exact bug from #46179 where installer zeroed out JSON files.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — first byte is null.
|
||||
TEST_METHOD(FileFilledWithNullBytesIsCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::vector<char> allNulls(1024, '\0');
|
||||
dir.WriteFileBytes(L"workspaces.json", allNulls);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"workspaces.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: path that does not exist returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — file.is_open() check.
|
||||
TEST_METHOD(NonExistentFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"missing.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: file larger than the 4096-byte read chunk
|
||||
// with no null bytes returns false.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — multi-chunk while loop.
|
||||
TEST_METHOD(LargeCleanFileIsNotCorrupted)
|
||||
{
|
||||
TempDir dir;
|
||||
std::string largeContent(8192, 'x');
|
||||
dir.WriteFile(L"large.json", largeContent);
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"large.json"));
|
||||
}
|
||||
|
||||
// Tests IsJsonFileCorrupted: null byte placed after the first 4096-byte
|
||||
// chunk boundary is still detected.
|
||||
// Covers: configBackup.h IsJsonFileCorrupted — second chunk scan.
|
||||
TEST_METHOD(NullByteAtEndOfLargeFileIsDetected)
|
||||
{
|
||||
TempDir dir;
|
||||
std::string content(5000, 'x');
|
||||
content[4999] = '\0';
|
||||
std::vector<char> bytes(content.begin(), content.end());
|
||||
dir.WriteFileBytes(L"sneaky.json", bytes);
|
||||
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"sneaky.json"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(BackupConfigFilesTests)
|
||||
{
|
||||
public:
|
||||
// Tests BackupConfigFiles: root-level .json files are copied to ConfigBackup.
|
||||
// Covers: configBackup.h BackupConfigFiles — root directory_iterator,
|
||||
// is_regular_file && extension == ".json" branch.
|
||||
// Setup: Two root-level JSON files.
|
||||
TEST_METHOD(BackupCopiesRootJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"UpdateState.json", R"({"state":0})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\UpdateState.json"));
|
||||
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: .json files inside module subdirectories are
|
||||
// copied to ConfigBackup/<module>/.
|
||||
// Covers: configBackup.h BackupConfigFiles — is_directory branch,
|
||||
// module directory_iterator with extension filter.
|
||||
// Setup: Root JSON + two module directories with JSON files.
|
||||
TEST_METHOD(BackupCopiesModuleJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[]})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
|
||||
Assert::AreEqual(std::string(R"({"zones":[]})"),
|
||||
dir.ReadFile(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: non-.json files at root level are not copied.
|
||||
// Covers: configBackup.h BackupConfigFiles — extension filter excludes .log.
|
||||
// Setup: One JSON file + one .log file at root.
|
||||
TEST_METHOD(BackupSkipsNonJsonFiles)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"debug.log", "log data");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\debug.log"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: the "Updates" directory is explicitly skipped.
|
||||
// Covers: configBackup.h BackupConfigFiles — dirName == L"Updates" continue.
|
||||
// Setup: Root JSON + Updates directory containing a file.
|
||||
TEST_METHOD(BackupSkipsUpdatesDirectory)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
dir.WriteFile(L"Updates\\installer.exe", "fake exe");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\Updates"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: running backup twice overwrites the previous
|
||||
// backup with current file content.
|
||||
// Covers: configBackup.h BackupConfigFiles — fs::remove_all(backupDir) +
|
||||
// copy_options::overwrite_existing.
|
||||
// Setup: Backup, modify original, backup again.
|
||||
TEST_METHOD(BackupOverwritesPreviousBackup)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"version":1})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Update the original
|
||||
dir.WriteFile(L"settings.json", R"({"version":2})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"ConfigBackup\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: non-.json files inside module subdirectories
|
||||
// (e.g., FancyZones/zones.dat) should NOT be backed up.
|
||||
// Covers: configBackup.h BackupConfigFiles — extension filter in module loop.
|
||||
TEST_METHOD(BackupSkipsNonJsonFilesInModuleDirs)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[]})");
|
||||
dir.WriteFile(L"FancyZones\\zones.dat", "binary data");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\FancyZones\\zones.dat"));
|
||||
}
|
||||
|
||||
// Tests BackupConfigFiles: empty root directory with no files produces
|
||||
// an empty ConfigBackup dir without errors.
|
||||
// Covers: configBackup.h BackupConfigFiles — empty directory_iterator.
|
||||
TEST_METHOD(BackupEmptyRootDirSucceeds)
|
||||
{
|
||||
TempDir dir;
|
||||
// Root dir exists but has no files
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup"));
|
||||
}
|
||||
};
|
||||
|
||||
TEST_CLASS(RestoreCorruptedConfigsTests)
|
||||
{
|
||||
public:
|
||||
// Tests RestoreCorruptedConfigs: corrupted root-level JSON file is restored
|
||||
// from the good backup copy.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — root file restore branch,
|
||||
// fs::exists + IsJsonFileCorrupted + backup integrity check.
|
||||
// Setup: Good file -> backup -> corrupt original -> restore.
|
||||
TEST_METHOD(RestoreFixesCorruptedRootFile)
|
||||
{
|
||||
TempDir dir;
|
||||
const std::string goodContent = R"({"theme":"dark"})";
|
||||
dir.WriteFile(L"settings.json", goodContent);
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt the original
|
||||
std::vector<char> corrupted(goodContent.size(), '\0');
|
||||
dir.WriteFileBytes(L"settings.json", corrupted);
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
Assert::AreEqual(goodContent, dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: corrupted module-level JSON file is restored
|
||||
// from the good backup copy.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — module directory branch,
|
||||
// moduleBackupEntry restore with integrity check.
|
||||
// Setup: Module file + root file -> backup -> corrupt module file -> restore.
|
||||
TEST_METHOD(RestoreFixesCorruptedModuleFile)
|
||||
{
|
||||
TempDir dir;
|
||||
const std::string goodContent = R"({"workspaces":[]})";
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", goodContent);
|
||||
dir.WriteFile(L"settings.json", R"({})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt the module file
|
||||
std::vector<char> corrupted(goodContent.size(), '\0');
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", corrupted);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::AreEqual(goodContent, dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: clean (non-corrupted) files are NOT
|
||||
// overwritten by backup — preserves user changes made after backup.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — IsJsonFileCorrupted
|
||||
// returns false, copy_file is skipped.
|
||||
// Setup: File -> backup -> modify (but keep valid) -> restore.
|
||||
TEST_METHOD(RestoreLeavesCleanFilesUntouched)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"version":1})");
|
||||
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Modify original (but keep it clean JSON)
|
||||
dir.WriteFile(L"settings.json", R"({"version":2})");
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Should NOT have been restored since it's not corrupted
|
||||
Assert::AreEqual(std::string(R"({"version":2})"), dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when no ConfigBackup directory exists,
|
||||
// restore silently does nothing (no crash, no data loss).
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — !fs::exists(backupDir)
|
||||
// early return.
|
||||
// Setup: File with no prior backup.
|
||||
TEST_METHOD(RestoreHandlesMissingBackupDirectory)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
|
||||
// No backup was created - restore should silently do nothing
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
Assert::AreEqual(std::string(R"({"theme":"dark"})"), dir.ReadFile(L"settings.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: end-to-end scenario with multiple modules,
|
||||
// some corrupted and some clean, verifying selective restore.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — both root and module
|
||||
// branches, selective restore based on corruption status.
|
||||
// Setup: 4 modules -> backup -> corrupt 2 -> restore -> verify all 4.
|
||||
TEST_METHOD(FullBackupAndRestoreRoundTrip)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// Set up a realistic config structure
|
||||
dir.WriteFile(L"settings.json", R"({"startup":true,"theme":"dark"})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json", R"({"zones":[{"id":1}]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json", R"({"workspaces":[{"name":"dev"}]})");
|
||||
dir.WriteFile(L"KeyboardManager\\default.json", R"({"remaps":[]})");
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt some files (simulating #46179 scenario)
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(100, '\0'));
|
||||
dir.WriteFileBytes(L"settings.json", std::vector<char>(50, '\0'));
|
||||
// Leave FancyZones and KBM clean
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Corrupted files should be restored
|
||||
Assert::AreEqual(std::string(R"({"startup":true,"theme":"dark"})"), dir.ReadFile(L"settings.json"));
|
||||
Assert::AreEqual(std::string(R"({"workspaces":[{"name":"dev"}]})"), dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
|
||||
// Clean files should be unchanged
|
||||
Assert::AreEqual(std::string(R"({"zones":[{"id":1}]})"), dir.ReadFile(L"FancyZones\\settings.json"));
|
||||
Assert::AreEqual(std::string(R"({"remaps":[]})"), dir.ReadFile(L"KeyboardManager\\default.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when the original file has been deleted
|
||||
// (not corrupted), restore should NOT recreate it from backup. The installer
|
||||
// may have intentionally removed obsolete config files.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — fs::exists guard.
|
||||
TEST_METHOD(RestoreSkipsDeletedOriginals)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"obsolete.json", R"({"old":true})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Installer deletes the file
|
||||
std::error_code ec;
|
||||
fs::remove(dir.path() / L"obsolete.json", ec);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Should NOT be recreated
|
||||
Assert::IsFalse(dir.FileExists(L"obsolete.json"));
|
||||
}
|
||||
|
||||
// Tests RestoreCorruptedConfigs: when the backup file itself is corrupted
|
||||
// (e.g., disk error during backup), restore should NOT copy corrupted
|
||||
// backup over the original — that would make things worse.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — backup integrity check (B2 fix).
|
||||
TEST_METHOD(RestoreSkipsCorruptedBackup)
|
||||
{
|
||||
TempDir dir;
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark"})");
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Corrupt BOTH the original AND the backup
|
||||
std::vector<char> nulls(50, '\0');
|
||||
dir.WriteFileBytes(L"settings.json", nulls);
|
||||
dir.WriteFileBytes(L"ConfigBackup\\settings.json", nulls);
|
||||
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Original should still be corrupted — we don't restore from bad backup
|
||||
Assert::IsTrue(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
}
|
||||
};
|
||||
|
||||
// Simulates what actually happens during a PowerToys upgrade:
|
||||
// 1. User has settings from normal use
|
||||
// 2. Updater backs up before install (Stage 1)
|
||||
// 3. Installer runs and corrupts some files (simulated)
|
||||
// 4. Updater restores corrupted files (Stage 2)
|
||||
// 5. PT relaunches and finds working configs
|
||||
TEST_CLASS(UpgradeSimulationTests)
|
||||
{
|
||||
public:
|
||||
// Tests full upgrade simulation: backup -> installer corrupts files -> restore.
|
||||
// Verifies that corrupted files are restored and clean files are untouched.
|
||||
// Covers: configBackup.h BackupConfigFiles + RestoreCorruptedConfigs —
|
||||
// end-to-end with 5 modules, 2 corrupted, 3 clean.
|
||||
// Setup: Realistic config structure with multiple modules.
|
||||
TEST_METHOD(SimulateUpgradeWithCorruption)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// === User's real config state before upgrade ===
|
||||
dir.WriteFile(L"settings.json",
|
||||
R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})");
|
||||
dir.WriteFile(L"FancyZones\\settings.json",
|
||||
R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})");
|
||||
dir.WriteFile(L"Workspaces\\workspaces.json",
|
||||
R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})");
|
||||
dir.WriteFile(L"KeyboardManager\\default.json",
|
||||
R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})");
|
||||
dir.WriteFile(L"MouseWithoutBorders\\settings.json",
|
||||
R"({"machineKey":"abc123","connectToAll":true})");
|
||||
|
||||
// Non-JSON files that should be left alone
|
||||
dir.WriteFile(L"update.log", "2026-04-11 update started");
|
||||
|
||||
// === Stage 1: Backup before killing PT ===
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// Verify backup was created correctly
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\FancyZones\\settings.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\Workspaces\\workspaces.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\KeyboardManager\\default.json"));
|
||||
Assert::IsTrue(dir.FileExists(L"ConfigBackup\\MouseWithoutBorders\\settings.json"));
|
||||
Assert::IsFalse(dir.FileExists(L"ConfigBackup\\update.log"));
|
||||
|
||||
// === Installer runs: some files get corrupted (the #46179 scenario) ===
|
||||
// Workspaces JSON filled with null bytes
|
||||
dir.WriteFileBytes(L"Workspaces\\workspaces.json", std::vector<char>(512, '\0'));
|
||||
// Main settings partially corrupted (null bytes injected)
|
||||
std::vector<char> partialCorrupt = { '{', '"', 's', '\0', '\0', '\0', '\0', '}' };
|
||||
dir.WriteFileBytes(L"settings.json", partialCorrupt);
|
||||
|
||||
// FancyZones, KBM, and MWB survive the install fine
|
||||
// (this is realistic - not all files get corrupted)
|
||||
|
||||
// === Stage 2: Restore after install completes ===
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// === Verify: PT relaunches and finds working configs ===
|
||||
|
||||
// Corrupted files should be restored from backup
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"settings.json"));
|
||||
Assert::IsFalse(updating::IsJsonFileCorrupted(dir.path() / L"Workspaces\\workspaces.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"startup":true,"theme":"dark","run_elevated":false,"download_updates_automatically":true})"),
|
||||
dir.ReadFile(L"settings.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"workspaces":[{"name":"dev","apps":["code","terminal"]}]})"),
|
||||
dir.ReadFile(L"Workspaces\\workspaces.json"));
|
||||
|
||||
// Clean files should be untouched (not overwritten with backup)
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"zones":[{"id":1,"rect":{"x":0,"y":0,"w":960,"h":1080}}]})"),
|
||||
dir.ReadFile(L"FancyZones\\settings.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"remapKeys":{"inProcess":[{"original":"0x41","new":"0x42"}]}})"),
|
||||
dir.ReadFile(L"KeyboardManager\\default.json"));
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"machineKey":"abc123","connectToAll":true})"),
|
||||
dir.ReadFile(L"MouseWithoutBorders\\settings.json"));
|
||||
}
|
||||
|
||||
// Tests upgrade from an old version that has fewer modules than the new version.
|
||||
// Verifies that new module configs (created by the installer) are not touched
|
||||
// by restore, while corrupted old configs are restored.
|
||||
// Covers: configBackup.h RestoreCorruptedConfigs — module dir in root that
|
||||
// has no corresponding backup entry.
|
||||
// Setup: Old version with 1 module -> backup -> new installer adds module -> corrupt old -> restore.
|
||||
TEST_METHOD(SimulateUpgradeFromVeryOldVersion)
|
||||
{
|
||||
TempDir dir;
|
||||
|
||||
// Old version had fewer modules - only settings.json
|
||||
dir.WriteFile(L"settings.json", R"({"theme":"dark","powertoys_version":"v0.60.0"})");
|
||||
|
||||
// Backup
|
||||
updating::BackupConfigFiles(dir.path());
|
||||
|
||||
// New installer creates new module dirs that didn't exist before
|
||||
dir.WriteFile(L"NewModule\\settings.json", R"({"enabled":true})");
|
||||
|
||||
// Old settings get corrupted during upgrade
|
||||
dir.WriteFileBytes(L"settings.json", std::vector<char>(100, '\0'));
|
||||
|
||||
// Restore
|
||||
updating::RestoreCorruptedConfigs(dir.path());
|
||||
|
||||
// Old settings restored
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"theme":"dark","powertoys_version":"v0.60.0"})"),
|
||||
dir.ReadFile(L"settings.json"));
|
||||
|
||||
// New module settings untouched (no backup existed for them)
|
||||
Assert::AreEqual(
|
||||
std::string(R"({"enabled":true})"),
|
||||
dir.ReadFile(L"NewModule\\settings.json"));
|
||||
}
|
||||
};
|
||||
|
||||
// Tests for the update lifecycle: argument passing between Stage 1 and Stage 2,
|
||||
// relaunch path construction, and the handoff that was broken in #42004/#43011/#44071.
|
||||
TEST_CLASS(UpdateLifecycleTests)
|
||||
{
|
||||
public:
|
||||
// Tests BuildStage2Arguments: output contains the stage 2 flag, installer path,
|
||||
// and install directory — all three components needed for Stage 2.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — concatenation logic.
|
||||
// Setup: Typical paths with spaces (Program Files).
|
||||
TEST_METHOD(BuildStage2ArgumentsContainsInstallerAndInstallDir)
|
||||
{
|
||||
const auto args = updating::BuildStage2Arguments(
|
||||
L"-update_now_stage_2",
|
||||
L"C:\\Users\\test\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-x64.exe",
|
||||
L"C:\\Program Files\\PowerToys");
|
||||
|
||||
// Must contain the stage 2 flag
|
||||
Assert::IsTrue(args.find(L"-update_now_stage_2") != std::wstring::npos);
|
||||
// Must contain the installer path (quoted)
|
||||
Assert::IsTrue(args.find(L"powertoyssetup-x64.exe") != std::wstring::npos);
|
||||
// Must contain the install directory (quoted) — this was MISSING before our fix
|
||||
Assert::IsTrue(args.find(L"C:\\Program Files\\PowerToys") != std::wstring::npos);
|
||||
}
|
||||
|
||||
// Tests BuildStage2Arguments: both paths are wrapped in double quotes to
|
||||
// survive CommandLineToArgvW parsing when paths contain spaces.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — quote wrapping.
|
||||
// Setup: Installer path with spaces.
|
||||
TEST_METHOD(BuildStage2ArgumentsQuotesBothPaths)
|
||||
{
|
||||
const auto args = updating::BuildStage2Arguments(
|
||||
L"-update_now_stage_2",
|
||||
L"C:\\path with spaces\\installer.exe",
|
||||
L"C:\\Program Files\\PowerToys");
|
||||
|
||||
// Count quotes — should have 4 (open/close for each path)
|
||||
size_t quoteCount = std::count(args.begin(), args.end(), L'"');
|
||||
Assert::AreEqual(size_t{ 4 }, quoteCount);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: appends "PowerToys.exe" to the install dir.
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path / operator.
|
||||
// Setup: Standard install path without trailing backslash.
|
||||
TEST_METHOD(BuildPowerToysExePathAppendsExeName)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys");
|
||||
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: trailing backslash does not produce double
|
||||
// backslash (e.g., "...PowerToys\\PowerToys.exe").
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path normalizes separators.
|
||||
// Setup: Install path with trailing backslash.
|
||||
TEST_METHOD(BuildPowerToysExePathHandlesTrailingBackslash)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"C:\\Program Files\\PowerToys\\");
|
||||
Assert::AreEqual(std::wstring(L"C:\\Program Files\\PowerToys\\PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests BuildPowerToysExePath: empty string produces just "PowerToys.exe".
|
||||
// Covers: updateLifecycle.h BuildPowerToysExePath — fs::path with empty input.
|
||||
// Setup: Empty install directory string.
|
||||
TEST_METHOD(BuildPowerToysExePathHandlesEmptyString)
|
||||
{
|
||||
const auto path = updating::BuildPowerToysExePath(L"");
|
||||
Assert::AreEqual(std::wstring(L"PowerToys.exe"), path);
|
||||
}
|
||||
|
||||
// Tests CanRelaunchAfterUpdate: returns true when Stage 2 receives
|
||||
// the install directory (argCount >= 4), false otherwise.
|
||||
// This is the gate that prevents relaunch when using an old Stage 1
|
||||
// that didn't pass the install dir (#42004/#43011/#44071).
|
||||
// Covers: updateLifecycle.h CanRelaunchAfterUpdate.
|
||||
TEST_METHOD(CanRelaunchReflectsArgCount)
|
||||
{
|
||||
// Old Stage 1 (pre-fix): only passed action + installer = 3 args
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(0));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(1));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(2));
|
||||
Assert::IsFalse(updating::CanRelaunchAfterUpdate(3));
|
||||
|
||||
// New Stage 1 (post-fix): passes action + installer + installDir = 4 args
|
||||
Assert::IsTrue(updating::CanRelaunchAfterUpdate(4));
|
||||
Assert::IsTrue(updating::CanRelaunchAfterUpdate(5));
|
||||
}
|
||||
|
||||
// Tests BuildStage2Arguments + CommandLineToArgvW round-trip: the exact
|
||||
// scenario where Stage 1 builds args and Windows parses them in Stage 2.
|
||||
// Verifies quoting is correct so paths with spaces survive the round trip.
|
||||
// Covers: updateLifecycle.h BuildStage2Arguments — quote correctness.
|
||||
// Setup: Realistic paths with spaces and version numbers.
|
||||
TEST_METHOD(Stage2ArgumentsCanBeRoundTrippedThroughCommandLineToArgvW)
|
||||
{
|
||||
const std::wstring installerPath = L"C:\\Users\\test user\\AppData\\Local\\PowerToys\\Updates\\powertoyssetup-0.86.0-x64.exe";
|
||||
const std::wstring installDir = L"C:\\Program Files\\PowerToys";
|
||||
|
||||
const auto args = updating::BuildStage2Arguments(L"-update_now_stage_2", installerPath, installDir);
|
||||
|
||||
// Simulate what Windows does: prepend a fake exe name and parse
|
||||
std::wstring commandLine = L"PowerToys.Update.exe " + args;
|
||||
|
||||
int argc = 0;
|
||||
LPWSTR* argv = CommandLineToArgvW(commandLine.c_str(), &argc);
|
||||
Assert::IsNotNull(argv);
|
||||
Assert::AreEqual(4, argc);
|
||||
Assert::AreEqual(std::wstring(L"-update_now_stage_2"), std::wstring(argv[1]));
|
||||
Assert::AreEqual(installerPath, std::wstring(argv[2]));
|
||||
Assert::AreEqual(installDir, std::wstring(argv[3]));
|
||||
|
||||
LocalFree(argv);
|
||||
}
|
||||
};
|
||||
}
|
||||
45
src/common/updating/UnitTests/UpdatingUnitTests.vcxproj
Normal file
45
src/common/updating/UnitTests/UpdatingUnitTests.vcxproj
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<RootNamespace>UpdatingUnitTests</RootNamespace>
|
||||
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
|
||||
<ProjectName>Updating.UnitTests</ProjectName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseOfMfc>false</UseOfMfc>
|
||||
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\tests\UpdatingUnitTests\</OutDir>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\;..\..\;..\..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="UpdatingTests.cpp" />
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
5
src/common/updating/UnitTests/pch.cpp
Normal file
5
src/common/updating/UnitTests/pch.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "pch.h"
|
||||
17
src/common/updating/UnitTests/pch.h
Normal file
17
src/common/updating/UnitTests/pch.h
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#ifndef PCH_H
|
||||
#define PCH_H
|
||||
|
||||
#include <atomic>
|
||||
#include <Windows.h>
|
||||
|
||||
// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 26466)
|
||||
#include "CppUnitTest.h"
|
||||
#pragma warning(pop)
|
||||
|
||||
#endif //PCH_H
|
||||
170
src/common/updating/configBackup.h
Normal file
170
src/common/updating/configBackup.h
Normal file
@@ -0,0 +1,170 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Check if a JSON file is corrupted (contains null bytes, as seen in #46179)
|
||||
inline bool IsJsonFileCorrupted(const fs::path& filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream file(filePath, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t c_readChunkSize{ 4096 };
|
||||
char buffer[c_readChunkSize];
|
||||
while (file.read(buffer, c_readChunkSize) || file.gcount() > 0)
|
||||
{
|
||||
const auto bytesRead = file.gcount();
|
||||
for (std::streamsize i = 0; i < bytesRead; ++i)
|
||||
{
|
||||
if (buffer[i] == '\0')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Backup all JSON config files before update to protect against corruption (#46179)
|
||||
inline void BackupConfigFiles(const fs::path& rootPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
const fs::path backupDir = rootPath / L"ConfigBackup";
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(backupDir, ec);
|
||||
// Note: remove_all failure means stale backup may persist; continue anyway
|
||||
// since create_directories will overlay
|
||||
fs::create_directories(backupDir, ec);
|
||||
if (ec)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& entry : fs::directory_iterator(rootPath, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.is_regular_file() && entry.path().extension() == L".json")
|
||||
{
|
||||
fs::copy_file(entry.path(), backupDir / entry.path().filename(), fs::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
else if (entry.is_directory())
|
||||
{
|
||||
const auto dirName = entry.path().filename().wstring();
|
||||
if (dirName == L"ConfigBackup" || dirName == L"Updates")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto moduleBackup = backupDir / entry.path().filename();
|
||||
fs::create_directories(moduleBackup, ec);
|
||||
|
||||
std::error_code moduleEc;
|
||||
for (const auto& moduleEntry : fs::directory_iterator(entry.path(), moduleEc))
|
||||
{
|
||||
if (moduleEc)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (moduleEntry.is_regular_file() && moduleEntry.path().extension() == L".json")
|
||||
{
|
||||
fs::copy_file(moduleEntry.path(), moduleBackup / moduleEntry.path().filename(), fs::copy_options::overwrite_existing, moduleEc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Intentionally swallowed — update must not fail due to backup errors.
|
||||
// Logging would require spdlog dependency which is unavailable in test context.
|
||||
}
|
||||
}
|
||||
|
||||
// Restore JSON configs from backup if corruption is detected after update
|
||||
inline void RestoreCorruptedConfigs(const fs::path& rootPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
const fs::path backupDir = rootPath / L"ConfigBackup";
|
||||
|
||||
if (!fs::exists(backupDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
for (const auto& backupEntry : fs::directory_iterator(backupDir, ec))
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (backupEntry.is_regular_file() && backupEntry.path().extension() == L".json")
|
||||
{
|
||||
const auto originalPath = rootPath / backupEntry.path().filename();
|
||||
// Only restore if the backup itself is valid
|
||||
if (fs::exists(originalPath) && IsJsonFileCorrupted(originalPath) && !IsJsonFileCorrupted(backupEntry.path()))
|
||||
{
|
||||
fs::copy_file(backupEntry.path(), originalPath, fs::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
}
|
||||
else if (backupEntry.is_directory())
|
||||
{
|
||||
const auto moduleDir = rootPath / backupEntry.path().filename();
|
||||
|
||||
std::error_code moduleEc;
|
||||
for (const auto& moduleBackupEntry : fs::directory_iterator(backupEntry.path(), moduleEc))
|
||||
{
|
||||
if (moduleEc)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (moduleBackupEntry.is_regular_file() && moduleBackupEntry.path().extension() == L".json")
|
||||
{
|
||||
const auto originalModulePath = moduleDir / moduleBackupEntry.path().filename();
|
||||
// Only restore if the backup itself is valid
|
||||
if (fs::exists(originalModulePath) && IsJsonFileCorrupted(originalModulePath) && !IsJsonFileCorrupted(moduleBackupEntry.path()))
|
||||
{
|
||||
fs::copy_file(moduleBackupEntry.path(), originalModulePath, fs::copy_options::overwrite_existing, moduleEc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Intentionally swallowed — update must not fail due to backup errors.
|
||||
// Logging would require spdlog dependency which is unavailable in test context.
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/common/updating/updateLifecycle.h
Normal file
47
src/common/updating/updateLifecycle.h
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace updating
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// Build the command-line arguments for Stage 2.
|
||||
// Stage 1 passes the installer path and the PT install directory
|
||||
// so Stage 2 can run the installer and relaunch PowerToys afterward.
|
||||
// Note: paths containing embedded double-quote characters are not supported.
|
||||
// This is safe because install paths come from get_module_folderpath().
|
||||
inline std::wstring BuildStage2Arguments(
|
||||
const std::wstring& stage2Flag,
|
||||
const fs::path& installerPath,
|
||||
const fs::path& installDir)
|
||||
{
|
||||
std::wstring arguments{ stage2Flag };
|
||||
arguments += L" \"";
|
||||
arguments += installerPath.c_str();
|
||||
arguments += L"\" \"";
|
||||
arguments += installDir.c_str();
|
||||
arguments += L"\"";
|
||||
return arguments;
|
||||
}
|
||||
|
||||
// Build the full path to PowerToys.exe from the install directory.
|
||||
// Used by Stage 2 to relaunch PT after a successful update.
|
||||
inline std::wstring BuildPowerToysExePath(const std::wstring& installDir)
|
||||
{
|
||||
return (std::filesystem::path(installDir) / L"PowerToys.exe").wstring();
|
||||
}
|
||||
|
||||
// Determine whether Stage 2 has enough information to relaunch PT.
|
||||
// Returns true if the install directory argument was provided.
|
||||
inline bool CanRelaunchAfterUpdate(int argCount)
|
||||
{
|
||||
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
|
||||
return argCount >= 4;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public class BaseDscTest
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the resource string.</param>
|
||||
/// <param name="args">The arguments to format the resource string with.</param>
|
||||
/// <returns></returns>
|
||||
/// <returns>The formatted resource string.</returns>
|
||||
public string GetResourceString(string name, params string[] args)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, _resourceManager.GetString(name, CultureInfo.InvariantCulture), args);
|
||||
@@ -35,9 +35,9 @@ public class BaseDscTest
|
||||
/// <summary>
|
||||
/// Execute a dsc command with the provided arguments.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
/// <typeparam name="T">The type of the DSC command to execute.</typeparam>
|
||||
/// <param name="args">The command-line arguments to pass to the DSC command.</param>
|
||||
/// <returns>The result of the DSC command execution.</returns>
|
||||
protected DscExecuteResult ExecuteDscCommand<T>(params string[] args)
|
||||
where T : Command, new()
|
||||
{
|
||||
|
||||
@@ -47,6 +47,6 @@ public interface ISettingsFunctionData
|
||||
/// <summary>
|
||||
/// Gets the schema for the settings resource object.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>The JSON schema string for the settings resource object.</returns>
|
||||
public string Schema();
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public class BaseResourceObject
|
||||
/// <summary>
|
||||
/// Generates a JSON representation of the resource object.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>A JSON representation of the resource object.</returns>
|
||||
public JsonNode ToJson()
|
||||
{
|
||||
return JsonSerializer.SerializeToNode(this, GetType(), _options) ?? new JsonObject();
|
||||
|
||||
@@ -150,7 +150,6 @@
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<!-- TEMPORARILY_DISABLED: PowerDisplay
|
||||
<policy name="ConfigureEnabledUtilityPowerDisplay" class="Both" displayName="$(string.ConfigureEnabledUtilityPowerDisplay)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityPowerDisplay">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_95_0" />
|
||||
@@ -161,7 +160,6 @@
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
-->
|
||||
<policy name="ConfigureEnabledUtilityEnvironmentVariables" class="Both" displayName="$(string.ConfigureEnabledUtilityEnvironmentVariables)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityEnvironmentVariables">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_75_0" />
|
||||
|
||||
@@ -249,7 +249,7 @@ If you don't configure this policy, the user will be able to control the setting
|
||||
<string id="ConfigureEnabledUtilityCmdPal">CmdPal: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityCropAndLock">Crop And Lock: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityLightSwitch">Light Switch: Configure enabled state</string>
|
||||
<!-- <string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string> --><!-- TEMPORARILY_DISABLED: PowerDisplay -->
|
||||
<string id="ConfigureEnabledUtilityPowerDisplay">PowerDisplay: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityEnvironmentVariables">Environment Variables: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityFancyZones">FancyZones: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityFileLocksmith">File Locksmith: Configure enabled state</string>
|
||||
|
||||
@@ -70,12 +70,12 @@
|
||||
Spacing="2">
|
||||
<TextBlock
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind Header, Mode=OneWay}"
|
||||
Text="{x:Bind Header, Mode=OneTime}"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Timestamp, Converter={StaticResource DateTimeToFriendlyStringConverter}, Mode=OneWay}" />
|
||||
Text="{x:Bind Timestamp, Converter={StaticResource DateTimeToFriendlyStringConverter}, Mode=OneTime}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -29,31 +29,31 @@
|
||||
Padding="-9,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
AutomationProperties.AcceleratorKey="{x:Bind ShortcutText, Mode=OneWay}"
|
||||
AutomationProperties.AcceleratorKey="{x:Bind ShortcutText, Mode=OneTime}"
|
||||
AutomationProperties.AutomationControlType="ListItem"
|
||||
AutomationProperties.FullDescription="{x:Bind ToolTip, Mode=OneWay}"
|
||||
AutomationProperties.HelpText="{x:Bind Name, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind AccessibleName, Mode=OneWay}">
|
||||
AutomationProperties.FullDescription="{x:Bind ToolTip, Mode=OneTime}"
|
||||
AutomationProperties.HelpText="{x:Bind Name, Mode=OneTime}"
|
||||
AutomationProperties.Name="{x:Bind AccessibleName, Mode=OneTime}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="48" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
<TextBlock Text="{x:Bind ToolTip, Mode=OneTime}" />
|
||||
</ToolTipService.ToolTip>
|
||||
<FontIcon
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="16"
|
||||
Glyph="{x:Bind IconGlyph, Mode=OneWay}" />
|
||||
Glyph="{x:Bind IconGlyph, Mode=OneTime}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
x:Phase="1"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
Text="{x:Bind Name, Mode=OneTime}" />
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="0,0,8,0"
|
||||
@@ -61,7 +61,7 @@
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ShortcutText, Mode=OneWay}" />
|
||||
Text="{x:Bind ShortcutText, Mode=OneTime}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</controls:PasteFormatTemplateSelector.ItemTemplate>
|
||||
@@ -83,13 +83,13 @@
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"
|
||||
Glyph="{x:Bind IconGlyph, Mode=OneWay}" />
|
||||
Glyph="{x:Bind IconGlyph, Mode=OneTime}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
x:Phase="1"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Name, Mode=OneWay}" />
|
||||
Text="{x:Bind Name, Mode=OneTime}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</controls:PasteFormatTemplateSelector.ItemTemplateDisabled>
|
||||
@@ -198,7 +198,7 @@
|
||||
<ItemsView.ItemTemplate>
|
||||
<DataTemplate x:DataType="local:ClipboardItem">
|
||||
<ItemContainer
|
||||
AutomationProperties.Name="{x:Bind Description, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind Description, Mode=OneTime}"
|
||||
CornerRadius="16"
|
||||
ToolTipService.ToolTip="{x:Bind Content}">
|
||||
<Grid
|
||||
|
||||
180
src/modules/Hosts/Hosts.Tests/ValidationHelperTest.cs
Normal file
180
src/modules/Hosts/Hosts.Tests/ValidationHelperTest.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Linq;
|
||||
|
||||
using HostsUILib;
|
||||
using HostsUILib.Helpers;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Hosts.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public class ValidationHelperTest
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("0.0.0.0")]
|
||||
[DataRow("127.0.0.1")]
|
||||
[DataRow("192.168.1.1")]
|
||||
[DataRow("255.255.255.255")]
|
||||
[DataRow("10.0.0.1")]
|
||||
[DataRow("172.16.0.1")]
|
||||
[DataRow("1.2.3.4")]
|
||||
[DataRow("01.01.01.01")]
|
||||
[DataRow("0.0.0.1")]
|
||||
public void ValidIPv4_ValidAddresses_ReturnsTrue(string address)
|
||||
{
|
||||
Assert.IsTrue(ValidationHelper.ValidIPv4(address));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("256.0.0.0")]
|
||||
[DataRow("0.256.0.0")]
|
||||
[DataRow("0.0.256.0")]
|
||||
[DataRow("0.0.0.256")]
|
||||
[DataRow("999.999.999.999")]
|
||||
[DataRow("1.2.3")]
|
||||
[DataRow("1.2.3.4.5")]
|
||||
[DataRow("1.2.3.")]
|
||||
[DataRow(".1.2.3")]
|
||||
[DataRow("1..2.3")]
|
||||
[DataRow("abc.def.ghi.jkl")]
|
||||
[DataRow("192.168.1.1/24")]
|
||||
[DataRow("192.168.1.1:80")]
|
||||
[DataRow("192.168.1")]
|
||||
[DataRow("-1.0.0.0")]
|
||||
public void ValidIPv4_InvalidAddresses_ReturnsFalse(string address)
|
||||
{
|
||||
Assert.IsFalse(ValidationHelper.ValidIPv4(address));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
[DataRow("\t")]
|
||||
[DataRow("\n")]
|
||||
public void ValidIPv4_NullOrWhitespace_ReturnsFalse(string address)
|
||||
{
|
||||
Assert.IsFalse(ValidationHelper.ValidIPv4(address));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("::1")]
|
||||
[DataRow("::")]
|
||||
[DataRow("2001:0db8:85a3:0000:0000:8a2e:0370:7334")]
|
||||
[DataRow("2001:db8:85a3:0:0:8a2e:370:7334")]
|
||||
[DataRow("2001:db8:85a3::8a2e:370:7334")]
|
||||
[DataRow("fe80::1")]
|
||||
[DataRow("ff02::1")]
|
||||
[DataRow("2001:db8::1")]
|
||||
[DataRow("::ffff:192.168.1.1")]
|
||||
[DataRow("fe80::1%eth0")]
|
||||
public void ValidIPv6_ValidAddresses_ReturnsTrue(string address)
|
||||
{
|
||||
Assert.IsTrue(ValidationHelper.ValidIPv6(address));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("2001:db8:85a3:0:0:8a2e:370:7334:extra")]
|
||||
[DataRow("gggg::1")]
|
||||
[DataRow("12345::1")]
|
||||
[DataRow("192.168.1.1")]
|
||||
[DataRow("::ffff:999.999.999.999")]
|
||||
[DataRow("hello")]
|
||||
[DataRow("2001:db8:85a3::8a2e:370:7334:1234:5678")]
|
||||
public void ValidIPv6_InvalidAddresses_ReturnsFalse(string address)
|
||||
{
|
||||
Assert.IsFalse(ValidationHelper.ValidIPv6(address));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
[DataRow("\t")]
|
||||
public void ValidIPv6_NullOrWhitespace_ReturnsFalse(string address)
|
||||
{
|
||||
Assert.IsFalse(ValidationHelper.ValidIPv6(address));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow("localhost")]
|
||||
[DataRow("example.com")]
|
||||
[DataRow("sub.domain.example.com")]
|
||||
[DataRow("my-host")]
|
||||
[DataRow("host1 host2")]
|
||||
[DataRow("host1 host2 host3")]
|
||||
[DataRow("example.com www.example.com")]
|
||||
public void ValidHosts_ValidHostnames_ReturnsTrue(string hosts)
|
||||
{
|
||||
Assert.IsTrue(ValidationHelper.ValidHosts(hosts, validateHostsLength: false));
|
||||
}
|
||||
|
||||
[DataTestMethod]
|
||||
[DataRow(null)]
|
||||
[DataRow("")]
|
||||
[DataRow(" ")]
|
||||
[DataRow("\t")]
|
||||
public void ValidHosts_NullOrWhitespace_ReturnsFalse(string hosts)
|
||||
{
|
||||
Assert.IsFalse(ValidationHelper.ValidHosts(hosts, validateHostsLength: false));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHosts_WithLengthValidation_ExceedsMaxCount_ReturnsFalse()
|
||||
{
|
||||
// Create a host string with one more than MaxHostsCount hosts
|
||||
var hosts = string.Join(" ", Enumerable.Range(1, Consts.MaxHostsCount + 1).Select(i => $"h{i}"));
|
||||
Assert.IsFalse(ValidationHelper.ValidHosts(hosts, validateHostsLength: true));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHosts_WithLengthValidation_AtMaxCount_ReturnsTrue()
|
||||
{
|
||||
// Create a host string with exactly MaxHostsCount hosts
|
||||
var hosts = string.Join(" ", Enumerable.Range(1, Consts.MaxHostsCount).Select(i => $"h{i}"));
|
||||
Assert.IsTrue(ValidationHelper.ValidHosts(hosts, validateHostsLength: true));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHosts_WithLengthValidation_BelowMaxCount_ReturnsTrue()
|
||||
{
|
||||
string hosts = "host1 host2 host3";
|
||||
Assert.IsTrue(ValidationHelper.ValidHosts(hosts, validateHostsLength: true));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHosts_WithoutLengthValidation_ExceedsMaxCount_ReturnsTrue()
|
||||
{
|
||||
// When validateHostsLength is false, exceeding max count should still return true
|
||||
var hosts = string.Join(" ", Enumerable.Range(1, Consts.MaxHostsCount + 1).Select(i => $"h{i}"));
|
||||
Assert.IsTrue(ValidationHelper.ValidHosts(hosts, validateHostsLength: false));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHosts_SingleHost_ReturnsTrue()
|
||||
{
|
||||
Assert.IsTrue(ValidationHelper.ValidHosts("localhost", validateHostsLength: true));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHosts_InvalidHostname_ReturnsFalse()
|
||||
{
|
||||
Assert.IsFalse(ValidationHelper.ValidHosts("host_with!invalid@chars", validateHostsLength: false));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHosts_HostWithSubdomains_ReturnsTrue()
|
||||
{
|
||||
Assert.IsTrue(ValidationHelper.ValidHosts("sub.domain.example.com", validateHostsLength: true));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ValidHosts_MultipleValidHosts_WithLengthValidation_ReturnsTrue()
|
||||
{
|
||||
Assert.IsTrue(ValidationHelper.ValidHosts("example.com www.example.com api.example.com", validateHostsLength: true));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Globalization;
|
||||
@@ -29,11 +30,12 @@ namespace PowerOCR;
|
||||
|
||||
internal sealed class ImageMethods
|
||||
{
|
||||
internal static Bitmap PadImage(Bitmap image, int minW = 64, int minH = 64)
|
||||
internal static bool PadImage(Bitmap image, [NotNullWhen(true)] out Bitmap? paddedBitmap, int minW = 64, int minH = 64)
|
||||
{
|
||||
if (image.Height >= minH && image.Width >= minW)
|
||||
{
|
||||
return image;
|
||||
paddedBitmap = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
int width = Math.Max(image.Width + 16, minW + 16);
|
||||
@@ -45,8 +47,9 @@ internal sealed class ImageMethods
|
||||
|
||||
gd.Clear(image.GetPixel(0, 0));
|
||||
gd.DrawImageUnscaled(image, 8, 8);
|
||||
paddedBitmap = destination;
|
||||
|
||||
return destination;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static ImageSource GetWindowBoundsImage(OCROverlay passedWindow)
|
||||
@@ -77,8 +80,15 @@ internal sealed class ImageMethods
|
||||
bmp.Size,
|
||||
CopyPixelOperation.SourceCopy);
|
||||
|
||||
bmp = PadImage(bmp);
|
||||
return bmp;
|
||||
if (PadImage(bmp, out var paddedBmp))
|
||||
{
|
||||
bmp.Dispose();
|
||||
return paddedBmp;
|
||||
}
|
||||
else
|
||||
{
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<string> GetRegionsText(OCROverlay? passedWindow, Rectangle selectedRegion, Language? preferredLanguage)
|
||||
|
||||
48
src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/.github/copilot-instructions.md
vendored
Normal file
48
src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# Command Palette Extension – Copilot Instructions
|
||||
|
||||
Concise guidance for AI-assisted development of this Command Palette extension.
|
||||
|
||||
## Project Structure
|
||||
|
||||
| Folder | Purpose |
|
||||
|--------|---------|
|
||||
| `Pages/` | Extension pages (ListPage, ContentPage, DynamicListPage implementations) |
|
||||
| `Assets/` | Icons and images (StoreLogo.png, etc.) |
|
||||
| `Properties/` | Launch settings and publish profiles |
|
||||
| Root `.cs` files | Extension entry point, COM server (Program.cs), and CommandsProvider |
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Extensions run **out-of-process** via COM server registration
|
||||
- `Program.cs` hosts the COM server — do not modify the hosting pattern
|
||||
- The `CommandProvider` subclass is the entry point for all commands
|
||||
- Pages are **ICommand** implementations — they can be used anywhere commands are used
|
||||
- Always **Deploy** (not just Build) to register the MSIX package
|
||||
- After deploying, use the **Reload** command in Command Palette to refresh
|
||||
|
||||
## Build & Deploy
|
||||
|
||||
1. In Visual Studio, use **Build > Deploy** (not just Build)
|
||||
2. In Command Palette, run `Reload` → select "Reload Command Palette extensions"
|
||||
3. For debugging, run in Debug configuration (F5) and check Output window (Ctrl+Alt+O)
|
||||
|
||||
## Source Control
|
||||
|
||||
If using git, remove these lines from `.gitignore` (needed for deployment):
|
||||
- `**/Properties/launchSettings.json`
|
||||
- `*.pubxml`
|
||||
|
||||
## Available Skills
|
||||
|
||||
This project includes Copilot skills for common workflows:
|
||||
- **add-adaptive-card-form** — Create form-based UI with Adaptive Cards
|
||||
- **add-extension-settings** — Add a settings page to your extension
|
||||
- **add-dock-band** — Add persistent toolbar widgets
|
||||
- **add-fallback-commands** — Add catch-all search commands
|
||||
- **publish-extension** — Publish to Microsoft Store or WinGet
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Creating an extension](https://learn.microsoft.com/windows/powertoys/command-palette/creating-an-extension)
|
||||
- [Extension samples](https://learn.microsoft.com/windows/powertoys/command-palette/samples)
|
||||
- [Extensibility overview](https://learn.microsoft.com/windows/powertoys/command-palette/extensibility-overview)
|
||||
@@ -0,0 +1,353 @@
|
||||
---
|
||||
description: 'Comprehensive guide for developing Command Palette extensions — covers pages, content, commands, items, icons, settings, dock, and debugging'
|
||||
applyTo: '**/*.cs'
|
||||
---
|
||||
|
||||
# Command Palette Extension Development
|
||||
|
||||
Complete reference for building Command Palette (CmdPal) extensions. Extensions run out-of-process as MSIX-packaged COM servers.
|
||||
|
||||
## Extension Architecture
|
||||
|
||||
### IExtension Interface
|
||||
|
||||
The root class implements `IExtension` and `IDisposable`:
|
||||
|
||||
```csharp
|
||||
[Guid("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")]
|
||||
public sealed partial class MyExtension : IExtension, IDisposable
|
||||
{
|
||||
private readonly ManualResetEvent _extensionDisposedEvent;
|
||||
private readonly MyCommandsProvider _provider = new();
|
||||
|
||||
public MyExtension(ManualResetEvent extensionDisposedEvent)
|
||||
{
|
||||
_extensionDisposedEvent = extensionDisposedEvent;
|
||||
}
|
||||
|
||||
public object? GetProvider(ProviderType providerType) => providerType switch
|
||||
{
|
||||
ProviderType.Commands => _provider,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
public void Dispose() => _extensionDisposedEvent.Set();
|
||||
}
|
||||
```
|
||||
|
||||
- Only `ProviderType.Commands` is currently supported
|
||||
- The `[Guid]` must match the CLSID in `Package.appxmanifest`
|
||||
|
||||
### CommandProvider
|
||||
|
||||
Override `TopLevelCommands()` to register main commands. Optionally override `FallbackCommands()` and `GetDockBands()`:
|
||||
|
||||
```csharp
|
||||
public partial class MyCommandsProvider : CommandProvider
|
||||
{
|
||||
public MyCommandsProvider()
|
||||
{
|
||||
DisplayName = "My Extension";
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => [
|
||||
new CommandItem(new MyPage()) { Title = DisplayName },
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### COM Server (Program.cs)
|
||||
|
||||
`Program.cs` hosts the COM server. Do not change this pattern:
|
||||
|
||||
```csharp
|
||||
public class Program
|
||||
{
|
||||
[MTAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
|
||||
{
|
||||
global::Shmuelie.WinRTServer.ComServer server = new();
|
||||
ManualResetEvent extensionDisposedEvent = new(false);
|
||||
var extensionInstance = new MyExtension(extensionDisposedEvent);
|
||||
server.RegisterClass<MyExtension, IExtension>(() => extensionInstance);
|
||||
server.Start();
|
||||
extensionDisposedEvent.WaitOne();
|
||||
server.Stop();
|
||||
server.UnsafeDispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Package.appxmanifest
|
||||
|
||||
Two critical extension registrations must be present:
|
||||
|
||||
1. **COM server** — `com:ComServer` with matching CLSID and `-RegisterProcessAsComServer` args
|
||||
2. **App extension** — `uap3:AppExtension` with `Name="com.microsoft.commandpalette"` and `CreateInstance ClassId` matching the GUID
|
||||
|
||||
The CLSID must be identical in three places: the `[Guid]` attribute, the `com:Class Id`, and the `CreateInstance ClassId`.
|
||||
|
||||
## Page Types
|
||||
|
||||
### ListPage (Most Common)
|
||||
|
||||
Displays a searchable list of items:
|
||||
|
||||
```csharp
|
||||
internal sealed partial class MyPage : ListPage
|
||||
{
|
||||
public MyPage()
|
||||
{
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
|
||||
Title = "My page";
|
||||
Name = "Open";
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => [
|
||||
new ListItem(new OpenUrlCommand("https://example.com")) { Title = "Example" },
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### DynamicListPage (Search-Reactive)
|
||||
|
||||
Responds to search text changes for filtering or live queries:
|
||||
|
||||
```csharp
|
||||
internal sealed partial class MyDynamicPage : DynamicListPage
|
||||
{
|
||||
private IListItem[] _filteredItems = [];
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
_filteredItems = _allItems
|
||||
.Where(i => i.Title.Contains(newSearch, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
RaiseItemsChanged();
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _filteredItems;
|
||||
}
|
||||
```
|
||||
|
||||
- Supports `Filters` property for category filtering
|
||||
- Call `RaiseItemsChanged()` after updating items to notify the UI
|
||||
|
||||
### ContentPage (Rich Content)
|
||||
|
||||
Displays rich content like markdown, forms, or images:
|
||||
|
||||
```csharp
|
||||
internal sealed partial class MyContentPage : ContentPage
|
||||
{
|
||||
public override IContent[] GetContent() => [
|
||||
new MarkdownContent("# Hello\nThis is **markdown**."),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
- Can return multiple `IContent` items (mix markdown, forms, images, etc.)
|
||||
- Supports `Commands` property for context menu items via `CommandContextItem`
|
||||
|
||||
## Content Types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `MarkdownContent(string)` | Renders markdown with headers, links, code blocks, tables, images |
|
||||
| `FormContent` | Adaptive Cards forms with `TemplateJson`, optional `DataJson`, and `SubmitForm()` |
|
||||
| `PlainTextContent(string)` | Plain text; optional `FontFamily.Monospace` and `WrapWords` |
|
||||
| `ImageContent` | Images with `MaxWidth`/`MaxHeight` constraints |
|
||||
| `TreeContent` | Hierarchical nested content; override `GetChildren()` for child `IContent[]` |
|
||||
|
||||
### MarkdownContent Images
|
||||
|
||||
Supports `file:`, `data:` (base64), and `https:` URLs. Image hints control rendering:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
### 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 |
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: add-adaptive-card-form
|
||||
description: >-
|
||||
Create form-based UI for your Command Palette extension using Adaptive Cards.
|
||||
Use when asked to add forms, user input fields, toggle switches, text inputs,
|
||||
dropdown menus, data entry, surveys, configuration dialogs, or interactive
|
||||
content pages. Supports the Adaptive Cards Designer for visual form building.
|
||||
---
|
||||
|
||||
# Add Forms with Adaptive Cards
|
||||
|
||||
Create interactive forms in your Command Palette extension using Adaptive Cards. Forms allow you to collect user input through text fields, toggles, dropdowns, and other controls.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Adding a form to collect user input (name, settings, feedback)
|
||||
- Creating interactive configuration dialogs
|
||||
- Building data entry interfaces
|
||||
- Adding toggle switches or dropdown menus
|
||||
- Displaying complex layouts beyond simple lists
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Familiarity with [Adaptive Cards](https://adaptivecards.io/)
|
||||
- Optional: Use the [Adaptive Card Designer](https://adaptivecards.io/designer/) to visually build your form
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Create a ContentPage with FormContent
|
||||
|
||||
Create a new file in your `Pages/` directory:
|
||||
|
||||
```csharp
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace YourExtension;
|
||||
|
||||
internal sealed partial class MyFormPage : ContentPage
|
||||
{
|
||||
private readonly MyForm _form = new();
|
||||
|
||||
public MyFormPage()
|
||||
{
|
||||
Name = "Open";
|
||||
Title = "My Form";
|
||||
Icon = new IconInfo("\uECA5");
|
||||
}
|
||||
|
||||
public override IContent[] GetContent() => [_form];
|
||||
}
|
||||
|
||||
internal sealed partial class MyForm : FormContent
|
||||
{
|
||||
public MyForm()
|
||||
{
|
||||
TemplateJson = """
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"label": "Name",
|
||||
"id": "Name",
|
||||
"isRequired": true,
|
||||
"errorMessage": "Name is required",
|
||||
"placeholder": "Enter your name"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Submit"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var formInput = JsonNode.Parse(payload)?.AsObject();
|
||||
if (formInput == null)
|
||||
{
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
var name = formInput["Name"]?.ToString() ?? "Unknown";
|
||||
return CommandResult.ShowToast($"Hello, {name}!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Register the Page
|
||||
|
||||
In your `CommandsProvider`, add the form page:
|
||||
|
||||
```csharp
|
||||
_commands = [
|
||||
new CommandItem(new MyFormPage()) { Title = "My Form" },
|
||||
];
|
||||
```
|
||||
|
||||
### Step 3: Deploy and Test
|
||||
|
||||
1. Deploy your extension
|
||||
2. In Command Palette, run `Reload`
|
||||
3. Navigate to your form and submit it
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### TemplateJson
|
||||
The JSON layout of your form (from Adaptive Cards schema). Design it at https://adaptivecards.io/designer/
|
||||
|
||||
### DataJson (Optional)
|
||||
Dynamic data binding using `${...}` placeholders in your TemplateJson:
|
||||
```csharp
|
||||
TemplateJson = """{ "body": [{ "type": "TextBlock", "text": "${title}" }] }""";
|
||||
DataJson = """{ "title": "Dynamic Title" }""";
|
||||
```
|
||||
|
||||
### SubmitForm
|
||||
Called when the user submits. Parse `payload` as JSON to read input values by their `id`.
|
||||
|
||||
### Mixing Content Types
|
||||
You can combine forms with markdown on the same page:
|
||||
```csharp
|
||||
public override IContent[] GetContent() => [
|
||||
new MarkdownContent("# Instructions\nFill out the form below."),
|
||||
_form,
|
||||
];
|
||||
```
|
||||
|
||||
## Common Form Patterns
|
||||
|
||||
See [form-patterns.md](references/form-patterns.md) for template JSON for common form types.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Get user input with forms](https://learn.microsoft.com/windows/powertoys/command-palette/using-form-pages)
|
||||
- [Adaptive Card Designer](https://adaptivecards.io/designer/)
|
||||
- [Adaptive Cards Schema](https://adaptivecards.io/explorer/)
|
||||
@@ -0,0 +1,536 @@
|
||||
# Common Adaptive Card Form Patterns
|
||||
|
||||
Reusable template JSON and handler code for the most common form types in Command Palette extensions.
|
||||
|
||||
---
|
||||
|
||||
## Simple Text Input Form
|
||||
|
||||
A basic form with one or two text fields and a submit button.
|
||||
|
||||
### TemplateJson
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"id": "FirstName",
|
||||
"label": "First Name",
|
||||
"placeholder": "Enter your first name",
|
||||
"isRequired": true,
|
||||
"errorMessage": "First name is required"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"id": "Email",
|
||||
"label": "Email Address",
|
||||
"placeholder": "user@example.com",
|
||||
"style": "Email",
|
||||
"isRequired": true,
|
||||
"errorMessage": "A valid email is required"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Submit"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### SubmitForm Handler
|
||||
|
||||
```csharp
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var input = JsonNode.Parse(payload)?.AsObject();
|
||||
if (input == null) return CommandResult.GoHome();
|
||||
|
||||
var firstName = input["FirstName"]?.ToString() ?? "";
|
||||
var email = input["Email"]?.ToString() ?? "";
|
||||
|
||||
return CommandResult.ShowToast($"Registered {firstName} ({email})");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Toggle/Checkbox Form
|
||||
|
||||
Use `Input.Toggle` for boolean on/off settings. Combine with `DataJson` for dynamic defaults.
|
||||
|
||||
### TemplateJson
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Preferences",
|
||||
"weight": "Bolder",
|
||||
"size": "Medium"
|
||||
},
|
||||
{
|
||||
"type": "Input.Toggle",
|
||||
"id": "AcceptsTerms",
|
||||
"title": "I accept the terms and conditions",
|
||||
"valueOn": "true",
|
||||
"valueOff": "false",
|
||||
"value": "false"
|
||||
},
|
||||
{
|
||||
"type": "Input.Toggle",
|
||||
"id": "EnableNotifications",
|
||||
"title": "Enable notifications",
|
||||
"valueOn": "true",
|
||||
"valueOff": "false",
|
||||
"value": "${notificationsDefault}"
|
||||
},
|
||||
{
|
||||
"type": "Input.Toggle",
|
||||
"id": "DarkMode",
|
||||
"title": "Use dark mode",
|
||||
"valueOn": "true",
|
||||
"valueOff": "false",
|
||||
"value": "${darkModeDefault}"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Save Preferences"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### DataJson (Dynamic Defaults)
|
||||
|
||||
```csharp
|
||||
DataJson = """
|
||||
{
|
||||
"notificationsDefault": "true",
|
||||
"darkModeDefault": "false"
|
||||
}
|
||||
""";
|
||||
```
|
||||
|
||||
### SubmitForm Handler
|
||||
|
||||
```csharp
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var input = JsonNode.Parse(payload)?.AsObject();
|
||||
if (input == null) return CommandResult.GoHome();
|
||||
|
||||
var accepted = input["AcceptsTerms"]?.ToString() == "true";
|
||||
var notifications = input["EnableNotifications"]?.ToString() == "true";
|
||||
var darkMode = input["DarkMode"]?.ToString() == "true";
|
||||
|
||||
if (!accepted)
|
||||
{
|
||||
return CommandResult.ShowToast("You must accept the terms to continue.");
|
||||
}
|
||||
|
||||
// Save preferences...
|
||||
return CommandResult.ShowToast("Preferences saved!");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Choice Set (Dropdown/Radio) Form
|
||||
|
||||
Use `Input.ChoiceSet` for single-select dropdowns or radio buttons.
|
||||
|
||||
### Compact Style (Dropdown)
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.ChoiceSet",
|
||||
"id": "Priority",
|
||||
"label": "Priority Level",
|
||||
"style": "compact",
|
||||
"value": "medium",
|
||||
"choices": [
|
||||
{ "title": "Low", "value": "low" },
|
||||
{ "title": "Medium", "value": "medium" },
|
||||
{ "title": "High", "value": "high" },
|
||||
{ "title": "Critical", "value": "critical" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Input.ChoiceSet",
|
||||
"id": "Category",
|
||||
"label": "Category",
|
||||
"style": "compact",
|
||||
"choices": [
|
||||
{ "title": "Bug Report", "value": "bug" },
|
||||
{ "title": "Feature Request", "value": "feature" },
|
||||
{ "title": "Documentation", "value": "docs" },
|
||||
{ "title": "Question", "value": "question" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Create Issue"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Expanded Style (Radio Buttons)
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.ChoiceSet",
|
||||
"id": "Theme",
|
||||
"label": "Select a theme",
|
||||
"style": "expanded",
|
||||
"value": "system",
|
||||
"choices": [
|
||||
{ "title": "Light", "value": "light" },
|
||||
{ "title": "Dark", "value": "dark" },
|
||||
{ "title": "System Default", "value": "system" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Apply"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Section Form
|
||||
|
||||
Combine multiple input types with TextBlock headers to create organized, multi-section forms. Use `Action.ShowCard` for progressive disclosure of optional sections.
|
||||
|
||||
### TemplateJson
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Personal Information",
|
||||
"weight": "Bolder",
|
||||
"size": "Medium",
|
||||
"separator": true
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"id": "FullName",
|
||||
"label": "Full Name",
|
||||
"placeholder": "Enter your full name",
|
||||
"isRequired": true,
|
||||
"errorMessage": "Name is required"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"id": "Email",
|
||||
"label": "Email",
|
||||
"placeholder": "user@example.com",
|
||||
"style": "Email"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Preferences",
|
||||
"weight": "Bolder",
|
||||
"size": "Medium",
|
||||
"separator": true,
|
||||
"spacing": "Large"
|
||||
},
|
||||
{
|
||||
"type": "Input.ChoiceSet",
|
||||
"id": "Language",
|
||||
"label": "Preferred Language",
|
||||
"style": "compact",
|
||||
"value": "en",
|
||||
"choices": [
|
||||
{ "title": "English", "value": "en" },
|
||||
{ "title": "Spanish", "value": "es" },
|
||||
{ "title": "French", "value": "fr" },
|
||||
{ "title": "German", "value": "de" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Input.Toggle",
|
||||
"id": "Newsletter",
|
||||
"title": "Subscribe to newsletter",
|
||||
"valueOn": "true",
|
||||
"valueOff": "false",
|
||||
"value": "true"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Save Profile"
|
||||
},
|
||||
{
|
||||
"type": "Action.ShowCard",
|
||||
"title": "Advanced Options",
|
||||
"card": {
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"id": "ApiKey",
|
||||
"label": "API Key (optional)",
|
||||
"placeholder": "Enter your API key"
|
||||
},
|
||||
{
|
||||
"type": "Input.Toggle",
|
||||
"id": "DebugMode",
|
||||
"title": "Enable debug mode",
|
||||
"valueOn": "true",
|
||||
"valueOff": "false",
|
||||
"value": "false"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Save All"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feedback Form
|
||||
|
||||
A common pattern for collecting user feedback with a multiline text area and a rating.
|
||||
|
||||
### TemplateJson
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "We'd love your feedback!",
|
||||
"weight": "Bolder",
|
||||
"size": "Medium"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Tell us what you think and how we can improve.",
|
||||
"wrap": true,
|
||||
"spacing": "Small"
|
||||
},
|
||||
{
|
||||
"type": "Input.ChoiceSet",
|
||||
"id": "Rating",
|
||||
"label": "How would you rate your experience?",
|
||||
"style": "expanded",
|
||||
"isRequired": true,
|
||||
"errorMessage": "Please select a rating",
|
||||
"choices": [
|
||||
{ "title": "⭐ Poor", "value": "1" },
|
||||
{ "title": "⭐⭐ Fair", "value": "2" },
|
||||
{ "title": "⭐⭐⭐ Good", "value": "3" },
|
||||
{ "title": "⭐⭐⭐⭐ Great", "value": "4" },
|
||||
{ "title": "⭐⭐⭐⭐⭐ Excellent", "value": "5" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"id": "Comments",
|
||||
"label": "Comments",
|
||||
"placeholder": "Share your thoughts...",
|
||||
"isMultiline": true,
|
||||
"maxLength": 500
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Send Feedback"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### SubmitForm Handler with Confirmation Dialog
|
||||
|
||||
```csharp
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var input = JsonNode.Parse(payload)?.AsObject();
|
||||
if (input == null) return CommandResult.GoHome();
|
||||
|
||||
var rating = input["Rating"]?.ToString() ?? "0";
|
||||
var comments = input["Comments"]?.ToString() ?? "";
|
||||
|
||||
return CommandResult.Confirm(new ConfirmationArgs
|
||||
{
|
||||
Title = "Submit feedback?",
|
||||
Description = $"Rating: {rating}/5\n\n{(string.IsNullOrEmpty(comments) ? "No comments" : comments)}",
|
||||
PrimaryCommand = new AnonymousCommand(() =>
|
||||
{
|
||||
// Process and store feedback
|
||||
new ToastStatusMessage("Thank you for your feedback!").Show();
|
||||
})
|
||||
{
|
||||
Name = "Submit",
|
||||
Result = CommandResult.Dismiss(),
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tree Content with Forms (Comment/Reply Pattern)
|
||||
|
||||
Use `TreeContent` to create nested, threaded discussions where each node can contain a form for replies.
|
||||
|
||||
### Post Content (Tree Node)
|
||||
|
||||
```csharp
|
||||
internal sealed partial class PostContent : TreeContent
|
||||
{
|
||||
private readonly string _author;
|
||||
private readonly string _body;
|
||||
private readonly PostReplyForm _replyForm;
|
||||
private readonly List<PostContent> _childPosts = [];
|
||||
|
||||
public PostContent(string author, string body)
|
||||
{
|
||||
_author = author;
|
||||
_body = body;
|
||||
_replyForm = new PostReplyForm(this);
|
||||
|
||||
TemplateJson = """
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${author}",
|
||||
"weight": "Bolder"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${body}",
|
||||
"wrap": true
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
DataJson = $$"""{ "author": "{{_author}}", "body": "{{_body}}" }""";
|
||||
}
|
||||
|
||||
public override IContent[] GetChildren() => [_replyForm, .. _childPosts];
|
||||
|
||||
public void AddReply(PostContent reply) => _childPosts.Add(reply);
|
||||
}
|
||||
```
|
||||
|
||||
### Reply Form (Child of Tree Node)
|
||||
|
||||
```csharp
|
||||
internal sealed partial class PostReplyForm : FormContent
|
||||
{
|
||||
private readonly PostContent _parent;
|
||||
|
||||
public PostReplyForm(PostContent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
TemplateJson = """
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"id": "ReplyText",
|
||||
"placeholder": "Write a reply...",
|
||||
"isMultiline": true
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "Reply"
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var input = JsonNode.Parse(payload)?.AsObject();
|
||||
if (input == null) return CommandResult.GoHome();
|
||||
|
||||
var replyText = input["ReplyText"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(replyText))
|
||||
{
|
||||
_parent.AddReply(new PostContent("You", replyText));
|
||||
}
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Hosting the Thread on a ContentPage
|
||||
|
||||
```csharp
|
||||
internal sealed partial class ThreadPage : ContentPage
|
||||
{
|
||||
private readonly PostContent _rootPost;
|
||||
|
||||
public ThreadPage()
|
||||
{
|
||||
Name = "Discussion";
|
||||
Title = "Discussion Thread";
|
||||
Icon = new IconInfo("\uE90A");
|
||||
|
||||
_rootPost = new PostContent("Alice", "Has anyone tried the new API?");
|
||||
_rootPost.AddReply(new PostContent("Bob", "Yes! It works great."));
|
||||
}
|
||||
|
||||
public override IContent[] GetContent() => [_rootPost];
|
||||
}
|
||||
```
|
||||
149
src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/.github/skills/add-dock-band/SKILL.md
vendored
Normal file
149
src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/.github/skills/add-dock-band/SKILL.md
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: add-dock-band
|
||||
description: >-
|
||||
Add dock band support to your Command Palette extension for persistent toolbar widgets.
|
||||
Use when asked to add dock support, toolbar buttons, persistent UI widgets,
|
||||
taskbar integration, live-updating status displays, quick-access buttons,
|
||||
or always-visible controls. Supports single buttons, multi-button strips,
|
||||
and live-updating content.
|
||||
---
|
||||
|
||||
# Add Dock Band Support
|
||||
|
||||
The Command Palette Dock is a persistent toolbar at the edge of the user's screen. Your extension can provide **dock bands** — strips of items that appear in the Dock — giving users quick access to commands without opening the full Command Palette.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Adding a quick-access button to the persistent toolbar
|
||||
- Creating a multi-button toolbar strip
|
||||
- Displaying live-updating information (clock, CPU usage, etc.)
|
||||
- Providing frequently-used commands without opening the full palette
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Command Palette Extension SDK version 0.9 or later (`Microsoft.CommandPalette.Extensions` ≥ 0.9.260303001)
|
||||
|
||||
## Quick Start: Single Button Dock Band
|
||||
|
||||
Override `GetDockBands()` in your `CommandProvider`:
|
||||
|
||||
```csharp
|
||||
public partial class MyCommandsProvider : CommandProvider
|
||||
{
|
||||
private readonly ICommandItem[] _commands;
|
||||
private readonly ICommandItem _dockBand;
|
||||
|
||||
public MyCommandsProvider()
|
||||
{
|
||||
DisplayName = "My Extension";
|
||||
Id = "com.mycompany.myextension"; // Unique ID required for dock
|
||||
|
||||
var mainPage = new MyPage();
|
||||
_dockBand = new CommandItem(mainPage) { Title = DisplayName };
|
||||
_commands = [new CommandItem(mainPage) { Title = DisplayName }];
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => _commands;
|
||||
|
||||
public override ICommandItem[]? GetDockBands() => [_dockBand];
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Button Dock Band
|
||||
|
||||
Use `WrappedDockItem` to create a band with multiple buttons:
|
||||
|
||||
```csharp
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
var button1 = new ListItem(new OpenUrlCommand("https://github.com"))
|
||||
{
|
||||
Title = "GitHub",
|
||||
Icon = new IconInfo("\uE774"),
|
||||
};
|
||||
var button2 = new ListItem(new OpenUrlCommand("https://learn.microsoft.com"))
|
||||
{
|
||||
Title = "Learn",
|
||||
Icon = new IconInfo("\uE82D"),
|
||||
};
|
||||
|
||||
var band = new WrappedDockItem(
|
||||
[button1, button2],
|
||||
"com.mycompany.myextension.quicklinks", // Unique band ID
|
||||
"Quick Links");
|
||||
|
||||
return [band];
|
||||
}
|
||||
```
|
||||
|
||||
## Live-Updating Dock Band
|
||||
|
||||
Create a dock band that updates its content periodically (like a clock):
|
||||
|
||||
```csharp
|
||||
internal sealed partial class LiveStatusBand : ListItem
|
||||
{
|
||||
private readonly System.Timers.Timer _timer;
|
||||
|
||||
public LiveStatusBand()
|
||||
: base(new NoOpCommand() { Result = CommandResult.KeepOpen() })
|
||||
{
|
||||
Title = DateTime.Now.ToString("HH:mm");
|
||||
Icon = new IconInfo("\uE823"); // Clock icon
|
||||
|
||||
_timer = new System.Timers.Timer(60_000); // Update every minute
|
||||
_timer.Elapsed += (s, e) =>
|
||||
{
|
||||
Title = DateTime.Now.ToString("HH:mm");
|
||||
Subtitle = DateTime.Now.ToString("dddd, MMMM d");
|
||||
};
|
||||
_timer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
// In CommandProvider:
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
var band = new WrappedDockItem(
|
||||
[new LiveStatusBand()],
|
||||
"com.mycompany.myextension.status",
|
||||
"Live Status");
|
||||
return [band];
|
||||
}
|
||||
```
|
||||
|
||||
## How Dock Bands Render
|
||||
|
||||
| Command Type on ICommandItem | Dock Behavior |
|
||||
|------------------------------|---------------|
|
||||
| `IInvokableCommand` | Single button that executes the command |
|
||||
| `IListPage` | Each list item renders as a separate button in one band |
|
||||
| `IContentPage` | Single expandable button with a flyout |
|
||||
|
||||
## Support Pinning Nested Commands
|
||||
|
||||
By default, only top-level commands and dock bands can be pinned. To allow pinning nested commands:
|
||||
|
||||
```csharp
|
||||
public override ICommandItem? GetCommandItem(string id)
|
||||
{
|
||||
// Look up commands by their Id
|
||||
foreach (var item in GetAllCommands())
|
||||
{
|
||||
if (item?.Command is ICommand cmd && cmd.Id == id)
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- All dock band `ICommandItem` objects must have a `Command` with a **non-empty `Id`** — items without an ID are ignored
|
||||
- Set `Id` on your `CommandProvider` (e.g., `Id = "com.mycompany.myextension"`)
|
||||
- Use `WrappedDockItem` for multi-button bands backed by a `ListPage`
|
||||
- Keep dock band updates lightweight — they run frequently
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Adding Dock support](https://learn.microsoft.com/windows/powertoys/command-palette/adding-dock-support)
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
name: add-extension-settings
|
||||
description: >-
|
||||
Add a settings page to your Command Palette extension.
|
||||
Use when asked to add settings, preferences, configuration options,
|
||||
toggles, text inputs, dropdowns, or user-customizable behavior.
|
||||
Covers ToggleSetting, TextSetting, ChoiceSetSetting, and persistence.
|
||||
---
|
||||
|
||||
# Add Extension Settings
|
||||
|
||||
Add a settings page to your Command Palette extension using the built-in settings helpers. Settings are automatically persisted and restored by the extension host.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Adding user-configurable options to your extension
|
||||
- Creating toggle switches for features
|
||||
- Adding text input fields for configuration
|
||||
- Creating dropdown menus for option selection
|
||||
- Persisting user preferences across sessions
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Create a Settings Manager
|
||||
|
||||
Create a new file `SettingsManager.cs`:
|
||||
|
||||
```csharp
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace YourExtension;
|
||||
|
||||
internal sealed class SettingsManager
|
||||
{
|
||||
private readonly Settings _settings;
|
||||
|
||||
public SettingsManager()
|
||||
{
|
||||
_settings = new Settings();
|
||||
|
||||
var maxResults = new TextSetting(
|
||||
"maxResults",
|
||||
"Maximum Results",
|
||||
"Maximum number of results to display",
|
||||
"10");
|
||||
|
||||
var showSubtitles = new ToggleSetting(
|
||||
"showSubtitles",
|
||||
"Show Subtitles",
|
||||
"Display subtitle text under each result",
|
||||
true);
|
||||
|
||||
var sortOrder = new ChoiceSetSetting(
|
||||
"sortOrder",
|
||||
"Sort Order",
|
||||
"How to sort results",
|
||||
[
|
||||
new ChoiceSetSetting.Choice("Alphabetical", "alpha"),
|
||||
new ChoiceSetSetting.Choice("Most Recent", "recent"),
|
||||
new ChoiceSetSetting.Choice("Most Used", "frequent"),
|
||||
],
|
||||
"alpha");
|
||||
|
||||
_settings.AddSetting(maxResults);
|
||||
_settings.AddSetting(showSubtitles);
|
||||
_settings.AddSetting(sortOrder);
|
||||
|
||||
// React to settings changes
|
||||
_settings.SettingsChanged += OnSettingsChanged;
|
||||
}
|
||||
|
||||
public ICommandSettings Settings => _settings;
|
||||
|
||||
public int MaxResults => int.TryParse(
|
||||
_settings.GetSetting<string>("maxResults"), out var val) ? val : 10;
|
||||
|
||||
public bool ShowSubtitles =>
|
||||
_settings.GetSetting<bool>("showSubtitles");
|
||||
|
||||
public string SortOrder =>
|
||||
_settings.GetSetting<string>("sortOrder") ?? "alpha";
|
||||
|
||||
private void OnSettingsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// React to settings changes (e.g., refresh data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Wire into CommandProvider
|
||||
|
||||
In your `CommandsProvider`, expose the settings:
|
||||
|
||||
```csharp
|
||||
public partial class MyCommandsProvider : CommandProvider
|
||||
{
|
||||
private readonly SettingsManager _settingsManager = new();
|
||||
private readonly ICommandItem[] _commands;
|
||||
|
||||
public MyCommandsProvider()
|
||||
{
|
||||
DisplayName = "My Extension";
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.png");
|
||||
Settings = _settingsManager.Settings; // This exposes settings to CmdPal
|
||||
_commands = [
|
||||
new CommandItem(new MyPage(_settingsManager)) { Title = DisplayName },
|
||||
];
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => _commands;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Use Settings in Pages
|
||||
|
||||
```csharp
|
||||
internal sealed partial class MyPage : ListPage
|
||||
{
|
||||
private readonly SettingsManager _settings;
|
||||
|
||||
public MyPage(SettingsManager settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
var items = GetAllItems();
|
||||
return items.Take(_settings.MaxResults).ToArray();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Setting Types
|
||||
|
||||
| Type | UI Control | Value Type | Constructor Parameters |
|
||||
|------|-----------|------------|----------------------|
|
||||
| `ToggleSetting` | Toggle switch | `bool` | `(id, label, description, defaultValue)` |
|
||||
| `TextSetting` | Text input | `string` | `(id, label, description, defaultValue)` |
|
||||
| `ChoiceSetSetting` | Dropdown | `string` | `(id, label, description, choices[], defaultValue)` |
|
||||
|
||||
## Key Points
|
||||
|
||||
- Settings are automatically persisted by the CmdPal host
|
||||
- Use `SettingsChanged` event to react to changes in real-time
|
||||
- Access values via `GetSetting<T>(id)` with the setting's string id
|
||||
- Pass the settings manager to pages/commands that need configuration
|
||||
- Settings page appears automatically when `Settings` is set on `CommandProvider`
|
||||
|
||||
## Grouping Settings
|
||||
|
||||
For extensions with many settings, organize them into logical groups:
|
||||
|
||||
```csharp
|
||||
public SettingsManager()
|
||||
{
|
||||
_settings = new Settings();
|
||||
|
||||
// Appearance group
|
||||
var theme = new ChoiceSetSetting("theme", "Theme", "UI theme",
|
||||
[
|
||||
new ChoiceSetSetting.Choice("Light", "light"),
|
||||
new ChoiceSetSetting.Choice("Dark", "dark"),
|
||||
new ChoiceSetSetting.Choice("System", "system"),
|
||||
],
|
||||
"system");
|
||||
|
||||
var fontSize = new TextSetting("fontSize", "Font Size", "Display font size", "14");
|
||||
|
||||
// Behavior group
|
||||
var autoRefresh = new ToggleSetting("autoRefresh", "Auto-Refresh",
|
||||
"Automatically refresh results", true);
|
||||
|
||||
var refreshInterval = new TextSetting("refreshInterval", "Refresh Interval",
|
||||
"Seconds between auto-refreshes", "30");
|
||||
|
||||
_settings.AddSetting(theme);
|
||||
_settings.AddSetting(fontSize);
|
||||
_settings.AddSetting(autoRefresh);
|
||||
_settings.AddSetting(refreshInterval);
|
||||
}
|
||||
```
|
||||
|
||||
## Reacting to Changes
|
||||
|
||||
Use the `SettingsChanged` event to update behavior when the user modifies settings:
|
||||
|
||||
```csharp
|
||||
private void OnSettingsChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// Invalidate cached data
|
||||
_cachedItems = null;
|
||||
|
||||
// Notify pages to refresh
|
||||
OnItemsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [SampleSettingsPage.cs](https://github.com/microsoft/PowerToys/blob/main/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleSettingsPage.cs)
|
||||
@@ -0,0 +1,164 @@
|
||||
---
|
||||
name: add-fallback-commands
|
||||
description: >-
|
||||
Add fallback commands to your Command Palette extension for catch-all search behavior.
|
||||
Use when asked to add search functionality, query matching, direct input handling,
|
||||
calculator-style evaluation, URL opening, command execution, or results that appear
|
||||
when no other extension matches. Used by 14 of 20 built-in extensions.
|
||||
---
|
||||
|
||||
# Add Fallback Commands
|
||||
|
||||
Fallback commands are shown in Command Palette when no other results match the user's query. They enable your extension to act as a catch-all handler — perfect for calculators, web search, command execution, file path opening, and more.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Adding search functionality that responds to any user input
|
||||
- Creating a calculator that evaluates expressions as the user types
|
||||
- Building a web search that triggers on unmatched queries
|
||||
- Opening files or URLs typed directly into the palette
|
||||
- Executing shell commands from the search bar
|
||||
|
||||
## How Fallback Commands Work
|
||||
|
||||
1. User types a query in Command Palette
|
||||
2. If no top-level commands match, CmdPal asks extensions for fallback results
|
||||
3. Your extension's `FallbackCommands()` provides items that respond to the query
|
||||
4. The fallback items can be static (always shown) or dynamic (filtered by query)
|
||||
|
||||
## Quick Start: Static Fallback
|
||||
|
||||
Override `FallbackCommands()` in your `CommandProvider`:
|
||||
|
||||
```csharp
|
||||
public partial class MyCommandsProvider : CommandProvider
|
||||
{
|
||||
private readonly ICommandItem[] _commands;
|
||||
private readonly FallbackCommandItem[] _fallbacks;
|
||||
|
||||
public MyCommandsProvider()
|
||||
{
|
||||
DisplayName = "Web Search";
|
||||
Icon = new IconInfo("\uE721"); // Search icon
|
||||
|
||||
var searchPage = new WebSearchPage();
|
||||
_commands = [new CommandItem(searchPage) { Title = DisplayName }];
|
||||
_fallbacks = [new FallbackCommandItem(searchPage) { Title = "Search the web" }];
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => _commands;
|
||||
public override IFallbackCommandItem[] FallbackCommands() => _fallbacks;
|
||||
}
|
||||
```
|
||||
|
||||
## Dynamic Fallback with DynamicListPage
|
||||
|
||||
For fallbacks that filter results based on the query, use `DynamicListPage`:
|
||||
|
||||
```csharp
|
||||
internal sealed partial class WebSearchPage : DynamicListPage
|
||||
{
|
||||
private string _query = string.Empty;
|
||||
|
||||
public WebSearchPage()
|
||||
{
|
||||
Icon = new IconInfo("\uE721");
|
||||
Title = "Web Search";
|
||||
Name = "Search";
|
||||
PlaceholderText = "Type to search...";
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
_query = newSearch;
|
||||
RaiseItemsChanged();
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_query))
|
||||
return [];
|
||||
|
||||
return [
|
||||
new ListItem(new OpenUrlCommand($"https://www.google.com/search?q={Uri.EscapeDataString(_query)}"))
|
||||
{
|
||||
Title = $"Search Google for \"{_query}\"",
|
||||
Icon = new IconInfo("\uE721"),
|
||||
},
|
||||
new ListItem(new OpenUrlCommand($"https://www.bing.com/search?q={Uri.EscapeDataString(_query)}"))
|
||||
{
|
||||
Title = $"Search Bing for \"{_query}\"",
|
||||
Icon = new IconInfo("\uE721"),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Responsive Fallback with Cancellation
|
||||
|
||||
For expensive operations (API calls, file searches), use cancellation to stay responsive:
|
||||
|
||||
```csharp
|
||||
internal sealed partial class SmartSearchPage : DynamicListPage
|
||||
{
|
||||
private CancellationTokenSource? _cts;
|
||||
private IListItem[] _results = [];
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
// Cancel any in-flight search
|
||||
_cts?.Cancel();
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Debounce: wait for user to stop typing
|
||||
await Task.Delay(300, token);
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
// Perform search
|
||||
_results = await SearchAsync(newSearch, token);
|
||||
RaiseItemsChanged();
|
||||
}, token);
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _results;
|
||||
|
||||
private async Task<IListItem[]> SearchAsync(string query, CancellationToken token)
|
||||
{
|
||||
// Your search logic here
|
||||
// Check token.IsCancellationRequested periodically
|
||||
return [];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Real-World Examples (from built-in extensions)
|
||||
|
||||
| Extension | Fallback Behavior |
|
||||
|-----------|------------------|
|
||||
| **Apps** | Search installed applications by name |
|
||||
| **Calc** | Evaluate mathematical expressions directly |
|
||||
| **Shell** | Execute command-line commands |
|
||||
| **WebSearch** | Search the web with configured engine |
|
||||
| **Indexer** | Open files by path |
|
||||
| **TimeDate** | Parse time/date queries |
|
||||
| **WindowsSettings** | Jump to Windows Settings pages |
|
||||
| **WinGet** | Search WinGet packages |
|
||||
| **WindowWalker** | Find and switch to open windows |
|
||||
|
||||
## Key Points
|
||||
|
||||
- `FallbackCommands()` returns `IFallbackCommandItem[]` (not `ICommandItem[]`)
|
||||
- Use `FallbackCommandItem` wrapper (not `CommandItem`)
|
||||
- Wrap a `DynamicListPage` for query-reactive results
|
||||
- Cancel previous searches when new input arrives
|
||||
- Keep fallback responses fast — users expect instant results
|
||||
- Use `PlaceholderText` on your page to guide users
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Extension samples](https://learn.microsoft.com/windows/powertoys/command-palette/samples)
|
||||
- [Extensibility overview](https://learn.microsoft.com/windows/powertoys/command-palette/extensibility-overview)
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: publish-extension
|
||||
description: >-
|
||||
Publish your Command Palette extension to the Microsoft Store or WinGet.
|
||||
Use when asked to publish, distribute, release, deploy to store,
|
||||
create MSIX packages, submit to WinGet, set up CI/CD for releases,
|
||||
or automate builds with GitHub Actions.
|
||||
---
|
||||
|
||||
# Publish Your Command Palette Extension
|
||||
|
||||
Guide for distributing your Command Palette extension through the Microsoft Store, WinGet, or both.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Publishing your extension to the Microsoft Store
|
||||
- Submitting your extension to WinGet for `winget install` discovery
|
||||
- Setting up GitHub Actions to automate builds and releases
|
||||
- Creating MSIX packages for Store submission
|
||||
- Creating EXE installers for WinGet submission
|
||||
|
||||
## Publishing Options
|
||||
|
||||
| Channel | Package Format | Discovery | Auto-Updates |
|
||||
|---------|---------------|-----------|--------------|
|
||||
| Microsoft Store | MSIX bundle | Store app, `ms-windows-store://` link | Yes |
|
||||
| WinGet | EXE installer | `winget install`, CmdPal browse | Yes (via manifest) |
|
||||
|
||||
**Recommendation**: Publish to both for maximum reach. WinGet enables direct discovery from within Command Palette.
|
||||
|
||||
## Workflows
|
||||
|
||||
### Microsoft Store Publishing
|
||||
See [store-publishing.md](references/store-publishing.md) for the complete step-by-step guide.
|
||||
|
||||
**Summary:**
|
||||
1. Register for Partner Center
|
||||
2. Update `Package.appxmanifest` and `.csproj` with Partner Center identity
|
||||
3. Build MSIX for x64 and ARM64
|
||||
4. Create MSIX bundle
|
||||
5. Submit to Partner Center
|
||||
|
||||
### WinGet Publishing
|
||||
See [winget-publishing.md](references/winget-publishing.md) for the complete step-by-step guide.
|
||||
|
||||
**Summary:**
|
||||
1. Switch project to unpackaged mode
|
||||
2. Create Inno Setup installer script
|
||||
3. Build EXE installers
|
||||
4. Submit manifest via `wingetcreate new`
|
||||
5. Optionally automate with GitHub Actions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Visual Studio](https://visualstudio.microsoft.com/) with C# and WinUI workloads
|
||||
- [Partner Center account](https://partner.microsoft.com/dashboard/home) (for Store publishing)
|
||||
- [GitHub CLI](https://cli.github.com/) (for WinGet publishing)
|
||||
- [WingetCreate](https://github.com/microsoft/winget-create) — `winget install Microsoft.WingetCreate`
|
||||
- [Inno Setup](https://jrsoftware.org/isdl.php) (for WinGet EXE packaging)
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Your extension's CLSID (the `[Guid("...")]` in your main .cs file) must be unique and consistent across all files
|
||||
- WinGet manifests must include the `windows-commandpalette-extension` tag for CmdPal discovery
|
||||
- MSIX packages require both x64 and ARM64 builds for Store submission
|
||||
- WindowsAppSdk must be listed as a dependency in WinGet manifests
|
||||
@@ -0,0 +1,169 @@
|
||||
# Microsoft Store Publishing Guide
|
||||
|
||||
Complete step-by-step guide for publishing your Command Palette extension to the Microsoft Store.
|
||||
|
||||
## Step 1: Set Up Microsoft Store
|
||||
|
||||
1. Go to [Partner Center](https://partner.microsoft.com/dashboard/home)
|
||||
2. Navigate to **Apps and Games** → **New product** → **MSIX or PWA app**
|
||||
3. Reserve your app name (e.g., `My Extension for Command Palette`)
|
||||
4. Once created, go to **Product Management** → **Product Identity**
|
||||
5. Copy these three values — you'll need them in the next step:
|
||||
|
||||
| Partner Center Field | Where It Goes |
|
||||
|---------------------|---------------|
|
||||
| **Package/Identity/Name** | `Package.appxmanifest` → `Identity Name` and `.csproj` → `AppxPackageIdentityName` |
|
||||
| **Package/Identity/Publisher** | `Package.appxmanifest` → `Identity Publisher` and `.csproj` → `AppxPackagePublisher` |
|
||||
| **Package/Properties/PublisherDisplayName** | `Package.appxmanifest` → `Properties PublisherDisplayName` |
|
||||
|
||||
## Step 2: Prepare the Extension
|
||||
|
||||
### Update `Package.appxmanifest`
|
||||
|
||||
Replace the placeholder identity values with your Partner Center values:
|
||||
|
||||
```xml
|
||||
<Identity
|
||||
Name="YOUR_PACKAGE_IDENTITY_NAME_HERE"
|
||||
Publisher="YOUR_PACKAGE_IDENTITY_PUBLISHER_HERE"
|
||||
Version="0.0.1.0" />
|
||||
```
|
||||
|
||||
And update the publisher display name:
|
||||
|
||||
```xml
|
||||
<Properties>
|
||||
<DisplayName>Your Extension Name</DisplayName>
|
||||
<PublisherDisplayName>YOUR_PUBLISHER_DISPLAY_NAME_HERE</PublisherDisplayName>
|
||||
<!-- ... -->
|
||||
</Properties>
|
||||
```
|
||||
|
||||
### Update `.csproj`
|
||||
|
||||
Add or update the following properties in your `.csproj` file:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<AppxPackageIdentityName>YOUR_PACKAGE_IDENTITY_NAME_HERE</AppxPackageIdentityName>
|
||||
<AppxPackagePublisher>YOUR_PACKAGE_IDENTITY_PUBLISHER_HERE</AppxPackagePublisher>
|
||||
<AppxPackageVersion>0.0.1.0</AppxPackageVersion>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Update Image Assets ItemGroup
|
||||
|
||||
Ensure all image assets are included in the package by updating the `ItemGroup`:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\**\*.png" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
> **Tip:** The `Assets` folder should contain your Store logos and extension icons at the required sizes (44x44, 150x150, etc.). You can generate these from a single high-resolution image.
|
||||
|
||||
## Step 3: Build MSIX Packages
|
||||
|
||||
Build for both x64 and ARM64 architectures:
|
||||
|
||||
```powershell
|
||||
# x64 build
|
||||
dotnet build --configuration Release -p:GenerateAppxPackageOnBuild=true -p:Platform=x64 -p:AppxPackageDir="AppPackages\x64\"
|
||||
|
||||
# ARM64 build
|
||||
dotnet build --configuration Release -p:GenerateAppxPackageOnBuild=true -p:Platform=ARM64 -p:AppxPackageDir="AppPackages\ARM64\"
|
||||
```
|
||||
|
||||
Verify the MSIX files were created:
|
||||
|
||||
```powershell
|
||||
dir AppPackages -Recurse -Filter "*.msix"
|
||||
```
|
||||
|
||||
You should see two `.msix` files, one for each architecture.
|
||||
|
||||
## Step 4: Create MSIX Bundle
|
||||
|
||||
### Create the bundle mapping file
|
||||
|
||||
Create a file named `bundle_mapping.txt` that maps each MSIX to its architecture:
|
||||
|
||||
```text
|
||||
[Files]
|
||||
"AppPackages\x64\YourExtension_0.0.1.0_x64\YourExtension_0.0.1.0_x64.msix" "YourExtension_0.0.1.0_x64.msix"
|
||||
"AppPackages\ARM64\YourExtension_0.0.1.0_ARM64\YourExtension_0.0.1.0_ARM64.msix" "YourExtension_0.0.1.0_ARM64.msix"
|
||||
```
|
||||
|
||||
> **Note:** Update the paths and filenames to match your actual build output. Check the `AppPackages` directory structure after building.
|
||||
|
||||
### Run makeappx
|
||||
|
||||
```powershell
|
||||
makeappx bundle /f bundle_mapping.txt /p YourExtension_0.0.1.0_Bundle.msixbundle
|
||||
```
|
||||
|
||||
> **Tip:** `makeappx.exe` is included with the Windows SDK. If it's not in your PATH, find it at:
|
||||
> `C:\Program Files (x86)\Windows Kits\10\bin\<version>\x64\makeappx.exe`
|
||||
|
||||
## Step 5: Submit to Partner Center
|
||||
|
||||
1. Go to [Partner Center](https://partner.microsoft.com/dashboard/home)
|
||||
2. Navigate to your app → **Start a new submission**
|
||||
3. In **Packages**, upload your `.msixbundle` file
|
||||
4. In **Store Listings** → **Description**, include a note like:
|
||||
|
||||
> `YourExtension` integrates with the Windows Command Palette to provide [describe your extension's functionality]. Requires PowerToys with Command Palette enabled.
|
||||
|
||||
5. In **Notes for certification**, add testing instructions:
|
||||
|
||||
> This extension requires Microsoft PowerToys (available from the Microsoft Store or https://github.com/microsoft/PowerToys) with the Command Palette feature enabled. To test:
|
||||
> 1. Install PowerToys and enable Command Palette
|
||||
> 2. Install this extension
|
||||
> 3. Open Command Palette (Win+Alt+Space by default)
|
||||
> 4. Search for [your extension's commands]
|
||||
|
||||
6. Set **Availability** and pricing as appropriate
|
||||
7. Click **Submit for certification**
|
||||
|
||||
Certification typically takes 1–3 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.
|
||||
@@ -0,0 +1,413 @@
|
||||
# WinGet Publishing Guide
|
||||
|
||||
Complete step-by-step guide for publishing your Command Palette extension to WinGet for `winget install` discovery and installation.
|
||||
|
||||
## Why WinGet?
|
||||
|
||||
Publishing to WinGet enables:
|
||||
|
||||
- Users to install via `winget install YourPublisher.YourExtension`
|
||||
- Discovery directly inside Command Palette's built-in browse experience
|
||||
- Automatic update detection via WinGet manifests
|
||||
|
||||
## Step 1: Prepare the Project for Unpackaged Distribution
|
||||
|
||||
WinGet distribution uses an unpackaged (EXE-based) build instead of MSIX.
|
||||
|
||||
### Update `.csproj`
|
||||
|
||||
Remove any existing `<PublishProfile>` property and add unpackaged mode:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<!-- Remove or comment out this line if present: -->
|
||||
<!-- <PublishProfile>win-$(Platform)</PublishProfile> -->
|
||||
|
||||
<!-- Add this for unpackaged distribution: -->
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Note Your CLSID
|
||||
|
||||
Find the `[Guid("...")]` attribute in your main `.cs` file (e.g., `SampleExtension.cs`):
|
||||
|
||||
```csharp
|
||||
[Guid("YOUR-GUID-HERE")]
|
||||
public sealed partial class SampleExtension : IExtension
|
||||
```
|
||||
|
||||
You'll need this exact GUID for the installer script. It must match across all files.
|
||||
|
||||
## Step 2: Create Installer Scripts
|
||||
|
||||
### Inno Setup Script: `setup-template.iss`
|
||||
|
||||
Create this file in your project root. Replace all `TODO` placeholders with your values:
|
||||
|
||||
```iss
|
||||
; Inno Setup script for Command Palette extension
|
||||
|
||||
#define MyAppName "TODO_YOUR_EXTENSION_NAME"
|
||||
#define MyAppVersion "TODO_YOUR_VERSION"
|
||||
#define MyAppPublisher "TODO_YOUR_PUBLISHER_NAME"
|
||||
#define MyAppURL "TODO_YOUR_PROJECT_URL"
|
||||
#define MyAppCLSID "TODO_YOUR_CLSID_WITH_BRACES"
|
||||
; Example CLSID: {12345678-1234-1234-1234-123456789012}
|
||||
|
||||
[Setup]
|
||||
AppId={#MyAppCLSID}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
OutputBaseFilename={#MyAppName}_{#MyAppVersion}_{#SetupSetting("ArchitecturesAllowed")}
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
PrivilegesRequired=lowest
|
||||
OutputDir=Installer
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Files]
|
||||
Source: "publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Registry]
|
||||
; Register the COM server for Command Palette discovery
|
||||
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppName}"; Flags: uninsdeletekey
|
||||
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}\InprocServer32"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppName}.dll"; Flags: uninsdeletekey
|
||||
Root: HKCU; Subkey: "Software\Classes\CLSID\{#MyAppCLSID}\InprocServer32"; ValueType: string; ValueName: "ThreadingModel"; ValueData: "Both"; Flags: uninsdeletekey
|
||||
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{app}"
|
||||
```
|
||||
|
||||
> **Important:** The `AppId` must use your CLSID wrapped in braces. The registry entries register your extension's COM server so Command Palette can discover it.
|
||||
|
||||
### Build Script: `build-exe.ps1`
|
||||
|
||||
Create this PowerShell script in your project root:
|
||||
|
||||
```powershell
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds EXE installers for x64 and ARM64 using dotnet publish and Inno Setup.
|
||||
.DESCRIPTION
|
||||
Publishes the project for both architectures, then runs Inno Setup to create
|
||||
EXE installers suitable for WinGet submission.
|
||||
#>
|
||||
|
||||
param(
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Version = "0.0.1"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$projectName = (Get-ChildItem -Filter "*.csproj" | Select-Object -First 1).BaseName
|
||||
if (-not $projectName) {
|
||||
Write-Error "No .csproj file found in the current directory."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$architectures = @("x64", "arm64")
|
||||
|
||||
foreach ($arch in $architectures) {
|
||||
Write-Host "`n=== Building $arch ===" -ForegroundColor Cyan
|
||||
|
||||
# Publish
|
||||
Write-Host "Publishing for $arch..."
|
||||
dotnet publish -c $Configuration -r "win-$arch" -o "publish" --self-contained=false
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "dotnet publish failed for $arch"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create installer
|
||||
Write-Host "Creating installer for $arch..."
|
||||
$issFile = "setup-template.iss"
|
||||
if (-not (Test-Path $issFile)) {
|
||||
Write-Error "Inno Setup script not found: $issFile"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$archFlag = if ($arch -eq "arm64") { "arm64" } else { "x64" }
|
||||
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
|
||||
/DMyAppVersion="$Version" `
|
||||
/DArchitecturesAllowed="$archFlag" `
|
||||
$issFile
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Inno Setup failed for $arch"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Clean publish directory for next architecture
|
||||
Remove-Item -Recurse -Force "publish" -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "=== $arch complete ===" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "`nInstallers created in the 'Installer' directory:" -ForegroundColor Cyan
|
||||
Get-ChildItem -Path "Installer" -Filter "*.exe" | ForEach-Object { Write-Host " $_" }
|
||||
```
|
||||
|
||||
## Step 3: Build EXE Installers
|
||||
|
||||
Run the build script from your project directory:
|
||||
|
||||
```powershell
|
||||
.\build-exe.ps1
|
||||
```
|
||||
|
||||
This produces two EXE files in the `Installer` directory:
|
||||
|
||||
```
|
||||
Installer\YourExtension_0.0.1_x64.exe
|
||||
Installer\YourExtension_0.0.1_arm64.exe
|
||||
```
|
||||
|
||||
Verify both installers work by running them locally and confirming your extension appears in Command Palette.
|
||||
|
||||
## Step 4: Create a GitHub Release
|
||||
|
||||
Tag your repository with the version and create a release with the EXE files:
|
||||
|
||||
```powershell
|
||||
# Tag the release
|
||||
git tag -a v0.0.1 -m "Release v0.0.1"
|
||||
git push origin v0.0.1
|
||||
|
||||
# Create release and upload assets (requires GitHub CLI)
|
||||
gh release create v0.0.1 `
|
||||
"Installer\YourExtension_0.0.1_x64.exe" `
|
||||
"Installer\YourExtension_0.0.1_arm64.exe" `
|
||||
--title "v0.0.1" `
|
||||
--notes "Initial release of YourExtension for Command Palette."
|
||||
```
|
||||
|
||||
After creating the release, copy the download URLs for both EXE files — you'll need them for the WinGet submission.
|
||||
|
||||
## Step 5: Submit to WinGet
|
||||
|
||||
Use `wingetcreate` to generate a WinGet manifest and submit a pull request:
|
||||
|
||||
```powershell
|
||||
wingetcreate new "<URL_TO_x64.exe>" "<URL_TO_arm64.exe>"
|
||||
```
|
||||
|
||||
`wingetcreate` will interactively prompt you for:
|
||||
|
||||
| Prompt | Example Value |
|
||||
|--------|---------------|
|
||||
| **PackageIdentifier** | `YourPublisher.YourExtension` |
|
||||
| **PackageVersion** | `0.0.1` |
|
||||
| **PackageLocale** | `en-US` |
|
||||
| **Publisher** | `Your Name` |
|
||||
| **PackageName** | `YourExtension for Command Palette` |
|
||||
| **License** | `MIT` |
|
||||
| **ShortDescription** | `A Command Palette extension that does X` |
|
||||
|
||||
After answering all prompts, `wingetcreate` will create a PR against the [winget-pkgs](https://github.com/microsoft/winget-pkgs) repository.
|
||||
|
||||
## Step 6: Add the Command Palette Tag (CRITICAL)
|
||||
|
||||
> **This step is required for your extension to appear in Command Palette's browse experience.**
|
||||
|
||||
After `wingetcreate` generates the manifest files, you **must** edit each `.locale.*.yaml` file to add the Command Palette tag.
|
||||
|
||||
In every locale YAML file (e.g., `YourPublisher.YourExtension.locale.en-US.yaml`), add:
|
||||
|
||||
```yaml
|
||||
Tags:
|
||||
- windows-commandpalette-extension
|
||||
```
|
||||
|
||||
Example of a complete locale file with the tag:
|
||||
|
||||
```yaml
|
||||
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
|
||||
PackageIdentifier: YourPublisher.YourExtension
|
||||
PackageVersion: 0.0.1
|
||||
PackageLocale: en-US
|
||||
Publisher: Your Name
|
||||
PackageName: YourExtension for Command Palette
|
||||
License: MIT
|
||||
ShortDescription: A Command Palette extension that does X
|
||||
Tags:
|
||||
- windows-commandpalette-extension
|
||||
ManifestType: defaultLocale
|
||||
ManifestVersion: 1.6.0
|
||||
```
|
||||
|
||||
Without this tag, Command Palette will not discover your extension in its browse experience.
|
||||
|
||||
## Step 7: Ensure WindowsAppSdk Dependency
|
||||
|
||||
Your WinGet manifest must declare a dependency on the Windows App SDK so it gets installed automatically. In the `installer.yaml` manifest file, add:
|
||||
|
||||
```yaml
|
||||
Dependencies:
|
||||
PackageDependencies:
|
||||
- PackageIdentifier: Microsoft.WindowsAppRuntime.1.7
|
||||
MinimumVersion: 7001.632.252.0
|
||||
```
|
||||
|
||||
> **Note:** Update the version number to match the Windows App SDK version your project targets. Check your `.csproj` for the `WindowsAppSDK` package version.
|
||||
|
||||
## Step 8: GitHub Actions Automation (Optional)
|
||||
|
||||
Automate your build, release, and WinGet submission process with GitHub Actions.
|
||||
|
||||
### Release Workflow: `.github/workflows/release-extension.yml`
|
||||
|
||||
```yaml
|
||||
name: Release Extension
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
PROJECT_NAME: YourExtension
|
||||
DOTNET_VERSION: '9.0.x'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [x64, arm64]
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Install Inno Setup
|
||||
run: choco install innosetup -y --no-progress
|
||||
|
||||
- name: Detect version
|
||||
id: version
|
||||
run: |
|
||||
$tag = "${{ github.ref_name }}" -replace '^v', ''
|
||||
echo "VERSION=$tag" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
dotnet publish -c Release -r win-${{ matrix.arch }} -o publish --self-contained=false
|
||||
|
||||
- name: Create installer
|
||||
run: |
|
||||
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" `
|
||||
/DMyAppVersion="${{ steps.version.outputs.VERSION }}" `
|
||||
/DArchitecturesAllowed="${{ matrix.arch }}" `
|
||||
setup-template.iss
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: installer-${{ matrix.arch }}
|
||||
path: Installer/*.exe
|
||||
|
||||
release:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: artifacts/*.exe
|
||||
generate_release_notes: true
|
||||
|
||||
winget-update:
|
||||
needs: release
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Detect version
|
||||
id: version
|
||||
run: |
|
||||
$tag = "${{ github.ref_name }}" -replace '^v', ''
|
||||
echo "VERSION=$tag" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Update WinGet manifest
|
||||
run: |
|
||||
$baseUrl = "https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}"
|
||||
wingetcreate update YourPublisher.YourExtension `
|
||||
--version ${{ steps.version.outputs.VERSION }} `
|
||||
--urls "$baseUrl/${{ env.PROJECT_NAME }}_${{ steps.version.outputs.VERSION }}_x64.exe" "$baseUrl/${{ env.PROJECT_NAME }}_${{ steps.version.outputs.VERSION }}_arm64.exe" `
|
||||
--submit `
|
||||
--token ${{ secrets.WINGET_PAT }}
|
||||
```
|
||||
|
||||
### Required Secrets
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `WINGET_PAT` | GitHub Personal Access Token with `public_repo` scope, used by `wingetcreate` to submit PRs to `microsoft/winget-pkgs` |
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Push a version tag** (e.g., `git tag v0.0.2 && git push origin v0.0.2`)
|
||||
2. **Build job** runs in parallel for x64 and ARM64, creating EXE installers
|
||||
3. **Release job** creates a GitHub Release and uploads the EXE files
|
||||
4. **WinGet update job** automatically submits an updated manifest to `winget-pkgs`
|
||||
|
||||
> **Note:** The `winget-update` job uses `wingetcreate update` (not `new`) because it assumes you've already submitted your initial manifest manually. For the first submission, follow Steps 5–7 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 |
|
||||
@@ -12,7 +12,7 @@ public struct InterlockedBoolean(bool initialValue = false)
|
||||
private int _value = initialValue ? 1 : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the boolean value atomically
|
||||
/// Gets or sets a value indicating whether the atomic boolean is true.
|
||||
/// </summary>
|
||||
public bool Value
|
||||
{
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MEL = Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="ILogger"/> implementation that delegates to <see cref="ManagedCommon.Logger"/>.
|
||||
/// Instances are created by <see cref="CmdPalLoggerProvider"/>.
|
||||
/// </summary>
|
||||
public sealed class CmdPalLogger(string categoryName) : MEL.ILogger
|
||||
{
|
||||
public IDisposable? BeginScope<TState>(TState state)
|
||||
where TState : notnull => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel,
|
||||
EventId eventId,
|
||||
TState state,
|
||||
Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArgumentNullException.ThrowIfNull(formatter);
|
||||
|
||||
var message = $"[{categoryName}] {formatter(state, exception)}";
|
||||
|
||||
switch (logLevel)
|
||||
{
|
||||
case LogLevel.Trace:
|
||||
ManagedCommon.Logger.LogTrace(message);
|
||||
break;
|
||||
case LogLevel.Debug:
|
||||
ManagedCommon.Logger.LogDebug(message);
|
||||
break;
|
||||
case LogLevel.Information:
|
||||
ManagedCommon.Logger.LogInfo(message);
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
ManagedCommon.Logger.LogWarning(message);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
case LogLevel.Critical:
|
||||
if (exception is not null)
|
||||
{
|
||||
ManagedCommon.Logger.LogError(message, exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
ManagedCommon.Logger.LogError(message);
|
||||
}
|
||||
|
||||
break;
|
||||
case LogLevel.None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using MEL = Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="MEL.ILoggerProvider"/> that creates <see cref="CmdPalLogger"/> instances
|
||||
/// backed by the <see cref="ManagedCommon.Logger"/> infrastructure.
|
||||
/// Register via <see cref="CmdPalLoggingExtensions.AddCmdPalLogging"/>.
|
||||
/// </summary>
|
||||
public sealed partial class CmdPalLoggerProvider : MEL.ILoggerProvider
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CmdPalLogger> _loggers = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public MEL.ILogger CreateLogger(string categoryName) =>
|
||||
_loggers.GetOrAdd(categoryName, name => new CmdPalLogger(name));
|
||||
|
||||
public void Dispose() => _loggers.Clear();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using MEL = Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Logging;
|
||||
|
||||
public static class CmdPalLoggingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the Microsoft.Extensions.Logging infrastructure and adds a
|
||||
/// <see cref="CmdPalLoggerProvider"/> that routes all <see cref="MEL.ILogger"/>
|
||||
/// output to <see cref="ManagedCommon.Logger"/>.
|
||||
/// </summary>
|
||||
public static IServiceCollection AddCmdPalLogging(this IServiceCollection services)
|
||||
{
|
||||
services.AddLogging(builder =>
|
||||
{
|
||||
builder.ClearProviders();
|
||||
builder.Services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<MEL.ILoggerProvider, CmdPalLoggerProvider>());
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -60,4 +61,8 @@
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Logging\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,6 +8,6 @@ public sealed class PinyinFuzzyMatcherOptions
|
||||
{
|
||||
public PinyinMode Mode { get; init; } = PinyinMode.AutoSimplifiedChineseUi;
|
||||
|
||||
/// <summary>Remove IME syllable separators (') for query secondary variant.</summary>
|
||||
/// <summary>Gets a value indicating whether IME syllable separators (') are removed for query secondary variant.</summary>
|
||||
public bool RemoveApostrophesForQuery { get; init; } = true;
|
||||
}
|
||||
|
||||
@@ -341,25 +341,25 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the backdrop opacity slider should be visible.
|
||||
/// Gets a value indicating whether the backdrop opacity slider should be visible.
|
||||
/// </summary>
|
||||
public bool IsBackdropOpacityVisible =>
|
||||
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the backdrop description (for styles without options) should be visible.
|
||||
/// Gets a value indicating whether the backdrop description (for styles without options) should be visible.
|
||||
/// </summary>
|
||||
public bool IsMicaBackdropDescriptionVisible =>
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsOpacity;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether background/colorization settings are available.
|
||||
/// Gets a value indicating whether background/colorization settings are available.
|
||||
/// </summary>
|
||||
public bool IsBackgroundSettingsEnabled =>
|
||||
BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
|
||||
/// Gets a value indicating whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
|
||||
/// </summary>
|
||||
public bool IsBackgroundNotAvailableVisible =>
|
||||
!BackdropStyles.Get(_settingsService.Settings.BackdropStyle).SupportsColorization;
|
||||
|
||||
Binary file not shown.
@@ -36,17 +36,17 @@ public sealed record BackdropStyleConfig
|
||||
public float FixedOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this backdrop style supports custom colorization (tint colors).
|
||||
/// Gets a value indicating whether this backdrop style supports custom colorization (tint colors).
|
||||
/// </summary>
|
||||
public bool SupportsColorization { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this backdrop style supports custom background images.
|
||||
/// Gets a value indicating whether this backdrop style supports custom background images.
|
||||
/// </summary>
|
||||
public bool SupportsBackgroundImage { get; init; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether this backdrop style supports opacity adjustment.
|
||||
/// Gets a value indicating whether this backdrop style supports opacity adjustment.
|
||||
/// </summary>
|
||||
public bool SupportsOpacity { get; init; } = true;
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ internal sealed class ExtensionTemplateService : IExtensionTemplateService
|
||||
|
||||
private static readonly HashSet<string> _copyAsIsTemplateExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".md",
|
||||
".png",
|
||||
};
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ public sealed class ThemeSnapshot
|
||||
public required float BackgroundBrightness { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether colorization is active (accent color, custom color, or image mode).
|
||||
/// Gets a value indicating whether colorization is active (accent color, custom color, or image mode).
|
||||
/// </summary>
|
||||
public required bool HasColorization { get; init; }
|
||||
}
|
||||
|
||||
@@ -93,19 +93,19 @@ public record DockBandSettings
|
||||
public required string CommandId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether titles are shown for items in this band.
|
||||
/// Gets whether titles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowTitles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether subtitles are shown for items in this band.
|
||||
/// Gets whether subtitles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowSubtitles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
|
||||
/// Gets a value for backward compatibility. Maps to ShowTitles.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool? ShowLabels
|
||||
|
||||
@@ -9,37 +9,37 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public sealed class WindowPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets left position in device pixels.
|
||||
/// Gets the left position in device pixels.
|
||||
/// </summary>
|
||||
public int X { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets top position in device pixels.
|
||||
/// Gets the top position in device pixels.
|
||||
/// </summary>
|
||||
public int Y { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets width in device pixels.
|
||||
/// Gets the width in device pixels.
|
||||
/// </summary>
|
||||
public int Width { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets height in device pixels.
|
||||
/// Gets the height in device pixels.
|
||||
/// </summary>
|
||||
public int Height { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets width of the screen in device pixels where the window is located.
|
||||
/// Gets the width of the screen in device pixels where the window is located.
|
||||
/// </summary>
|
||||
public int ScreenWidth { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets height of the screen in device pixels where the window is located.
|
||||
/// Gets the height of the screen in device pixels where the window is located.
|
||||
/// </summary>
|
||||
public int ScreenHeight { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets DPI (dots per inch) of the display where the window is located.
|
||||
/// Gets the DPI (dots per inch) of the display where the window is located.
|
||||
/// </summary>
|
||||
public int Dpi { get; init; }
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Logging;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
@@ -125,6 +126,8 @@ public partial class App : Application, IDisposable
|
||||
services.AddSingleton(TaskScheduler.FromCurrentSynchronizationContext());
|
||||
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
services.AddCmdPalLogging();
|
||||
|
||||
AddBuiltInCommands(services, appInfoService.ConfigDirectory);
|
||||
|
||||
AddCoreServices(services, appInfoService);
|
||||
|
||||
@@ -157,12 +157,6 @@ public sealed partial class SearchBar : UserControl,
|
||||
{
|
||||
// Clear the search box
|
||||
FilterBox.Text = string.Empty;
|
||||
|
||||
// hack TODO GH #245
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -170,14 +164,6 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Back)
|
||||
{
|
||||
// hack TODO GH #245
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
@@ -332,19 +318,6 @@ public sealed partial class SearchBar : UserControl,
|
||||
private void FilterBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
// Logger.LogInfo($"FilterBox_TextChanged: {FilterBox.Text}");
|
||||
|
||||
// TERRIBLE HACK TODO GH #245
|
||||
// There's weird wacky bugs with debounce currently. We're trying
|
||||
// to get them ingested, but while we wait for the toolkit feeds to
|
||||
// bubble, just manually send the first character, always
|
||||
// (otherwise aliases just stop working)
|
||||
if (FilterBox.Text.Length == 1)
|
||||
{
|
||||
DoFilterBoxUpdate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (InSuggestion)
|
||||
{
|
||||
// Logger.LogInfo($"-- skipping, in suggestion --");
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Microsoft.CmdPal.UI.Events;
|
||||
public class CmdPalDockConfiguration : EventBase, IEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether the dock is enabled.
|
||||
/// Gets or sets a value indicating whether the dock is enabled.
|
||||
/// </summary>
|
||||
public bool IsDockEnabled { get; set; }
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public class CmdPalExtensionInvoked : EventBase, IEvent
|
||||
public string CommandName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the command executed successfully.
|
||||
/// Gets or sets a value indicating whether the command executed successfully.
|
||||
/// </summary>
|
||||
public bool Success { get; set; }
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@
|
||||
<tkcontrols:MarkdownThemes
|
||||
x:Key="DefaultMarkdownThemeConfig"
|
||||
H3FontSize="12"
|
||||
H3FontWeight="Normal" />
|
||||
H3FontWeight="Normal"
|
||||
InlineCodeBackground="{StaticResource ControlFillColorDefaultBrush}"
|
||||
InlineCodeBorderBrush="{StaticResource ControlElevationBorderBrush}"
|
||||
InlineCodeCornerRadius="2"
|
||||
InlineCodeForeground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
InlineCodePadding="2,0,2,1" />
|
||||
<markdownImageProviders:ImageProvider x:Key="ImageProvider" />
|
||||
<tkcontrols:MarkdownConfig
|
||||
x:Key="DefaultMarkdownConfig"
|
||||
|
||||
@@ -166,7 +166,12 @@
|
||||
<tkcontrols:MarkdownThemes
|
||||
x:Key="DefaultMarkdownThemeConfig"
|
||||
H3FontSize="12"
|
||||
H3FontWeight="Normal" />
|
||||
H3FontWeight="Normal"
|
||||
InlineCodeBackground="{StaticResource ControlFillColorDefaultBrush}"
|
||||
InlineCodeBorderBrush="{StaticResource ControlElevationBorderBrush}"
|
||||
InlineCodeCornerRadius="2"
|
||||
InlineCodeForeground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
InlineCodePadding="2,0,2,1" />
|
||||
<markdownImageProviders:ImageProvider x:Key="ImageProvider" />
|
||||
<tkcontrols:MarkdownConfig
|
||||
x:Key="DefaultMarkdownConfig"
|
||||
|
||||
@@ -22,7 +22,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
SetSearchBox("files");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Search files");
|
||||
Assert.AreEqual(searchFileItem.Name, "Search files");
|
||||
Assert.AreEqual("Search files", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
|
||||
SetFilesExtensionSearchBox("AppData");
|
||||
@@ -36,7 +36,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
SetSearchBox("calculator");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Calculator");
|
||||
Assert.AreEqual(searchFileItem.Name, "Calculator");
|
||||
Assert.AreEqual("Calculator", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
|
||||
SetCalculatorExtensionSearchBox("1+2");
|
||||
@@ -50,7 +50,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
SetSearchBox("time and date");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Time and date");
|
||||
Assert.AreEqual(searchFileItem.Name, "Time and date");
|
||||
Assert.AreEqual("Time and date", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
|
||||
SetTimeAndDaterExtensionSearchBox("year");
|
||||
@@ -64,7 +64,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
SetSearchBox("Windows Terminal");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Open Windows Terminal profiles");
|
||||
Assert.AreEqual(searchFileItem.Name, "Open Windows Terminal profiles");
|
||||
Assert.AreEqual("Open Windows Terminal profiles", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
|
||||
// SetSearchBox("PowerShell");
|
||||
@@ -77,7 +77,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
SetSearchBox("Windows settings");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Windows settings");
|
||||
Assert.AreEqual(searchFileItem.Name, "Windows settings");
|
||||
Assert.AreEqual("Windows settings", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
|
||||
SetSearchBox("power");
|
||||
@@ -91,7 +91,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
SetSearchBox("Registry");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Registry");
|
||||
Assert.AreEqual(searchFileItem.Name, "Registry");
|
||||
Assert.AreEqual("Registry", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
|
||||
// Type the string will cause strange behavior.so comment it out for now.
|
||||
@@ -105,7 +105,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
SetSearchBox("Windows Services");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Windows Services");
|
||||
Assert.AreEqual(searchFileItem.Name, "Windows Services");
|
||||
Assert.AreEqual("Windows Services", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
|
||||
SetSearchBox("hyper-v");
|
||||
@@ -119,7 +119,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
SetSearchBox("Windows System Commands");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Windows System Commands");
|
||||
Assert.AreEqual(searchFileItem.Name, "Windows System Commands");
|
||||
Assert.AreEqual("Windows System Commands", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
|
||||
SetSearchBox("Sleep");
|
||||
|
||||
@@ -45,7 +45,7 @@ public class IndexerTests : CommandPaletteTestBase
|
||||
SetSearchBox("files");
|
||||
|
||||
var searchFileItem = this.Find<NavigationViewItem>("Search files");
|
||||
Assert.AreEqual(searchFileItem.Name, "Search files");
|
||||
Assert.AreEqual("Search files", searchFileItem.Name);
|
||||
searchFileItem.DoubleClick();
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Parsed search result limit. Returns <see langword="null"/> when the caller should
|
||||
/// Gets the parsed search result limit. Returns <see langword="null"/> when the caller should
|
||||
/// use its own default (unrecognized value, empty, or old stored "0").
|
||||
/// </summary>
|
||||
public int? SearchResultLimit
|
||||
@@ -146,8 +146,7 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
public AllAppsSettings()
|
||||
@@ -162,7 +161,6 @@ public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
|
||||
Settings.Add(_enablePathEnvironmentVariableSource);
|
||||
Settings.Add(_searchResultLimitSource);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
|
||||
@@ -102,8 +102,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
@@ -117,7 +116,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Settings.Add(_copyResultToSearchBarIfQueryEndsWithEqualSign);
|
||||
Settings.Add(_autoFixQuery);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
|
||||
@@ -48,8 +48,7 @@ internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{Namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
@@ -60,7 +59,6 @@ internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
|
||||
Settings.Add(_confirmDelete);
|
||||
Settings.Add(_primaryAction);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (_, _) => SaveSettings();
|
||||
|
||||
@@ -18,18 +18,13 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
|
||||
// Add settings here when needed
|
||||
// Settings.Add(setting);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
|
||||
@@ -36,7 +36,7 @@ internal class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
@@ -45,7 +45,6 @@ internal class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
Settings.Add(_predefinedConnections);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.Shell.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -53,8 +51,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
@@ -64,7 +61,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Settings.Add(_leaveShellOpen);
|
||||
Settings.Add(_shellCommandExecution);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
|
||||
@@ -42,8 +42,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
public bool ShowDialogToConfirmCommand() => _showDialogToConfirmCommand.Value;
|
||||
@@ -65,7 +64,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Settings.Add(_hideEmptyRecycleBin);
|
||||
Settings.Add(_hideDisconnectedNetworkInfo);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
|
||||
@@ -152,8 +152,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
@@ -170,7 +169,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
_customFormats.Placeholder = CUSTOMFORMATPLACEHOLDER;
|
||||
Settings.Add(_customFormats);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
|
||||
@@ -12,6 +12,6 @@ public interface IBrowserInfoService
|
||||
/// <summary>
|
||||
/// Gets information about the system's default web browser.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>The default browser information, or <see langword="null"/> if it could not be determined.</returns>
|
||||
BrowserInfo? GetDefaultBrowser();
|
||||
}
|
||||
|
||||
@@ -85,8 +85,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
private static string HistoryStateJsonPath()
|
||||
|
||||
@@ -99,8 +99,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{Namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
@@ -118,7 +117,6 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
Settings.Add(_inMruOrder);
|
||||
Settings.Add(_useWindowIcon);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (_, _) => SaveSettings();
|
||||
|
||||
@@ -65,8 +65,7 @@ public class SettingsManager : JsonSettingsManager
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
return Path.Combine(directory, $"{_namespace}.settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
@@ -79,7 +78,6 @@ public class SettingsManager : JsonSettingsManager
|
||||
Settings.Add(_saveLastSelectedChannel);
|
||||
Settings.Add(_profileSortOrder);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
|
||||
@@ -28,7 +28,8 @@ public abstract class JsonSettingsManager
|
||||
var filePath = FilePath;
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "The provided settings file does not exist" });
|
||||
// No settings file yet: keep in-memory defaults without persisting.
|
||||
// The file is created on the first user-initiated settings change.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
public static class ShellHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// These are the executable file extensions that Windows Shell recognizes. Unlike CMD/PowerShell,
|
||||
/// Gets the executable file extensions that Windows Shell recognizes. Unlike CMD/PowerShell,
|
||||
/// Shell does not use PATHEXT, but has a magic fixed list.
|
||||
/// </summary>
|
||||
public static string[] ExecutableExtensions { get; } = [".PIF", ".COM", ".EXE", ".BAT", ".CMD"];
|
||||
@@ -246,9 +246,9 @@ public static class ShellHelpers
|
||||
/// <summary>
|
||||
/// Mimics Windows Shell behavior to resolve an executable name to a full path.
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="fullPath"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="name">The name of the executable to resolve.</param>
|
||||
/// <param name="fullPath">When this method returns, contains the full path to the executable if found; otherwise, <see langword="null"/>.</param>
|
||||
/// <returns><see langword="true"/> if the executable was resolved to a full path; otherwise, <see langword="false"/>.</returns>
|
||||
public static bool TryResolveExecutableAsShell(string name, out string fullPath)
|
||||
{
|
||||
// First check if we can find the file in the registry
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
|
||||
using ColorPicker.Helpers;
|
||||
using ManagedCommon;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ColorPicker.UnitTests.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Test class to test <see cref="ColorFormatHelper"/> conversion methods not covered
|
||||
/// by the existing <see cref="ColorConverterTest"/> (which tests HSL and HSV).
|
||||
/// Covers: CMYK, HSB, HSI, HWB, CIE LAB, CIE XYZ, Oklab, Oklch, sRGB-to-linear, NCol.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class ColorFormatConversionTest
|
||||
{
|
||||
[TestMethod]
|
||||
public void ConvertToCMYK_Black_Returns0_0_0_1()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.Cyan);
|
||||
Assert.AreEqual(0d, result.Magenta);
|
||||
Assert.AreEqual(0d, result.Yellow);
|
||||
Assert.AreEqual(1d, result.BlackKey);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCMYK_White_Returns0_0_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(0d, result.Cyan, 0.01);
|
||||
Assert.AreEqual(0d, result.Magenta, 0.01);
|
||||
Assert.AreEqual(0d, result.Yellow, 0.01);
|
||||
Assert.AreEqual(0d, result.BlackKey, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCMYK_Red_Returns0_1_1_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 255, 0, 0));
|
||||
Assert.AreEqual(0d, result.Cyan, 0.01);
|
||||
Assert.AreEqual(1d, result.Magenta, 0.01);
|
||||
Assert.AreEqual(1d, result.Yellow, 0.01);
|
||||
Assert.AreEqual(0d, result.BlackKey, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCMYK_Green_Returns1_0_1_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 0, 255, 0));
|
||||
Assert.AreEqual(1d, result.Cyan, 0.01);
|
||||
Assert.AreEqual(0d, result.Magenta, 0.01);
|
||||
Assert.AreEqual(1d, result.Yellow, 0.01);
|
||||
Assert.AreEqual(0d, result.BlackKey, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCMYK_Blue_Returns1_1_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 0, 0, 255));
|
||||
Assert.AreEqual(1d, result.Cyan, 0.01);
|
||||
Assert.AreEqual(1d, result.Magenta, 0.01);
|
||||
Assert.AreEqual(0d, result.Yellow, 0.01);
|
||||
Assert.AreEqual(0d, result.BlackKey, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCMYK_MidGray_Returns0_0_0_Half()
|
||||
{
|
||||
// RGB(128, 128, 128) should give roughly K ≈ 0.498
|
||||
var result = ColorFormatHelper.ConvertToCMYKColor(Color.FromArgb(255, 128, 128, 128));
|
||||
Assert.AreEqual(0d, result.Cyan, 0.01);
|
||||
Assert.AreEqual(0d, result.Magenta, 0.01);
|
||||
Assert.AreEqual(0d, result.Yellow, 0.01);
|
||||
Assert.AreEqual(128d / 255d, result.BlackKey, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHSB_Black_Returns0_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHSBColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(0d, result.Saturation, 0.01);
|
||||
Assert.AreEqual(0d, result.Brightness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHSB_White_Returns0_0_1()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHSBColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(0d, result.Saturation, 0.01);
|
||||
Assert.AreEqual(1d, result.Brightness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHSB_Red_Returns0_1_1()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHSBColor(Color.FromArgb(255, 255, 0, 0));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(1d, result.Saturation, 0.01);
|
||||
Assert.AreEqual(1d, result.Brightness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHSI_Black_Returns0_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHSIColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(0d, result.Saturation, 0.01);
|
||||
Assert.AreEqual(0d, result.Intensity, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHSI_White_Returns0_0_1()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHSIColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(0d, result.Saturation, 0.01);
|
||||
Assert.AreEqual(1d, result.Intensity, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHSI_Red_Returns0_1_Third()
|
||||
{
|
||||
// Pure red: intensity = (255+0+0)/(3*255) = 1/3
|
||||
var result = ColorFormatHelper.ConvertToHSIColor(Color.FromArgb(255, 255, 0, 0));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(1d, result.Saturation, 0.01);
|
||||
Assert.AreEqual(1d / 3d, result.Intensity, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHWB_Black_Returns0_0_1()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHWBColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(0d, result.Whiteness, 0.01);
|
||||
Assert.AreEqual(1d, result.Blackness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHWB_White_Returns0_1_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHWBColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(1d, result.Whiteness, 0.01);
|
||||
Assert.AreEqual(0d, result.Blackness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHWB_Red_Returns0_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHWBColor(Color.FromArgb(255, 255, 0, 0));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(0d, result.Whiteness, 0.01);
|
||||
Assert.AreEqual(0d, result.Blackness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToHWB_MidGray_Returns0_Half_Half()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToHWBColor(Color.FromArgb(255, 128, 128, 128));
|
||||
Assert.AreEqual(0d, result.Hue, 0.5);
|
||||
Assert.AreEqual(128d / 255d, result.Whiteness, 0.01);
|
||||
Assert.AreEqual(1d - (128d / 255d), result.Blackness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCIEXYZ_Black_Returns0_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToCIEXYZColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.X, 0.001);
|
||||
Assert.AreEqual(0d, result.Y, 0.001);
|
||||
Assert.AreEqual(0d, result.Z, 0.001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCIEXYZ_White_ReturnsD65Illuminant()
|
||||
{
|
||||
// White should be close to D65 illuminant: X≈0.9505, Y≈1.0, Z≈1.089
|
||||
var result = ColorFormatHelper.ConvertToCIEXYZColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(0.9505d, result.X, 0.02);
|
||||
Assert.AreEqual(1.0d, result.Y, 0.02);
|
||||
Assert.AreEqual(1.089d, result.Z, 0.02);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCIEXYZ_Red_HasExpectedValues()
|
||||
{
|
||||
// Pure red: X≈0.4124, Y≈0.2126, Z≈0.0193
|
||||
var result = ColorFormatHelper.ConvertToCIEXYZColor(Color.FromArgb(255, 255, 0, 0));
|
||||
Assert.AreEqual(0.4124d, result.X, 0.02);
|
||||
Assert.AreEqual(0.2126d, result.Y, 0.02);
|
||||
Assert.AreEqual(0.0193d, result.Z, 0.02);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCIELAB_Black_Returns0_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToCIELABColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.Lightness, 0.5);
|
||||
Assert.AreEqual(0d, result.ChromaticityA, 0.5);
|
||||
Assert.AreEqual(0d, result.ChromaticityB, 0.5);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCIELAB_White_Returns100_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToCIELABColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(100d, result.Lightness, 1.0);
|
||||
Assert.AreEqual(0d, result.ChromaticityA, 1.0);
|
||||
Assert.AreEqual(0d, result.ChromaticityB, 1.0);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCIELAB_Red_HasPositiveA()
|
||||
{
|
||||
// Red is in the +a* direction
|
||||
var result = ColorFormatHelper.ConvertToCIELABColor(Color.FromArgb(255, 255, 0, 0));
|
||||
Assert.IsTrue(result.ChromaticityA > 0, "Red should have positive a* in CIE LAB");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToCIELAB_Blue_HasNegativeB()
|
||||
{
|
||||
// Blue is in the -b* direction
|
||||
var result = ColorFormatHelper.ConvertToCIELABColor(Color.FromArgb(255, 0, 0, 255));
|
||||
Assert.IsTrue(result.ChromaticityB < 0, "Blue should have negative b* in CIE LAB");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToOklab_Black_Returns0_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToOklabColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.Lightness, 0.01);
|
||||
Assert.AreEqual(0d, result.ChromaticityA, 0.01);
|
||||
Assert.AreEqual(0d, result.ChromaticityB, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToOklab_White_Returns1_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToOklabColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(1d, result.Lightness, 0.02);
|
||||
Assert.AreEqual(0d, result.ChromaticityA, 0.02);
|
||||
Assert.AreEqual(0d, result.ChromaticityB, 0.02);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToOklch_Black_Returns0_0_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToOklchColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.Lightness, 0.01);
|
||||
Assert.AreEqual(0d, result.Chroma, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToOklch_White_Returns1_0_Any()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToOklchColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(1d, result.Lightness, 0.02);
|
||||
Assert.AreEqual(0d, result.Chroma, 0.02);
|
||||
|
||||
// Hue is undefined for achromatic colors, so we don't assert it
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToOklch_Chroma_IsNonNegative()
|
||||
{
|
||||
// Chroma should always be non-negative
|
||||
var colors = new[] { Color.Red, Color.Green, Color.Blue, Color.Yellow, Color.Cyan, Color.Magenta };
|
||||
foreach (var color in colors)
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToOklchColor(color);
|
||||
Assert.IsTrue(result.Chroma >= 0, $"Chroma should be non-negative for {color.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertSRGBToLinear_Zero_ReturnsZero()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertSRGBToLinearRGB(0, 0, 0);
|
||||
Assert.AreEqual(0d, result.R, 0.001);
|
||||
Assert.AreEqual(0d, result.G, 0.001);
|
||||
Assert.AreEqual(0d, result.B, 0.001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertSRGBToLinear_One_ReturnsOne()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertSRGBToLinearRGB(1, 1, 1);
|
||||
Assert.AreEqual(1d, result.R, 0.001);
|
||||
Assert.AreEqual(1d, result.G, 0.001);
|
||||
Assert.AreEqual(1d, result.B, 0.001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertSRGBToLinear_SmallValues_UsesLinearPath()
|
||||
{
|
||||
// For small values (≤ 0.04045), the formula is linear: value / 12.92
|
||||
var result = ColorFormatHelper.ConvertSRGBToLinearRGB(0.04, 0.04, 0.04);
|
||||
Assert.AreEqual(0.04 / 12.92, result.R, 0.001);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertSRGBToLinear_LargeValues_UsesGammaPath()
|
||||
{
|
||||
// For larger values, the gamma function is applied
|
||||
// sRGB 0.5 should map to ~0.214
|
||||
var result = ColorFormatHelper.ConvertSRGBToLinearRGB(0.5, 0.5, 0.5);
|
||||
Assert.AreEqual(0.214, result.R, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToNaturalColor_Red_ReturnsR0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 255, 0, 0));
|
||||
Assert.AreEqual("R0", result.Hue);
|
||||
Assert.AreEqual(0d, result.Whiteness, 0.01);
|
||||
Assert.AreEqual(0d, result.Blackness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToNaturalColor_Green_HueStartsWithG()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 0, 128, 0));
|
||||
Assert.IsTrue(result.Hue.StartsWith('G'), $"Green should start with G, got {result.Hue}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToNaturalColor_Blue_HueStartsWithB()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 0, 0, 255));
|
||||
Assert.IsTrue(result.Hue.StartsWith('B'), $"Blue should start with B, got {result.Hue}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToNaturalColor_Black_Returns0_0_100()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 0, 0, 0));
|
||||
Assert.AreEqual(0d, result.Whiteness, 0.01);
|
||||
Assert.AreEqual(1d, result.Blackness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertToNaturalColor_White_Returns0_100_0()
|
||||
{
|
||||
var result = ColorFormatHelper.ConvertToNaturalColor(Color.FromArgb(255, 255, 255, 255));
|
||||
Assert.AreEqual(1d, result.Whiteness, 0.01);
|
||||
Assert.AreEqual(0d, result.Blackness, 0.01);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("CMYK", "cmyk(0%, 100%, 100%, 0%)")]
|
||||
[DataRow("HEX", "ff0000")]
|
||||
[DataRow("RGB", "rgb(255, 0, 0)")]
|
||||
[DataRow("HSL", "hsl(0, 100%, 50%)")]
|
||||
[DataRow("HSV", "hsv(0, 100%, 100%)")]
|
||||
[DataRow("HSB", "hsb(0, 100%, 100%)")]
|
||||
[DataRow("HSI", "hsi(0, 100%, 33%)")]
|
||||
[DataRow("HWB", "hwb(0, 0%, 0%)")]
|
||||
[DataRow("Decimal", "255")]
|
||||
[DataRow("HEX Int", "0xFFFF0000")]
|
||||
[DataRow("VEC4", "(1f, 0f, 0f, 1f)")]
|
||||
public void GetStringRepresentation_Red(string type, string expected)
|
||||
{
|
||||
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 255, 0, 0), type, ColorFormatHelper.GetDefaultFormat(type));
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("CMYK", "cmyk(0%, 0%, 0%, 0%)")]
|
||||
[DataRow("HEX", "ffffff")]
|
||||
[DataRow("RGB", "rgb(255, 255, 255)")]
|
||||
[DataRow("HSL", "hsl(0, 0%, 100%)")]
|
||||
[DataRow("HSV", "hsv(0, 0%, 100%)")]
|
||||
[DataRow("Decimal", "16777215")]
|
||||
[DataRow("HEX Int", "0xFFFFFFFF")]
|
||||
[DataRow("VEC4", "(1f, 1f, 1f, 1f)")]
|
||||
public void GetStringRepresentation_White(string type, string expected)
|
||||
{
|
||||
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 255, 255, 255), type, ColorFormatHelper.GetDefaultFormat(type));
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("CMYK", "cmyk(100%, 0%, 100%, 0%)")]
|
||||
[DataRow("HEX", "00ff00")]
|
||||
[DataRow("RGB", "rgb(0, 255, 0)")]
|
||||
[DataRow("HSL", "hsl(120, 100%, 50%)")]
|
||||
[DataRow("HSV", "hsv(120, 100%, 100%)")]
|
||||
[DataRow("Decimal", "65280")]
|
||||
[DataRow("HEX Int", "0xFF00FF00")]
|
||||
public void GetStringRepresentation_Green(string type, string expected)
|
||||
{
|
||||
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 0, 255, 0), type, ColorFormatHelper.GetDefaultFormat(type));
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("CMYK", "cmyk(100%, 100%, 0%, 0%)")]
|
||||
[DataRow("HEX", "0000ff")]
|
||||
[DataRow("RGB", "rgb(0, 0, 255)")]
|
||||
[DataRow("HSL", "hsl(240, 100%, 50%)")]
|
||||
[DataRow("HSV", "hsv(240, 100%, 100%)")]
|
||||
[DataRow("Decimal", "16711680")]
|
||||
[DataRow("HEX Int", "0xFF0000FF")]
|
||||
public void GetStringRepresentation_Blue(string type, string expected)
|
||||
{
|
||||
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 0, 0, 255), type, ColorFormatHelper.GetDefaultFormat(type));
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetStringRepresentation_EmptyFormat_ReturnsHex()
|
||||
{
|
||||
// When colorFormat is null or empty, should return hex
|
||||
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 255, 0, 0), "RGB", string.Empty);
|
||||
Assert.AreEqual("ff0000", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetStringRepresentation_NullFormat_ReturnsHex()
|
||||
{
|
||||
var result = ColorRepresentationHelper.GetStringRepresentation(Color.FromArgb(255, 255, 0, 0), "RGB", null);
|
||||
Assert.AreEqual("ff0000", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("RGB")]
|
||||
[DataRow("HEX")]
|
||||
[DataRow("CMYK")]
|
||||
[DataRow("HSL")]
|
||||
[DataRow("HSV")]
|
||||
[DataRow("HSB")]
|
||||
[DataRow("HSI")]
|
||||
[DataRow("HWB")]
|
||||
[DataRow("NCol")]
|
||||
[DataRow("CIEXYZ")]
|
||||
[DataRow("CIELAB")]
|
||||
[DataRow("Oklab")]
|
||||
[DataRow("Oklch")]
|
||||
[DataRow("VEC4")]
|
||||
[DataRow("Decimal")]
|
||||
[DataRow("HEX Int")]
|
||||
public void GetDefaultFormat_KnownTypes_ReturnsNonEmptyString(string formatName)
|
||||
{
|
||||
var result = ColorFormatHelper.GetDefaultFormat(formatName);
|
||||
Assert.IsFalse(string.IsNullOrEmpty(result), $"Default format for {formatName} should not be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ namespace ColorPicker.Helpers
|
||||
public void GetStringRepresentationTest(string type, string expected)
|
||||
{
|
||||
var result = ColorRepresentationHelper.GetStringRepresentation(Color.Black, type, ColorFormatHelper.GetDefaultFormat(type));
|
||||
Assert.AreEqual(result, expected);
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,14 @@ namespace ImageResizer.Models
|
||||
{
|
||||
encoder.BitmapTransform.Bounds = cropBounds.Value;
|
||||
}
|
||||
|
||||
// Apply codec-specific properties (e.g., JPEG quality).
|
||||
// Must be set after transforms since re-encoding will occur.
|
||||
var encoderProps = GetEncoderPropertySet(encoderGuid);
|
||||
if (encoderProps != null)
|
||||
{
|
||||
await encoder.BitmapProperties.SetPropertiesAsync(encoderProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -515,6 +523,25 @@ namespace ImageResizer.Models
|
||||
};
|
||||
}
|
||||
|
||||
if (encoderGuid == BitmapEncoder.PngEncoderId)
|
||||
{
|
||||
// Only override when explicitly set; Default lets the WIC encoder decide.
|
||||
if (_settings.PngInterlaceOption == PngInterlaceOption.On)
|
||||
{
|
||||
return new BitmapPropertySet
|
||||
{
|
||||
{ "InterlaceOption", new BitmapTypedValue(true, PropertyType.Boolean) },
|
||||
};
|
||||
}
|
||||
else if (_settings.PngInterlaceOption == PngInterlaceOption.Off)
|
||||
{
|
||||
return new BitmapPropertySet
|
||||
{
|
||||
{ "InterlaceOption", new BitmapTypedValue(false, PropertyType.Boolean) },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (encoderGuid == BitmapEncoder.TiffEncoderId)
|
||||
{
|
||||
var compressionMethod = MapTiffCompression(_settings.TiffCompressOption);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
public class KeyChangedEventArgs : EventArgs
|
||||
{
|
||||
public string OldKeyName { get; }
|
||||
|
||||
public string NewKeyName { get; }
|
||||
|
||||
public int NewKeyCode { get; }
|
||||
|
||||
public KeyChangedEventArgs(string oldKeyName, string newKeyName, int newKeyCode)
|
||||
{
|
||||
OldKeyName = oldKeyName;
|
||||
NewKeyName = newKeyName;
|
||||
NewKeyCode = newKeyCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="KeyboardManagerEditorUI.Controls.KeyDropDownButton"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
|
||||
xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<DropDownButton
|
||||
x:Name="KeyButton"
|
||||
MinWidth="48"
|
||||
MinHeight="36"
|
||||
Padding="8"
|
||||
HorizontalAlignment="Center"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
FontSize="16"
|
||||
Style="{StaticResource DefaultKeyVisualDropDownButtonStyle}">
|
||||
<DropDownButton.Flyout>
|
||||
<Flyout
|
||||
x:Name="KeyListFlyout"
|
||||
Closed="KeyListFlyout_Closed"
|
||||
Opening="KeyListFlyout_Opening"
|
||||
Placement="Bottom">
|
||||
<ListView
|
||||
x:Name="KeyListView"
|
||||
MinWidth="200"
|
||||
MaxHeight="320"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="KeyListView_ItemClick"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</Flyout>
|
||||
</DropDownButton.Flyout>
|
||||
|
||||
<commoncontrols:KeyCharPresenter x:Name="KeyNamePresenter" Content="{x:Bind KeyName, Mode=OneWay}" />
|
||||
</DropDownButton>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using KeyboardManagerEditorUI.Interop;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
public sealed partial class KeyDropDownButton : UserControl
|
||||
{
|
||||
private static List<KeyNameEntry>? _cachedKeyList;
|
||||
private static List<KeyNameEntry>? _cachedShortcutKeyList;
|
||||
|
||||
public static readonly DependencyProperty KeyNameProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(KeyName),
|
||||
typeof(string),
|
||||
typeof(KeyDropDownButton),
|
||||
new PropertyMetadata(string.Empty));
|
||||
|
||||
public static readonly DependencyProperty IsShortcutProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(IsShortcut),
|
||||
typeof(bool),
|
||||
typeof(KeyDropDownButton),
|
||||
new PropertyMetadata(true));
|
||||
|
||||
public static readonly DependencyProperty UseAccentStyleProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(UseAccentStyle),
|
||||
typeof(bool),
|
||||
typeof(KeyDropDownButton),
|
||||
new PropertyMetadata(false));
|
||||
|
||||
public string KeyName
|
||||
{
|
||||
get => (string)GetValue(KeyNameProperty);
|
||||
set => SetValue(KeyNameProperty, value);
|
||||
}
|
||||
|
||||
public bool IsShortcut
|
||||
{
|
||||
get => (bool)GetValue(IsShortcutProperty);
|
||||
set => SetValue(IsShortcutProperty, value);
|
||||
}
|
||||
|
||||
public bool UseAccentStyle
|
||||
{
|
||||
get => (bool)GetValue(UseAccentStyleProperty);
|
||||
set => SetValue(UseAccentStyleProperty, value);
|
||||
}
|
||||
|
||||
public event EventHandler<KeyChangedEventArgs>? KeyChanged;
|
||||
|
||||
public KeyDropDownButton()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.Loaded += (_, _) =>
|
||||
{
|
||||
if (UseAccentStyle)
|
||||
{
|
||||
KeyButton.Style = (Style)Application.Current.Resources["AccentKeyVisualDropDownButtonStyle"];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void KeyListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is KeyNameEntry entry)
|
||||
{
|
||||
string oldKeyName = KeyName;
|
||||
KeyListFlyout.Hide();
|
||||
KeyChanged?.Invoke(this, new KeyChangedEventArgs(oldKeyName, entry.DisplayName, entry.KeyCode));
|
||||
}
|
||||
}
|
||||
|
||||
private void KeyListFlyout_Closed(object sender, object e)
|
||||
{
|
||||
// Clear selection when flyout closes
|
||||
KeyListView.SelectedItem = null;
|
||||
}
|
||||
|
||||
private void KeyListFlyout_Opening(object sender, object e)
|
||||
{
|
||||
RefreshKeyList();
|
||||
}
|
||||
|
||||
private List<KeyNameEntry> GetKeyList()
|
||||
{
|
||||
bool isShortcut = IsShortcut;
|
||||
ref var cached = ref (isShortcut ? ref _cachedShortcutKeyList : ref _cachedKeyList);
|
||||
|
||||
if (cached == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var service = new KeyboardMappingService())
|
||||
{
|
||||
var list = service.GetKeyboardKeysList(isShortcut);
|
||||
|
||||
// Filter out the synthetic "None" entry (keycode 0) that the native layer
|
||||
// injects for shortcut lists; selecting it would store an invalid key code.
|
||||
cached = list.Where(e => e.KeyCode != 0).ToList();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
cached = new List<KeyNameEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
internal void RefreshKeyList()
|
||||
{
|
||||
KeyListView.ItemsSource = GetKeyList();
|
||||
|
||||
// Scroll to current key if possible
|
||||
var list = GetKeyList();
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (string.Equals(list[i].DisplayName, KeyName, StringComparison.Ordinal))
|
||||
{
|
||||
KeyListView.SelectedIndex = i;
|
||||
KeyListView.ScrollIntoView(list[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:KeyboardManagerEditorUI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
@@ -80,28 +81,29 @@
|
||||
Style="{StaticResource CustomShortcutToggleButtonStyle}"
|
||||
Unchecked="TriggerKeyToggleBtn_Unchecked">
|
||||
<ToggleButton.Content>
|
||||
<ItemsControl x:Name="TriggerKeys">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls:WrapPanel
|
||||
HorizontalSpacing="4"
|
||||
Orientation="Horizontal"
|
||||
VerticalSpacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<commoncontrols:KeyVisual
|
||||
Padding="8"
|
||||
Background="{ThemeResource ControlFillColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
Content="{Binding}"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
FontSize="16"
|
||||
Style="{StaticResource DefaultKeyVisualStyle}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Grid>
|
||||
<TextBlock
|
||||
x:Name="TriggerKeyPlaceholder"
|
||||
x:Uid="TriggerKeyPlaceholder"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorDisabledBrush}" />
|
||||
<ItemsControl x:Name="TriggerKeys" IsTabStop="False">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls:WrapPanel
|
||||
HorizontalSpacing="4"
|
||||
Orientation="Horizontal"
|
||||
VerticalSpacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<local:KeyDropDownButton KeyName="{Binding}" Loaded="TriggerKeyDropDown_Loaded" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</ToggleButton.Content>
|
||||
</ToggleButton>
|
||||
<CheckBox
|
||||
@@ -204,6 +206,12 @@
|
||||
<TextBlock x:Uid="ActionType_OpenApp_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<ComboBoxItem x:Uid="ActionType_Disable" Tag="Disable">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock x:Uid="ActionType_Disable_Text" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
<!--
|
||||
<ComboBoxItem x:Uid="ActionType_MouseClick" Tag="MouseClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
@@ -235,29 +243,32 @@
|
||||
Style="{StaticResource CustomShortcutToggleButtonStyle}"
|
||||
Unchecked="ActionKeyToggleBtn_Unchecked">
|
||||
<ToggleButton.Content>
|
||||
<ItemsControl x:Name="ActionKeys">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls:WrapPanel
|
||||
HorizontalSpacing="4"
|
||||
Orientation="Horizontal"
|
||||
VerticalSpacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<commoncontrols:KeyVisual
|
||||
Padding="8"
|
||||
Background="{ThemeResource CustomAccentBackgroundBrush}"
|
||||
BorderThickness="0"
|
||||
Content="{Binding}"
|
||||
CornerRadius="{StaticResource OverlayCornerRadius}"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Style="{StaticResource DefaultKeyVisualStyle}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Grid>
|
||||
<TextBlock
|
||||
x:Name="ActionKeyPlaceholder"
|
||||
x:Uid="ActionKeyPlaceholder"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorDisabledBrush}" />
|
||||
<ItemsControl x:Name="ActionKeys" IsTabStop="False">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<controls:WrapPanel
|
||||
HorizontalSpacing="4"
|
||||
Orientation="Horizontal"
|
||||
VerticalSpacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<local:KeyDropDownButton
|
||||
KeyName="{Binding}"
|
||||
Loaded="ActionKeyDropDown_Loaded"
|
||||
UseAccentStyle="True" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</ToggleButton.Content>
|
||||
</ToggleButton>
|
||||
</tkcontrols:Case>
|
||||
@@ -288,7 +299,7 @@
|
||||
<!-- Open App Action -->
|
||||
<tkcontrols:Case Value="OpenApp">
|
||||
<StackPanel Orientation="Vertical" Spacing="16">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -307,13 +318,17 @@
|
||||
Click="ProgramPathSelectButton_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="ProgramPathSelectButtonTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
</Grid>
|
||||
<TextBox
|
||||
x:Name="ProgramArgsInput"
|
||||
x:Uid="ProgramArgsInput"
|
||||
GotFocus="ProgramArgsInput_GotFocus" />
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid ColumnSpacing="4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -331,7 +346,11 @@
|
||||
Click="StartInSelectButton_Click"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}" />
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="StartInSelectButtonTooltip" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
</Grid>
|
||||
<ComboBox
|
||||
x:Name="ElevationComboBox"
|
||||
@@ -372,6 +391,13 @@
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</tkcontrols:Case>
|
||||
<!-- Disable Action -->
|
||||
<tkcontrols:Case Value="Disable">
|
||||
<TextBlock
|
||||
x:Uid="DisableDescription"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
TextWrapping="Wrap" />
|
||||
</tkcontrols:Case>
|
||||
</tkcontrols:SwitchPresenter>
|
||||
</StackPanel>
|
||||
<!-- Validation InfoBar spanning all columns -->
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using KeyboardManagerEditorUI.Helpers;
|
||||
using KeyboardManagerEditorUI.Interop;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Storage;
|
||||
@@ -77,6 +78,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
OpenUrl,
|
||||
OpenApp,
|
||||
MouseClick,
|
||||
Disable,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -129,6 +131,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
"OpenUrl" => ActionType.OpenUrl,
|
||||
"OpenApp" => ActionType.OpenApp,
|
||||
"MouseClick" => ActionType.MouseClick,
|
||||
"Disable" => ActionType.Disable,
|
||||
_ => ActionType.KeyOrShortcut,
|
||||
};
|
||||
}
|
||||
@@ -148,8 +151,17 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
TriggerKeys.ItemsSource = _triggerKeys;
|
||||
ActionKeys.ItemsSource = _actionKeys;
|
||||
|
||||
_triggerKeys.CollectionChanged += (_, _) => RaiseValidationStateChanged();
|
||||
_actionKeys.CollectionChanged += (_, _) => RaiseValidationStateChanged();
|
||||
_triggerKeys.CollectionChanged += (_, _) =>
|
||||
{
|
||||
UpdatePlaceholderVisibility();
|
||||
RaiseValidationStateChanged();
|
||||
};
|
||||
|
||||
_actionKeys.CollectionChanged += (_, _) =>
|
||||
{
|
||||
UpdatePlaceholderVisibility();
|
||||
RaiseValidationStateChanged();
|
||||
};
|
||||
|
||||
this.Unloaded += UnifiedMappingControl_Unloaded;
|
||||
}
|
||||
@@ -209,6 +221,9 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
ActionKeyToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
// Disable dropdowns during recording
|
||||
SetDropDownsEnabled(TriggerKeys, false);
|
||||
|
||||
KeyboardHookHelper.Instance.ActivateHook(this);
|
||||
}
|
||||
}
|
||||
@@ -219,6 +234,8 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
|
||||
SetDropDownsEnabled(TriggerKeys, true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -262,6 +279,9 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
TriggerKeyToggleBtn.IsChecked = false;
|
||||
}
|
||||
|
||||
// Disable dropdowns during recording
|
||||
SetDropDownsEnabled(ActionKeys, false);
|
||||
|
||||
KeyboardHookHelper.Instance.ActivateHook(this);
|
||||
}
|
||||
}
|
||||
@@ -272,6 +292,238 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
{
|
||||
CleanupKeyboardHook();
|
||||
}
|
||||
|
||||
SetDropDownsEnabled(ActionKeys, true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Key Dropdown Handling
|
||||
|
||||
private void TriggerKeyDropDown_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is KeyDropDownButton dropDown)
|
||||
{
|
||||
// Ensure we do not accumulate multiple subscriptions when Loaded fires repeatedly.
|
||||
dropDown.KeyChanged -= TriggerKeyDropDown_KeyChanged;
|
||||
dropDown.KeyChanged += TriggerKeyDropDown_KeyChanged;
|
||||
|
||||
// Use a named Unloaded handler so we can detach it and avoid accumulating handlers.
|
||||
dropDown.Unloaded -= TriggerKeyDropDown_Unloaded;
|
||||
dropDown.Unloaded += TriggerKeyDropDown_Unloaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerKeyDropDown_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is KeyDropDownButton dropDown)
|
||||
{
|
||||
dropDown.KeyChanged -= TriggerKeyDropDown_KeyChanged;
|
||||
dropDown.Unloaded -= TriggerKeyDropDown_Unloaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void ActionKeyDropDown_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is KeyDropDownButton dropDown)
|
||||
{
|
||||
// Ensure we do not accumulate multiple subscriptions when Loaded fires repeatedly.
|
||||
dropDown.KeyChanged -= ActionKeyDropDown_KeyChanged;
|
||||
dropDown.KeyChanged += ActionKeyDropDown_KeyChanged;
|
||||
|
||||
// Use a named Unloaded handler so we can detach it and avoid accumulating handlers.
|
||||
dropDown.Unloaded -= ActionKeyDropDown_Unloaded;
|
||||
dropDown.Unloaded += ActionKeyDropDown_Unloaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void ActionKeyDropDown_Unloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is KeyDropDownButton dropDown)
|
||||
{
|
||||
dropDown.KeyChanged -= ActionKeyDropDown_KeyChanged;
|
||||
dropDown.Unloaded -= ActionKeyDropDown_Unloaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerKeyDropDown_KeyChanged(object? sender, KeyChangedEventArgs e)
|
||||
{
|
||||
if (sender is KeyDropDownButton dropDown)
|
||||
{
|
||||
int index = GetDropDownIndex(TriggerKeys, dropDown);
|
||||
if (index >= 0 && index < _triggerKeys.Count)
|
||||
{
|
||||
// KeyCode 0 means "None" — treat as invalid selection and do not update.
|
||||
if (e.NewKeyCode == 0)
|
||||
{
|
||||
RevertKeySelection(_triggerKeys, index);
|
||||
return;
|
||||
}
|
||||
|
||||
string? validationError = ValidateDropDownSelection(_triggerKeys, index, e.NewKeyCode, e.NewKeyName);
|
||||
if (validationError != null)
|
||||
{
|
||||
RevertKeySelection(_triggerKeys, index);
|
||||
ShowNotificationTip(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
_triggerKeys[index] = e.NewKeyName;
|
||||
HandleAutoGrowShrink(_triggerKeys, index, e.NewKeyCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ActionKeyDropDown_KeyChanged(object? sender, KeyChangedEventArgs e)
|
||||
{
|
||||
if (sender is KeyDropDownButton dropDown)
|
||||
{
|
||||
int index = GetDropDownIndex(ActionKeys, dropDown);
|
||||
if (index >= 0 && index < _actionKeys.Count)
|
||||
{
|
||||
// KeyCode 0 means "None" — treat as invalid selection and do not update.
|
||||
if (e.NewKeyCode == 0)
|
||||
{
|
||||
RevertKeySelection(_actionKeys, index);
|
||||
return;
|
||||
}
|
||||
|
||||
string? validationError = ValidateDropDownSelection(_actionKeys, index, e.NewKeyCode, e.NewKeyName);
|
||||
if (validationError != null)
|
||||
{
|
||||
RevertKeySelection(_actionKeys, index);
|
||||
ShowNotificationTip(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
_actionKeys[index] = e.NewKeyName;
|
||||
HandleAutoGrowShrink(_actionKeys, index, e.NewKeyCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reverts a key selection by re-inserting the current value via the bound ObservableCollection,
|
||||
/// which forces the binding to refresh without breaking the binding expression.
|
||||
/// </summary>
|
||||
private static void RevertKeySelection(ObservableCollection<string> keys, int index)
|
||||
{
|
||||
string current = keys[index];
|
||||
keys.RemoveAt(index);
|
||||
keys.Insert(index, current);
|
||||
}
|
||||
|
||||
private static int GetDropDownIndex(ItemsControl itemsControl, KeyDropDownButton dropDown)
|
||||
{
|
||||
for (int i = 0; i < itemsControl.Items.Count; i++)
|
||||
{
|
||||
var container = itemsControl.ContainerFromIndex(i) as ContentPresenter;
|
||||
if (container != null)
|
||||
{
|
||||
// Walk the visual tree to find the KeyDropDownButton
|
||||
var child = FindChild<KeyDropDownButton>(container);
|
||||
if (child == dropDown)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static T? FindChild<T>(Microsoft.UI.Xaml.DependencyObject parent)
|
||||
where T : Microsoft.UI.Xaml.DependencyObject
|
||||
{
|
||||
int childCount = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(parent);
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
var child = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is T result)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var descendant = FindChild<T>(child);
|
||||
if (descendant != null)
|
||||
{
|
||||
return descendant;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a key selection from a dropdown before it is applied.
|
||||
/// Returns null if valid, or an error message string if invalid.
|
||||
/// </summary>
|
||||
private static string? ValidateDropDownSelection(ObservableCollection<string> keys, int changedIndex, int newKeyCode, string newKeyName)
|
||||
{
|
||||
const int maxShortcutSize = 5;
|
||||
|
||||
// KeyType: 0=Win, 1=Ctrl, 2=Alt, 3=Shift, 4=Action
|
||||
int newKeyType = KeyboardManagerInterop.GetKeyType(newKeyCode);
|
||||
bool isModifier = newKeyType < 4;
|
||||
|
||||
// Count only non-empty (real) entries to determine effective shortcut size.
|
||||
int nonEmptyCount = keys.Count(k => !string.IsNullOrEmpty(k));
|
||||
|
||||
// Rule: action key at position 0 in multi-key shortcut (shortcut must start with modifier)
|
||||
if (!isModifier && changedIndex == 0 && nonEmptyCount > 1)
|
||||
{
|
||||
return ResourceHelper.GetString("Warning_ShortcutStartWithModifier");
|
||||
}
|
||||
|
||||
// Rule: no repeated modifier types (skip empty placeholder slots)
|
||||
if (isModifier)
|
||||
{
|
||||
for (int i = 0; i < keys.Count; i++)
|
||||
{
|
||||
if (i == changedIndex || string.IsNullOrEmpty(keys[i]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int existingKeyCode = KeyboardManagerInterop.GetKeyCodeFromName(keys[i]);
|
||||
int existingKeyType = KeyboardManagerInterop.GetKeyType(existingKeyCode);
|
||||
|
||||
if (existingKeyType == newKeyType)
|
||||
{
|
||||
return ResourceHelper.GetString("Warning_RepeatedModifier");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rule: modifier at last position when already at max size
|
||||
if (isModifier && changedIndex == keys.Count - 1 && nonEmptyCount >= maxShortcutSize)
|
||||
{
|
||||
return ResourceHelper.GetString("Warning_MaxShortcutSize");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void HandleAutoGrowShrink(ObservableCollection<string> keys, int changedIndex, int newKeyCode)
|
||||
{
|
||||
const int maxShortcutSize = 5;
|
||||
|
||||
int keyType = KeyboardManagerInterop.GetKeyType(newKeyCode);
|
||||
bool isModifier = keyType < 4;
|
||||
|
||||
if (isModifier && changedIndex == keys.Count - 1 && keys.Count < maxShortcutSize)
|
||||
{
|
||||
// Modifier at last position — auto-grow: add placeholder for next key
|
||||
keys.Add(string.Empty);
|
||||
}
|
||||
else if (!isModifier)
|
||||
{
|
||||
// Action key — trim any trailing entries after this one
|
||||
while (keys.Count > changedIndex + 1)
|
||||
{
|
||||
keys.RemoveAt(keys.Count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -453,7 +705,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
|
||||
public void OnInputLimitReached()
|
||||
{
|
||||
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
|
||||
ShowNotificationTip(ResourceHelper.GetString("Warning_InputLimitReached"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -463,12 +715,12 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
/// <summary>
|
||||
/// Gets the trigger keys.
|
||||
/// </summary>
|
||||
public List<string> GetTriggerKeys() => _triggerKeys.ToList();
|
||||
public List<string> GetTriggerKeys() => _triggerKeys.Where(k => !string.IsNullOrEmpty(k)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action keys (for Key/Shortcut action type).
|
||||
/// </summary>
|
||||
public List<string> GetActionKeys() => _actionKeys.ToList();
|
||||
public List<string> GetActionKeys() => _actionKeys.Where(k => !string.IsNullOrEmpty(k)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the selected mouse trigger.
|
||||
@@ -567,6 +819,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
ActionType.Text => !string.IsNullOrEmpty(TextContentBox?.Text),
|
||||
ActionType.OpenUrl => !string.IsNullOrWhiteSpace(UrlPathInput?.Text),
|
||||
ActionType.OpenApp => !string.IsNullOrWhiteSpace(ProgramPathInput?.Text),
|
||||
ActionType.Disable => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
@@ -612,18 +865,28 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
/// </summary>
|
||||
public void SetActionType(ActionType actionType)
|
||||
{
|
||||
int index = actionType switch
|
||||
if (ActionTypeComboBox == null)
|
||||
{
|
||||
ActionType.Text => 1,
|
||||
ActionType.OpenUrl => 2,
|
||||
ActionType.OpenApp => 3,
|
||||
ActionType.MouseClick => 4,
|
||||
_ => 0,
|
||||
return;
|
||||
}
|
||||
|
||||
string tag = actionType switch
|
||||
{
|
||||
ActionType.Text => "Text",
|
||||
ActionType.OpenUrl => "OpenUrl",
|
||||
ActionType.OpenApp => "OpenApp",
|
||||
ActionType.Disable => "Disable",
|
||||
ActionType.MouseClick => "MouseClick",
|
||||
_ => "KeyOrShortcut",
|
||||
};
|
||||
|
||||
if (ActionTypeComboBox != null)
|
||||
foreach (var item in ActionTypeComboBox.Items)
|
||||
{
|
||||
ActionTypeComboBox.SelectedIndex = index;
|
||||
if (item is ComboBoxItem comboBoxItem && comboBoxItem.Tag is string itemTag && itemTag == tag)
|
||||
{
|
||||
ActionTypeComboBox.SelectedItem = comboBoxItem;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -748,11 +1011,44 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetDropDownsEnabled(ItemsControl itemsControl, bool enabled)
|
||||
{
|
||||
for (int i = 0; i < itemsControl.Items.Count; i++)
|
||||
{
|
||||
var container = itemsControl.ContainerFromIndex(i) as ContentPresenter;
|
||||
if (container != null)
|
||||
{
|
||||
var dropDown = FindChild<KeyDropDownButton>(container);
|
||||
if (dropDown != null)
|
||||
{
|
||||
dropDown.IsEnabled = enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupKeyboardHook()
|
||||
{
|
||||
KeyboardHookHelper.Instance.CleanupHook();
|
||||
}
|
||||
|
||||
private void UpdatePlaceholderVisibility()
|
||||
{
|
||||
if (TriggerKeyPlaceholder != null)
|
||||
{
|
||||
TriggerKeyPlaceholder.Visibility = _triggerKeys.Count == 0
|
||||
? Microsoft.UI.Xaml.Visibility.Visible
|
||||
: Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (ActionKeyPlaceholder != null)
|
||||
{
|
||||
ActionKeyPlaceholder.Visibility = _actionKeys.Count == 0
|
||||
? Microsoft.UI.Xaml.Visibility.Visible
|
||||
: Microsoft.UI.Xaml.Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseValidationStateChanged()
|
||||
{
|
||||
UpdateInlineValidation();
|
||||
@@ -910,7 +1206,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
/// </summary>
|
||||
public void ShowNotificationTip(string message)
|
||||
{
|
||||
ShowValidationMessage("Warning", message, InfoBarSeverity.Warning);
|
||||
ShowValidationMessage(ResourceHelper.GetString("Warning_Title"), message, InfoBarSeverity.Warning);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -932,7 +1228,7 @@ namespace KeyboardManagerEditorUI.Controls
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowValidationError("Validation Error", "An unknown validation error occurred.");
|
||||
ShowValidationError(ResourceHelper.GetString("Error_UnknownValidation_Title"), ResourceHelper.GetString("Error_UnknownValidation_Message"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using KeyboardManagerEditorUI.Interop;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Windows.System;
|
||||
|
||||
@@ -19,7 +20,7 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
|
||||
public static KeyboardHookHelper Instance => _instance ??= new KeyboardHookHelper();
|
||||
|
||||
private KeyboardMappingService _mappingService;
|
||||
private KeyboardMappingService? _mappingService;
|
||||
|
||||
private HotkeySettingsControlHook? _keyboardHook;
|
||||
|
||||
@@ -34,7 +35,14 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
// Singleton to make sure only one instance of the hook is active
|
||||
private KeyboardHookHelper()
|
||||
{
|
||||
_mappingService = new KeyboardMappingService();
|
||||
try
|
||||
{
|
||||
_mappingService = new KeyboardMappingService();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Native KBM library unavailable for keyboard hook: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivateHook(IKeyboardHookTarget target)
|
||||
@@ -46,11 +54,18 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
_currentlyPressedKeys.Clear();
|
||||
_keyPressOrder.Clear();
|
||||
|
||||
_keyboardHook = new HotkeySettingsControlHook(
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
() => true,
|
||||
(key, extraInfo) => true);
|
||||
try
|
||||
{
|
||||
_keyboardHook = new HotkeySettingsControlHook(
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
() => true,
|
||||
(key, extraInfo) => true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Keyboard hook unavailable: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void CleanupHook()
|
||||
@@ -110,6 +125,17 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
_keyPressOrder.Add(virtualKey);
|
||||
|
||||
// When building chords, cap at 2 action keys: if a third action key arrives,
|
||||
// remove the oldest (shift behavior matching old editor).
|
||||
if (_activeTarget.AllowChords && !RemappingHelper.IsModifierKey(virtualKey))
|
||||
{
|
||||
var actionKeysInOrder = _keyPressOrder.Where(k => !RemappingHelper.IsModifierKey(k)).ToList();
|
||||
if (actionKeysInOrder.Count > 2)
|
||||
{
|
||||
_keyPressOrder.Remove(actionKeysInOrder[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the target page
|
||||
_activeTarget.OnKeyDown(virtualKey, GetFormattedKeyList());
|
||||
}
|
||||
@@ -126,7 +152,13 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
|
||||
if (_currentlyPressedKeys.Remove(virtualKey))
|
||||
{
|
||||
_keyPressOrder.Remove(virtualKey);
|
||||
// When building chords, preserve released action keys in _keyPressOrder
|
||||
// so the next action key press is recognized as the chord's second key.
|
||||
// Only modifier releases clear from _keyPressOrder (matching old editor behavior).
|
||||
if (!_activeTarget.AllowChords || RemappingHelper.IsModifierKey(virtualKey))
|
||||
{
|
||||
_keyPressOrder.Remove(virtualKey);
|
||||
}
|
||||
|
||||
_activeTarget.OnKeyUp(virtualKey, GetFormattedKeyList());
|
||||
}
|
||||
@@ -140,6 +172,11 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
if (_mappingService is null)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
List<string> keyList = new List<string>();
|
||||
List<VirtualKey> modifierKeys = new List<VirtualKey>();
|
||||
VirtualKey? actionKey = null;
|
||||
@@ -147,9 +184,15 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
|
||||
foreach (var key in _keyPressOrder)
|
||||
{
|
||||
// For modifiers, only include if currently pressed.
|
||||
// For action keys when building chords, also include released keys
|
||||
// so the chord's first key stays visible while waiting for the second.
|
||||
if (!_currentlyPressedKeys.Contains(key))
|
||||
{
|
||||
continue;
|
||||
if (RemappingHelper.IsModifierKey(key) || !_activeTarget.AllowChords)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (RemappingHelper.IsModifierKey(key))
|
||||
@@ -189,6 +232,11 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
|
||||
private void RemoveExistingModifierVariant(VirtualKey key)
|
||||
{
|
||||
if (_mappingService is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KeyType keyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)key);
|
||||
|
||||
// No need to remove if the key is an action key
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public static class ResourceHelper
|
||||
{
|
||||
private static ResourceLoader? _resourceLoader;
|
||||
|
||||
public static string GetString(string resourceKey)
|
||||
{
|
||||
_resourceLoader ??= new ResourceLoader();
|
||||
return _resourceLoader.GetString(resourceKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public static class ServiceStatusHelper
|
||||
{
|
||||
private const string KeyboardManagerEngineProcessName = "PowerToys.KeyboardManagerEngine";
|
||||
|
||||
public static bool IsKeyboardManagerServiceRunning()
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(KeyboardManagerEngineProcessName);
|
||||
bool running = processes.Length > 0;
|
||||
foreach (var process in processes)
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
|
||||
return running;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsPowerToysRunning()
|
||||
{
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName("PowerToys");
|
||||
bool running = processes.Length > 0;
|
||||
foreach (var process in processes)
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
|
||||
return running;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
EmptyAppName,
|
||||
IllegalShortcut,
|
||||
DuplicateMapping,
|
||||
ConflictingModifier,
|
||||
SelfMapping,
|
||||
EmptyTargetText,
|
||||
EmptyUrl,
|
||||
|
||||
@@ -16,17 +16,18 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
{
|
||||
public static readonly Dictionary<ValidationErrorType, (string Title, string Message)> ValidationMessages = new()
|
||||
{
|
||||
{ ValidationErrorType.EmptyOriginalKeys, ("Missing Original Keys", "Please enter at least one original key to create a remapping.") },
|
||||
{ ValidationErrorType.EmptyRemappedKeys, ("Missing Target Keys", "Please enter at least one target key to create a remapping.") },
|
||||
{ ValidationErrorType.ModifierOnly, ("Invalid Shortcut", "Shortcuts must contain at least one action key in addition to modifier keys (Ctrl, Alt, Shift, Win).") },
|
||||
{ ValidationErrorType.EmptyAppName, ("Missing Application Name", "You've selected app-specific remapping but haven't specified an application name. Please enter the application name.") },
|
||||
{ ValidationErrorType.IllegalShortcut, ("Reserved System Shortcut", "Win+L and Ctrl+Alt+Delete are reserved system shortcuts and cannot be remapped.") },
|
||||
{ ValidationErrorType.DuplicateMapping, ("Duplicate Remapping", "This key or shortcut is already remapped.") },
|
||||
{ ValidationErrorType.SelfMapping, ("Invalid Remapping", "A key or shortcut cannot be remapped to itself. Please choose a different target.") },
|
||||
{ ValidationErrorType.EmptyTargetText, ("Missing Target Text", "Please enter the text to be inserted when the shortcut is pressed.") },
|
||||
{ ValidationErrorType.EmptyUrl, ("Missing URL", "Please enter the URL to open when the shortcut is pressed.") },
|
||||
{ ValidationErrorType.EmptyProgramPath, ("Missing Program Path", "Please enter the program path to launch when the shortcut is pressed.") },
|
||||
{ ValidationErrorType.OneKeyMapping, ("Invalid Remapping", "A single key cannot be remapped to a Program or URL shortcut. Please choose a combination of keys.") },
|
||||
{ ValidationErrorType.EmptyOriginalKeys, (ResourceHelper.GetString("Validation_EmptyOriginalKeys_Title"), ResourceHelper.GetString("Validation_EmptyOriginalKeys_Message")) },
|
||||
{ ValidationErrorType.EmptyRemappedKeys, (ResourceHelper.GetString("Validation_EmptyRemappedKeys_Title"), ResourceHelper.GetString("Validation_EmptyRemappedKeys_Message")) },
|
||||
{ ValidationErrorType.ModifierOnly, (ResourceHelper.GetString("Validation_ModifierOnly_Title"), ResourceHelper.GetString("Validation_ModifierOnly_Message")) },
|
||||
{ ValidationErrorType.EmptyAppName, (ResourceHelper.GetString("Validation_EmptyAppName_Title"), ResourceHelper.GetString("Validation_EmptyAppName_Message")) },
|
||||
{ ValidationErrorType.IllegalShortcut, (ResourceHelper.GetString("Validation_IllegalShortcut_Title"), ResourceHelper.GetString("Validation_IllegalShortcut_Message")) },
|
||||
{ ValidationErrorType.DuplicateMapping, (ResourceHelper.GetString("Validation_DuplicateMapping_Title"), ResourceHelper.GetString("Validation_DuplicateMapping_Message")) },
|
||||
{ ValidationErrorType.ConflictingModifier, (ResourceHelper.GetString("Validation_ConflictingModifier_Title"), ResourceHelper.GetString("Validation_ConflictingModifier_Message")) },
|
||||
{ ValidationErrorType.SelfMapping, (ResourceHelper.GetString("Validation_SelfMapping_Title"), ResourceHelper.GetString("Validation_SelfMapping_Message")) },
|
||||
{ ValidationErrorType.EmptyTargetText, (ResourceHelper.GetString("Validation_EmptyTargetText_Title"), ResourceHelper.GetString("Validation_EmptyTargetText_Message")) },
|
||||
{ ValidationErrorType.EmptyUrl, (ResourceHelper.GetString("Validation_EmptyUrl_Title"), ResourceHelper.GetString("Validation_EmptyUrl_Message")) },
|
||||
{ ValidationErrorType.EmptyProgramPath, (ResourceHelper.GetString("Validation_EmptyProgramPath_Title"), ResourceHelper.GetString("Validation_EmptyProgramPath_Message")) },
|
||||
{ ValidationErrorType.OneKeyMapping, (ResourceHelper.GetString("Validation_OneKeyMapping_Title"), ResourceHelper.GetString("Validation_OneKeyMapping_Message")) },
|
||||
};
|
||||
|
||||
public static ValidationErrorType ValidateKeyMapping(
|
||||
@@ -69,6 +70,11 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
return ValidationErrorType.DuplicateMapping;
|
||||
}
|
||||
|
||||
if (originalKeys.Count == 1 && HasConflictingModifierMapping(originalKeys[0], isEditMode, mappingService))
|
||||
{
|
||||
return ValidationErrorType.ConflictingModifier;
|
||||
}
|
||||
|
||||
if (IsSelfMapping(originalKeys, remappedKeys, mappingService))
|
||||
{
|
||||
return ValidationErrorType.SelfMapping;
|
||||
@@ -77,6 +83,47 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
return ValidationErrorType.NoError;
|
||||
}
|
||||
|
||||
public static ValidationErrorType ValidateDisableMapping(
|
||||
List<string> originalKeys,
|
||||
bool isAppSpecific,
|
||||
string appName,
|
||||
KeyboardMappingService mappingService,
|
||||
bool isEditMode = false,
|
||||
Remapping? editingRemapping = null)
|
||||
{
|
||||
if (originalKeys == null || originalKeys.Count == 0)
|
||||
{
|
||||
return ValidationErrorType.EmptyOriginalKeys;
|
||||
}
|
||||
|
||||
if (originalKeys.Count > 1 && ContainsOnlyModifierKeys(originalKeys))
|
||||
{
|
||||
return ValidationErrorType.ModifierOnly;
|
||||
}
|
||||
|
||||
if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
|
||||
{
|
||||
return ValidationErrorType.EmptyAppName;
|
||||
}
|
||||
|
||||
if (originalKeys.Count > 1 && IsIllegalShortcut(originalKeys, mappingService))
|
||||
{
|
||||
return ValidationErrorType.IllegalShortcut;
|
||||
}
|
||||
|
||||
if (IsDuplicateMapping(originalKeys, isEditMode, mappingService, appName))
|
||||
{
|
||||
return ValidationErrorType.DuplicateMapping;
|
||||
}
|
||||
|
||||
if (originalKeys.Count == 1 && HasConflictingModifierMapping(originalKeys[0], isEditMode, mappingService))
|
||||
{
|
||||
return ValidationErrorType.ConflictingModifier;
|
||||
}
|
||||
|
||||
return ValidationErrorType.NoError;
|
||||
}
|
||||
|
||||
public static ValidationErrorType ValidateTextMapping(
|
||||
List<string> keys,
|
||||
string textContent,
|
||||
@@ -239,6 +286,58 @@ namespace KeyboardManagerEditorUI.Helpers
|
||||
return KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a single key conflicts with existing single-key mappings via modifier variants.
|
||||
/// E.g., remapping LCtrl when Ctrl is already mapped, or vice versa.
|
||||
/// </summary>
|
||||
private static bool HasConflictingModifierMapping(string keyName, bool isEditMode, KeyboardMappingService mappingService)
|
||||
{
|
||||
int keyCode = KeyboardManagerInterop.GetKeyCodeFromName(keyName);
|
||||
int keyType = KeyboardManagerInterop.GetKeyType(keyCode);
|
||||
|
||||
// Only modifier keys can conflict with their variants
|
||||
if (keyType >= 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int upperLimit = isEditMode ? 1 : 0;
|
||||
int conflictCount = 0;
|
||||
|
||||
foreach (var settings in SettingsManager.EditorSettings.ShortcutSettingsDictionary.Values)
|
||||
{
|
||||
string existingOriginal = settings.Shortcut.OriginalKeys;
|
||||
|
||||
// Only check single-key mappings (no semicolons)
|
||||
if (string.IsNullOrEmpty(existingOriginal) || existingOriginal.Contains(';'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (int.TryParse(existingOriginal, out int existingKeyCode))
|
||||
{
|
||||
if (existingKeyCode == keyCode)
|
||||
{
|
||||
continue; // Exact match handled by DuplicateMapping
|
||||
}
|
||||
|
||||
int existingKeyType = KeyboardManagerInterop.GetKeyType(existingKeyCode);
|
||||
|
||||
// Same modifier type (e.g., Ctrl and LCtrl) = conflict
|
||||
if (existingKeyType == keyType)
|
||||
{
|
||||
conflictCount++;
|
||||
if (conflictCount > upperLimit)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildKeyCodeString(List<string> keys, KeyboardMappingService mappingService)
|
||||
{
|
||||
return string.Join(";", keys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace KeyboardManagerEditorUI.Interop
|
||||
{
|
||||
public record KeyNameEntry(int KeyCode, string DisplayName);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user