Compare commits

...

35 Commits

Author SHA1 Message Date
Michael Jolley
c7f8ac0b06 Merging main (broken) 2026-02-13 17:15:25 -06:00
Gordon Lam
09bdbfac38 build(updating): Add WinRT coroutine support and refactor async methods (#45522)
## Summary of the Pull Request  
There are many build warnings now with like "cl : command line warning
D9047: option 'await' has been deprecated and will be re moved in a
future release." after we update to VS2026.

Introduce WinRT coroutine support by replacing `std::future` with
`IAsyncOperation` for asynchronous methods. Adjust output directories
and remove the `/await` option from project files to streamline the
build process. Update methods to utilize `std::expected` and `co_await`,
enhancing the async handling of version checks and downloads.

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

## Detailed Description of the Pull Request / Additional comments  
Refactor async methods to improve performance and compatibility with
WinRT. The changes include modifying the return types of several
functions in the `updating` namespace, specifically
`uninstall_previous_msix_version_async`,
`get_github_version_info_async`, and `download_new_version_async`.

## Validation Steps Performed  
Manual testing was conducted to ensure that the new async methods
function correctly and that the application behaves as expected during
version checks and downloads. Automated tests were updated to cover the
new coroutine implementations.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 06:31:46 -08:00
Jiří Polášek
603ac55f8a CmdPal: Prevent item template selectors from modifying containers (#45498)
## Summary of the Pull Request

This PR updates the item template selectors for ListView and GridView
and prevents them from modifying the container.
As a flyby, it introduces an enum for the list item type and centralizes
the logic that determines the type to the view model.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-11 06:03:48 -06:00
Mike Griese
3f5418132d [CmdPal] Fix context menu command items (#45499)
maintainers: we talked about this at length on Teams

> Seems like adding `IExtendedAttributesProvider` onto `CommandItem` is
what broke this. I don't know why. I'm not gonna pretend to understand
the cswinrt voodoo that's causing it to pick `IEAP` as the leaf
interface instead of `ICommandContextItem`.
 
 
drive by: fix the sample project on ARM

_resurrected from #45329 because spellbot killed that PR_
2026-02-11 05:51:00 -06:00
Heiko
e935faf08c [Settings > Advanced Paste] Move GPO info bar for Clipboard History (#45030)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

GPO Info bar for clipboard history has to be placed directly after the
setting. The PR moves the bar to the correct place.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-11 15:31:18 +08:00
Heiko
eb5f4c6bd0 [Settings > Generla] Create Bug Report button is wrongly disabled if Telemetry GPO is disabled (#45033)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR replaces the GPI control around the report button with an info
bar between diagnostics settings and report button.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-11 14:26:10 +08:00
Jessica Dene Earley-Cha
658f90d6f8 CmdPal: Add AutomationProperties.Name to Extensions page "More" button (#45516)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Add an AutomationProperties.Name to the "More options" button on the
CmdPal Settings Extensions page, improving accessibility for screen
readers. Add localized AutomationProperties.Name ("More options") for
the Settings_ExtensionsPage_More_Button in Resources.resw

This address A11y bug 60946859

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

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

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

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


https://github.com/user-attachments/assets/9cd5f580-bf7d-4dd1-9344-96f6c51c7958
2026-02-11 06:44:38 +01:00
Michael Jolley
12bfa13b72 Fixing spelling 2026-02-10 20:15:29 -06:00
Michael Jolley
54d1ab3b70 WinRTExtensionService 2026-02-10 20:13:56 -06:00
Jessica Dene Earley-Cha
3f3e04086e Add developer documentation for implementing telemetry events in PowerToys modules. (#44912)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Adds a new doc (`doc/devdocs/Events.md`) that walks developers through
how to add telemetry events to PowerToys with next steps of reaching out
to Carlos or Jessica.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-10 11:22:51 -08:00
Mike Griese
3b874a9567 CmdPal: Port the devhome perf widgets to cmdpal (#45217)
Pretty direct port of the code, to prove it works.

There's definitely some improvement to be made here, esp WRT to the
network and GPU listing - networks should all just be listed. Or at
least automatically track the active one. And GPU should aggregate a
bunch of stats.

And we can probably add the details to these list items.

But most importantly, _it works_.

re: #45201
2026-02-10 06:00:27 -06:00
Kai Tao
7a86543c8d v0.97.2 hotfix release note (#45485)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

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

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

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

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

---------

Co-authored-by: Jiří Polášek <me@jiripolasek.com>
2026-02-09 23:37:48 -08:00
Kai Tao
67a013f729 Advanced Paste: Handle Foundry local Port change on the fly (#45362)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Foundry Local returns 400 Bad Request if a manual port change made for
foundry local.

Fix #45340

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Follow steps described in the issue, and the advanced paste can work
without having to restart powertoys itself
2026-02-10 15:31:23 +08:00
Jiří Polášek
521d34f1eb CmdPal: Add a button to toggle the visibility of the dev ribbon (#45379)
## Summary of the Pull Request

This PR adds an option to toggle visibility of the dev ribbon. The state
is only temporary and does not persist across app runs -- that is
intentional to prevent the dev from turning it off permanently.

<img width="648" height="519" alt="image"
src="https://github.com/user-attachments/assets/c5b348f6-8f3c-4ec2-a250-e051fd003e09"
/>

<img width="1313" height="751" alt="image"
src="https://github.com/user-attachments/assets/89928e30-b0dc-4a14-b5fb-bf68357a145b"
/>


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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-09 18:15:22 -06:00
Jiří Polášek
a02a5a9736 CmdPal: Improvements and fixes for icon loading (#45460)
## Summary of the Pull Request

This PR is a follow-up for Icon cache:

- Adds decoding and rasterization limit (by width) to reduce memory
usage and improves throughput (noticeably)
- Fixes timing issue when setting padding for font icons
- Resolves race condition in IconBox caused by incorrect guard

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-09 14:06:39 -06:00
Jiří Polášek
1e25d17920 CmdPal: ApplicationInfoService + fix version in the error report builder (#45374)
## Summary of the Pull Request

This PR introduces a new service, ApplicationInfoService, that
encapsulates host app state and infrastructure and moves the logic for
obtaining version information there. It then uses this information to
fix an issue with incorrect version reporting for unpackaged CmdPal in
ErrorReportBuilder.

- Adds ApplicationInfoService.
- Fixes an error in ErrorReportBuilder when the app runs unpackaged.
- Adds logging of the app version and environmental info at startup.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-09 14:00:01 -06:00
Jiří Polášek
4959273875 CmdPal: Prevent the main window from stealing focus (#45309)
## Summary of the Pull Request

This PR adds two fixes to prevent Command Palette’s main window from
stealing focus while or after it is being hidden.

1) GoBack was not forwarding the withAnimation and focusSearch
parameters when chaining to GoHome.

2) Page focus after load can be triggered too late—when the window is
already hidden—causing CmdPal to regain focus unexpectedly.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-09 13:56:30 -06:00
Jiří Polášek
095961402b CmdPal: Transparent window (#45159)
## Summary

This PR adds:
- Backdrop material customization
  - Alongside acrylic, the following options are now available:
  - Transparent background
  - Mica background
- Background material opacity
  - Lets you control how transparent the background is

## Pictures? Pictures!

<img width="1491" height="928" alt="image"
src="https://github.com/user-attachments/assets/ff4e9e06-fcf1-4f05-bc0a-fb70dc4f39be"
/>



https://github.com/user-attachments/assets/84e83279-afab-481e-b904-f054318c5d2f

<img width="977" height="628" alt="image"
src="https://github.com/user-attachments/assets/241a228d-af3f-448a-94a6-0a282218bd8c"
/>


## PR Checklist

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-09 13:42:01 -06:00
Jiří Polášek
7477b561a1 CmdPal: Add precomputed fuzzy string matching to Command Palette (#44090)
## Summary of the Pull Request

This PR improves fuzzy matching in Command Palette by:
- Precomputing normalized strings to enable faster comparisons
- Reducing memory allocations during matching, effectively down to zero

It also introduces several behavioral improvements:
- Strips diacritics from the normalized search string to improve
matching across languages
- Suppresses the same-case bonus when the query consists entirely of
lowercase characters -- reflecting typical user input patterns
- Allows skipping word separators -- enabling queries like Power Point
to match PowerPoint

This implementation is currently kept internal and is used only on the
home page. For other scenarios, the `FuzzyStringMatcher` from
`Microsoft.CommandPalette.Extensions.Toolkit` is being improved instead.

`PrecomputedFuzzyMatcher` offers up to a 100× performance improvement
over the current `FuzzyStringMatcher`, and approximately 2–5× better
performance compared to the improved version.

The improvement might seem small, but it adds up and becomes quite
noticeable when filtering the entire home page—whether the user starts a
new search or changes the query non-incrementally (e.g., using
backspace).


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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-09 13:37:59 -06:00
Michael Jolley
82671661a8 WinRTExtensionService 2026-02-09 13:21:41 -06:00
Michael Jolley
99a6c8c74e Working on DI 2026-02-05 16:33:20 -06:00
Michael Jolley
285183899f I don't know what I've done. Send help... or Jiri 2026-02-04 16:32:58 -06:00
Michael Jolley
d3d39f91dc DI 2026-02-03 20:42:22 -06:00
Michael Jolley
10f953f684 Working on DI 2026-02-03 16:48:40 -06:00
Michael Jolley
54c5d7b55d Working on DI 2026-02-03 16:48:30 -06:00
Michael Jolley
94a699d00b Working on DI 2026-01-30 22:38:03 -06:00
Michael Jolley
fa4bc0a397 Working on DI 2026-01-29 21:42:53 -06:00
Michael Jolley
303be86d86 Working on DI 2026-01-28 17:00:27 -06:00
Michael Jolley
ec423177da Refactor CmdPal to inject SettingsService instead of SettingsModel
- Update all ViewModels to receive SettingsService via DI
- Implement IDisposable pattern for proper event cleanup
- Add App.Services property for DI access where needed
- Restore GetProviderSettings/GetGlobalFallbacks to SettingsModel
- Fix CommandProviderWrapper and TopLevelCommandManager logging
- Update all Settings pages to use SettingsService
- Add DefaultAppExtensionHost to replace CommandPaletteHost.Instance
- Add parameterless constructors for XAML-instantiated types
- Fix various ILogger injection issues throughout UI layer
2026-01-27 17:33:58 -06:00
Michael Jolley
f78ec0c8e5 Migrate CoreLogger to ILogger with LoggerMessage pattern (partial)
UI.ViewModels:
- CommandSettingsViewModel: Add ILogger injection
- CommandItemViewModel: Add ILogger with LoggerMessage methods
- ContextMenuViewModel: Add ILogger injection
- ExtensionObjectViewModel: Add Logger property with NullLogger default
- ShellViewModel: Replace CoreLogger calls with LoggerMessage methods
- UpdateCommandBarMessage: Remove logging from interface default method

UI helpers/services:
- WallpaperHelper: Add ILogger injection with LoggerMessage methods
- LocalKeyboardListener: Add ILogger injection with LoggerMessage methods
- ImageProvider: Add ILogger injection with LoggerMessage methods
- ThemeService: Add ILogger injection with LoggerMessage methods

Note: UI project migration is partial - XAML code-behind files still use static Logger
2026-01-27 14:05:21 -06:00
Michael Jolley
a1e8b6aca9 Add AppStateService and SettingsService with PersistenceService
- Extract persistence logic into PersistenceService
- Add AppStateService to manage app state
- Add SettingsService to manage settings
- Add JsonSerializationContext for AOT-compatible JSON
- Add EscapeKeyBehavior and MonitorBehavior enums
2026-01-26 18:12:58 -06:00
Michael Jolley
81aeb74fda Use ILogger in AppExtensionHost instead of CoreLogger 2026-01-26 17:00:06 -06:00
Michael Jolley
b5ae2efc0d Rename CoreLogger to CmdPalLogger 2026-01-26 16:56:43 -06:00
Michael Jolley
ba9585a663 Migrate Microsoft.CmdPal.Core.ViewModels into Microsoft.CmdPal.UI.ViewModels
- Move all source files from Core.ViewModels to UI.ViewModels
- Update namespace from Microsoft.CmdPal.Core.ViewModels to Microsoft.CmdPal.UI.ViewModels
- Remove Core.ViewModels project from solution files
- Update all using statements in dependent projects
- Delete the Core folder which is now empty
2026-01-26 16:34:38 -06:00
Michael Jolley
5f273c7be6 Rename Microsoft.CmdPal.Core.Common to Microsoft.CmdPal.Common
- Move project from Core/ subfolder to cmdpal/ root
- Update namespace from Microsoft.CmdPal.Core.Common to Microsoft.CmdPal.Common
- Update all project references and using statements
- Update solution files (PowerToys.slnx, CommandPalette.slnf)
2026-01-26 15:52:52 -06:00
374 changed files with 11523 additions and 2904 deletions

View File

@@ -38,6 +38,7 @@ Gbps
gcode
Heatshrink
Mbits
Kbits
MBs
mkv
msix
@@ -97,6 +98,7 @@ Yubico
Perplexity
Groq
svgl
devhome
# KEYS
@@ -322,6 +324,7 @@ REGSTR
# Misc Win32 APIs and PInvokes
INVOKEIDLIST
MEMORYSTATUSEX
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
DDDD
@@ -342,3 +345,7 @@ reportbug
#ffmpeg
crf
nostdin
# Performance counter keys
engtype
Nonpaged

View File

@@ -192,6 +192,7 @@ ycv
yeelam
Yuniardi
yuyoyuppe
zadjii
Zeol
Zhao
Zhaopeng
@@ -228,6 +229,7 @@ regedit
roslyn
Skia
Spotify
taskmgr
tldr
Vanara
wangyi
@@ -243,4 +245,3 @@ xamlstyler
Xavalon
Xbox
Youdao
zadjii

View File

@@ -111,6 +111,7 @@
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/.*\.TestData\.cs$
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Core\.Common\.UnitTests/Text/.*\.cs$
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$

View File

@@ -197,6 +197,7 @@ Canvascustomlayout
CAPTUREBLT
CAPTURECHANGED
CARETBLINKING
carlos
Carlseibert
CAtl
caub
@@ -217,6 +218,7 @@ certmgr
cfp
CHANGECBCHAIN
changecursor
chatasweetie
checkmarks
CHILDACTIVATE
CHILDWINDOW
@@ -1527,6 +1529,7 @@ randi
RAquadrant
rasterization
Rasterize
rasterizing
RAWINPUTDEVICE
RAWINPUTHEADER
RAWMODE
@@ -2241,6 +2244,7 @@ YSpeed
YStr
YTimer
YVIRTUALSCREEN
zamora
ZEROINIT
zonability
zonable

View File

@@ -64,7 +64,6 @@
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<BuildStlModules>false</BuildStlModules>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<!-- TODO: _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING for compatibility with VS 17.8. Check if we can remove. -->
<PreprocessorDefinitions>_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<!-- CLR + CFG are not compatible >:{ -->

View File

@@ -196,6 +196,10 @@
<Folder Name="/modules/CommandPalette/">
<Project Path="src/modules/cmdpal/CmdPalKeyboardService/CmdPalKeyboardService.vcxproj" Id="5f63c743-f6ce-4dba-a200-2b3f8a14e8c2" />
<Project Path="src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj" Id="0adeb797-c8c7-4ffa-acd5-2af6cad7ecd8" />
<Project Path="src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/CommandPalette/Built-in Extensions/">
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Microsoft.CmdPal.Ext.Apps.csproj">
@@ -219,6 +223,10 @@
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -271,16 +279,6 @@
<Deploy />
</Project>
</Folder>
<Folder Name="/modules/CommandPalette/Core/">
<Project Path="src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Microsoft.CmdPal.Core.Common.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/CommandPalette/Extension SDK/">
<Project Path="src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/Microsoft.CommandPalette.Extensions.Toolkit.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />

View File

@@ -103,10 +103,38 @@ There are <a href="https://learn.microsoft.com/windows/powertoys/install#communi
</details>
## ✨ What's new
**Version 0.97.1 (January 2026)**
**Version 0.97.2 (Feb 2026)**
This patch release fixes several important stability issues identified in v0.97.0 based on incoming reports. Check out the [v0.97.0](https://github.com/microsoft/PowerToys/releases/tag/v0.97.0) notes for the full list of changes.
## Advanced Paste
- #45207 Fixed a crash in the Advanced Paste settings page caused by null values during JSON deserialization.
## Color Picker
- #45367 Fixed contrast issue in Color picker UI.
## Command Palette
- #45194 Fixed an issue where some Command Palette PowerToys Extension strings were not localised.
## Cursor Wrap
- #45210 Fixed "Automatically activate on utility startup" setting not persisting when disabled. Thanks [@ThanhNguyxn](https://github.com/ThanhNguyxn)!
- #45303 Added option to disable Cursor Wrapping when only a single monitor is connected. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
## Image Resizer
- #45184 Fixed Image Resizer not working after upgrading PowerToys on Windows 10 by properly cleaning up legacy sparse app packages.
## LightSwitch
- #45304 Fixed Light Switch startup logic to correctly apply the appropriate theme on launch.
## Workspaces
- #45183 Fixed overlay positioning issue in workspace snapshot draw caused by DPI-aware coordinate mismatch.
## Quick Access and Measure Tool
- #45443 Fixed crash related to `IsShownInSwitchers` property when Explorer is not running.
**Version 0.97.1 (January 2026)**
**Highlights**
### Advanced Paste

197
doc/devdocs/events.md Normal file
View File

@@ -0,0 +1,197 @@
# Telemetry Events
PowerToys collects limited telemetry to understand feature usage, reliability, and product quality. When adding a new telemetry event, follow the steps below to ensure the event is properly declared, documented, and available after release.
**⚠️ Important**: Telemetry must never include personal information, file paths, or usergenerated content.
## Developer Effort Overview (What to Expect)
Adding a telemetry event is a **multi-step process** that typically spans several areas of the codebase and documentation.
At a high level, developers should expect to:
1. Within one PR:
1. Add a new telemetry event(s) to module
1. Add the new event(s) DATA_AND_PRIVACY.md
1. Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s)
### Privacy Guidelines
**NEVER** log:
- User data (text, files, emails, etc.)
- File paths or filenames
- Personal information
- Sensitive system information
- Anything that could identify a specific user
DO log:
- Feature usage (which features, how often)
- Success/failure status
- Timing/performance metrics
- Error types (not error messages with user data)
- Aggregate counts
### Event Naming Convention
Follow this pattern: `UtilityName_EventDescription`
Examples:
- `ColorPicker_Session`
- `FancyZones_LayoutApplied`
- `PowerRename_Rename`
- `AdvancedPaste_FormatClicked`
- `CmdPal_ExtensionInvoked`
## Adding Telemetry Events to PowerToys
PowerToys uses ETW (Event Tracing for Windows) for telemetry in both C++ and C# modules. The telemetry system is:
- Opt-in by default (disabled since v0.86)
- Privacy-focused - never logs personal info, file paths, or user-generated content
- Controlled by registry - HKEY_CURRENT_USER\Software\Classes\PowerToys\AllowDataDiagnostics
### C++ Telemetry Implementation
**Core Components**
| File | Purpose |
| ------------- |:-------------:|
| [ProjectTelemetry.h](../../src/common/Telemetry/ProjectTelemetry.h) | Declares the global ETW provider g_hProvider |
| [TraceBase.h](../../src/common/Telemetry/TraceBase.h) | Base class with RegisterProvider(), UnregisterProvider(), and IsDataDiagnosticsEnabled() check |
| [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Privacy tags and telemetry option group macros
#### Pattern for C++ Modules
1. Create a `Trace` class inheriting from `telemetry::TraceBase` (src/common/Telemetry/TraceBase.h):
```c
// trace.h
#pragma once
#include <common/Telemetry/TraceBase.h>
class Trace : public telemetry::TraceBase
{
public:
static void MyEvent(/* parameters */);
};
```
2. Implement events using `TraceLoggingWriteWrapper`:
```cpp
// trace.cpp
#include "trace.h"
#include <common/Telemetry/TraceBase.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::MyEvent(bool enabled)
{
TraceLoggingWriteWrapper(
g_hProvider,
"ModuleName_EventName", // Event name
TraceLoggingBoolean(enabled, "Enabled"), // Event data
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
```
**Key C++ Telemetry Macros**
| Macro | Purpose |
| ------------- |:-------------:|
| `TraceLoggingWriteWrapper` [CustomAction.cpp](../../installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp) | Wraps `TraceLoggingWrite` with `IsDataDiagnosticsEnabled()` check |
| `ProjectTelemetryPrivacyDataTag(tag)` [TraceLoggingDefines.h](../../src/common/Telemetry/TraceLoggingDefines.h) | Sets privacy classification |
### C# Telemetry Implementation
**Core Components**
| File | Purpose |
| ------------- |:-------------:|
| [PowerToysTelemetry.cs](../../src/common/ManagedTelemetry/Telemetry/PowerToysTelemetry.cs) | Singleton `Log` instance with `WriteEvent<T>()` method |
| [EventBase.cs](../../src/common/ManagedTelemetry/Telemetry/Events/EventBase.cs) | Base class for all events (provides `EventName`, `Version`) |
| [IEvent.cs](../../src/common/ManagedTelemetry/Telemetry/Events/IEvent.cs) | Interface requiring `PartA_PrivTags` property |
| [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Inherits from `EventSource`, defines ETW constants |
| [DataDiagnosticsSettings.cs](../../src/common/ManagedTelemetry/Telemetry/DataDiagnosticsSettings.cs) | Registry-based enable/disable check
#### Pattern for C# Modules
1. Create an event class inheriting from `EventBase` and implementing `IEvent`:
```csharp
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using Microsoft.PowerToys.Telemetry;
using Microsoft.PowerToys.Telemetry.Events;
namespace MyModule.Telemetry
{
[EventData]
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
public class MyModuleEvent : EventBase, IEvent
{
// Event properties (logged as telemetry data)
public string SomeProperty { get; set; }
public int SomeValue { get; set; }
// Required: Privacy tag
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
// Optional: Set EventName in constructor (defaults to class name)
public MyModuleEvent(string prop, int val)
{
EventName = "MyModule_EventName";
SomeProperty = prop;
SomeValue = val;
}
}
}
```
2. Log the event:
```csharp
PowerToysTelemetry.Log.WriteEvent(new MyModuleEvent("value", 42));
```
**Privacy Tags (C#)**
| Tag | Use Case |
| ------------- |:-------------:|
| `PartA_PrivTags.ProductAndServiceUsage` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Feature usage events
| `PartA_PrivTags.ProductAndServicePerformance` [TelemetryBase.cs](../../src/common/Telemetry/TelemetryBase.cs) | Performance/timing events
### Update DATA_AND_PRIVACY.md file
Add your new event(s) to [DATA_AND_PRIVACY.md](../../DATA_AND_PRIVACY.md).
## Launch Product Version Containing the new events
Events do not become active until they ship in a released PowerToys version. After your PRs are merged:
- The event will begin firing once users install the version that includes it
- In order for PowerToys to process these events, you must complete the next section
## Next Steps
Reach out to @carlos-zamora or @chatasweetie so internal scripts can process new event(s).
## Summary
Required steps:
1. In one PR:
- Add the event(s) in code
- Document event(s) in DATA_AND_PRIVACY.md
1. Ship the change in a PowerToys release
1. Reach out for next steps

View File

@@ -88,7 +88,7 @@
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>inc;..\..\src\;..\..\src\common\Telemetry;telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalOptions>/await /Zc:twoPhase- /Wv:18 %(AdditionalOptions)</AdditionalOptions>
<AdditionalOptions>/Zc:twoPhase- /Wv:18 %(AdditionalOptions)</AdditionalOptions>
<WarningLevel>Level4</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
</ClCompile>

View File

@@ -57,7 +57,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
auto state = UpdateState::read();
const auto new_version_info = get_github_version_info_async().get();
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;
@@ -76,7 +76,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = download_new_version(std::get<new_version_download_info>(*new_version_info)).get();
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");

View File

@@ -34,8 +34,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
}
// Check if model is in catalog
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
if (!isInCatalog)
if (!EnsureModelInCatalog(modelId))
{
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
Logger.LogError($"[FoundryLocal] {errorMessage}");
@@ -43,15 +42,28 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
}
// Ensure the model is loaded before returning chat client
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
var isLoaded = EnsureModelLoadedWithRefresh(modelId);
if (!isLoaded)
{
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
}
var client = _foundryClient;
if (client == null)
{
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
Logger.LogError($"[FoundryLocal] {message}");
throw new InvalidOperationException(message);
}
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
var baseUri = _foundryClient.GetServiceUri();
var baseUri = client.GetServiceUri();
if (baseUri == null && TryRefreshClient("Service URI was not available"))
{
baseUri = _foundryClient?.GetServiceUri();
}
if (baseUri == null)
{
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
@@ -124,6 +136,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
{
await _foundryClient.EnsureRunning().ConfigureAwait(false);
_serviceUrl = await _foundryClient.GetServiceUrl().ConfigureAwait(false);
return;
}
@@ -153,4 +166,75 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
Logger.LogInfo($"[FoundryLocal] Available: {available}");
return available;
}
private bool EnsureModelInCatalog(string modelId)
{
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
if (isInCatalog)
{
return true;
}
Logger.LogWarning($"[FoundryLocal] Model not found in catalog. Refreshing client for model: {modelId}");
if (!TryRefreshClient("Model not in catalog"))
{
return false;
}
return _catalogModels?.Any(m => m.Name == modelId) ?? false;
}
private bool EnsureModelLoadedWithRefresh(string modelId)
{
var isLoaded = false;
try
{
isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogWarning($"[FoundryLocal] EnsureModelLoaded failed: {ex.Message}");
}
if (isLoaded)
{
return true;
}
if (!TryRefreshClient("EnsureModelLoaded failed"))
{
return false;
}
try
{
return _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogError($"[FoundryLocal] EnsureModelLoaded failed after refresh: {ex.Message}", ex);
return false;
}
}
private bool TryRefreshClient(string reason)
{
Logger.LogInfo($"[FoundryLocal] Refreshing Foundry Local client: {reason}");
try
{
_foundryClient = null;
_catalogModels = null;
_serviceUrl = null;
InitializeAsync().GetAwaiter().GetResult();
return _foundryClient != null;
}
catch (Exception ex)
{
Logger.LogError($"[FoundryLocal] Failed to refresh Foundry Local client: {ex.Message}", ex);
return false;
}
}
}

View File

@@ -18,7 +18,7 @@ namespace // Strings in this namespace should not be localized
namespace updating
{
std::future<bool> uninstall_previous_msix_version_async()
winrt::Windows::Foundation::IAsyncOperation<bool> uninstall_previous_msix_version_async()
{
winrt::Windows::Management::Deployment::PackageManager package_manager;

View File

@@ -2,11 +2,11 @@
#include <string>
#include <optional>
#include <future>
#include <winrt/Windows.Foundation.h>
#include <common/version/helper.h>
namespace updating
{
std::future<bool> uninstall_previous_msix_version_async();
}
winrt::Windows::Foundation::IAsyncOperation<bool> uninstall_previous_msix_version_async();
}

View File

@@ -33,6 +33,7 @@
#include <winrt/Windows.System.h>
#include <wil/resource.h>
#include <wil/coroutine.h>
#endif //PCH_H

View File

@@ -82,11 +82,7 @@ namespace updating
// prevent the warning that may show up depend on the value of the constants (#defines)
#pragma warning(push)
#pragma warning(disable : 4702)
#if USE_STD_EXPECTED
std::future<std::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease)
#else
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease)
#endif
wil::task<github_version_result> get_github_version_info_async(const bool prerelease)
{
// If the current version starts with 0.0.*, it means we're on a local build from a farm and shouldn't check for updates.
if constexpr (VERSION_MAJOR == 0 && VERSION_MINOR == 0)
@@ -170,7 +166,7 @@ namespace updating
return !ec ? std::optional{ installer_download_path } : std::nullopt;
}
std::future<std::optional<std::filesystem::path>> download_new_version(const new_version_download_info& new_version)
wil::task<std::optional<std::filesystem::path>> download_new_version_async(new_version_download_info new_version)
{
auto installer_download_path = create_download_path();
if (!installer_download_path)

View File

@@ -2,7 +2,6 @@
#include <optional>
#include <string>
#include <future>
#include <filesystem>
#include <variant>
#include <winrt/Windows.Foundation.h>
@@ -16,6 +15,7 @@
#endif
#include <common/version/helper.h>
#include <wil/coroutine.h>
namespace updating
{
@@ -32,13 +32,15 @@ namespace updating
};
using github_version_info = std::variant<new_version_download_info, version_up_to_date>;
std::future<std::optional<std::filesystem::path>> download_new_version(const new_version_download_info& new_version);
std::filesystem::path get_pending_updates_path();
#if USE_STD_EXPECTED
std::future<std::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
using github_version_result = std::expected<github_version_info, std::wstring>;
#else
std::future<nonstd::expected<github_version_info, std::wstring>> get_github_version_info_async(const bool prerelease = false);
using github_version_result = nonstd::expected<github_version_info, std::wstring>;
#endif
wil::task<github_version_result> get_github_version_info_async(bool prerelease = false);
wil::task<std::optional<std::filesystem::path>> download_new_version_async(new_version_download_info new_version);
std::filesystem::path get_pending_updates_path();
void cleanup_updates();
// non-localized

View File

@@ -1,6 +1,7 @@
#pragma once
#include <future>
#include <functional>
#include <string>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Storage.Streams.h>
#include <winrt/Windows.Web.Http.h>
@@ -21,15 +22,15 @@ namespace http
headers.UserAgent().TryParseAdd(USER_AGENT);
}
std::future<std::wstring> request(const winrt::Windows::Foundation::Uri& url)
winrt::Windows::Foundation::IAsyncOperation<winrt::hstring> request(winrt::Windows::Foundation::Uri url)
{
auto response = co_await m_client.GetAsync(url);
(void)response.EnsureSuccessStatusCode();
auto body = co_await response.Content().ReadAsStringAsync();
co_return std::wstring(body);
co_return body;
}
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath)
winrt::Windows::Foundation::IAsyncAction download(winrt::Windows::Foundation::Uri url, std::wstring dstFilePath)
{
auto response = co_await m_client.GetAsync(url);
(void)response.EnsureSuccessStatusCode();
@@ -38,7 +39,7 @@ namespace http
file_stream.Close();
}
std::future<void> download(const winrt::Windows::Foundation::Uri& url, const std::wstring& dstFilePath, const std::function<void(float)>& progressUpdateCallback)
winrt::Windows::Foundation::IAsyncAction download(winrt::Windows::Foundation::Uri url, std::wstring dstFilePath, std::function<void(float)> progressUpdateCallback)
{
auto response = co_await m_client.GetAsync(url, HttpCompletionOption::ResponseHeadersRead);
response.EnsureSuccessStatusCode();

View File

@@ -13,7 +13,6 @@
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>

View File

@@ -13,7 +13,6 @@
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>

View File

@@ -13,7 +13,6 @@
<ConformanceMode>false</ConformanceMode>
<TreatWarningAsError>true</TreatWarningAsError>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalOptions>/await %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>

View File

@@ -5140,7 +5140,7 @@ bool IsPenInverted( WPARAM wParam )
// Captures the specified screen using the capture APIs
//
//----------------------------------------------------------------------------
std::future<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat)
wil::task<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDirect3DDevice const& device, winrt::GraphicsCaptureItem const& item, winrt::DirectXPixelFormat const& pixelFormat)
{
auto d3dDevice = GetDXGIInterfaceFromObject<ID3D11Device>(device);
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
@@ -5176,9 +5176,7 @@ std::future<winrt::com_ptr<ID3D11Texture2D>> CaptureScreenshotAsync(winrt::IDire
framePool.Close();
auto texture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
auto result = util::CopyD3DTexture(d3dDevice, texture, true);
co_return result;
co_return util::CopyD3DTexture(d3dDevice, texture, true);
}
//----------------------------------------------------------------------------
@@ -5205,10 +5203,7 @@ winrt::com_ptr<ID3D11Texture2D>CaptureScreenshot(winrt::DirectXPixelFormat const
auto item = util::CreateCaptureItemForMonitor(hMon);
auto capture = CaptureScreenshotAsync(device, item, pixelFormat);
capture.wait();
return capture.get();
return CaptureScreenshotAsync(device, item, pixelFormat).get();
}

View File

@@ -69,6 +69,7 @@
// WIL
#include <wil/com.h>
#include <wil/resource.h>
#include <wil/coroutine.h>
// DirectX
#include <d3d11_4.h>

View File

@@ -10,8 +10,7 @@
"src\\common\\version\\version.vcxproj",
"src\\modules\\cmdpal\\CmdPalKeyboardService\\CmdPalKeyboardService.vcxproj",
"src\\modules\\cmdpal\\CmdPalModuleInterface\\CmdPalModuleInterface.vcxproj",
"src\\modules\\cmdpal\\Core\\Microsoft.CmdPal.Core.Common\\Microsoft.CmdPal.Core.Common.csproj",
"src\\modules\\cmdpal\\Core\\Microsoft.CmdPal.Core.ViewModels\\Microsoft.CmdPal.Core.ViewModels.csproj",
"src\\modules\\cmdpal\\Microsoft.CmdPal.Common\\Microsoft.CmdPal.Common.csproj",
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI.ViewModels\\Microsoft.CmdPal.UI.ViewModels.csproj",
"src\\modules\\cmdpal\\Microsoft.CmdPal.UI\\Microsoft.CmdPal.UI.csproj",
"src\\modules\\cmdpal\\Microsoft.Terminal.UI\\Microsoft.Terminal.UI.vcxproj",

View File

@@ -1,62 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.CmdPal.Core.Common;
public static class CoreLogger
{
public static void InitializeLogger(ILogger implementation)
{
_logger = implementation;
}
private static ILogger? _logger;
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);
}
public static void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
_logger?.LogError(message, memberName, sourceFilePath, sourceLineNumber);
}
public static void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
_logger?.LogWarning(message, memberName, sourceFilePath, sourceLineNumber);
}
public static void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
_logger?.LogInfo(message, memberName, sourceFilePath, sourceLineNumber);
}
public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
_logger?.LogDebug(message, memberName, sourceFilePath, sourceLineNumber);
}
public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
_logger?.LogTrace(memberName, sourceFilePath, sourceLineNumber);
}
}
public interface ILogger
{
void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0);
}

View File

@@ -1,26 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\CoreCommonProps.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Core.Common</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -1,37 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation;
namespace Microsoft.CmdPal.Core.Common.Services;
public interface IExtensionService
{
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(bool includeDisabledExtensions = false);
// Task<IEnumerable<string>> GetInstalledHomeWidgetPackageFamilyNamesAsync(bool includeDisabledExtensions = false);
Task<IEnumerable<IExtensionWrapper>> GetInstalledExtensionsAsync(Microsoft.CommandPalette.Extensions.ProviderType providerType, bool includeDisabledExtensions = false);
IExtensionWrapper? GetInstalledExtension(string extensionUniqueId);
Task SignalStopExtensionsAsync();
event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;
event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionRemoved;
void EnableExtension(string extensionUniqueId);
void DisableExtension(string extensionUniqueId);
///// <summary>
///// Gets a boolean indicating whether the extension was disabled due to the corresponding Windows optional feature
///// being absent from the machine or in an unknown state.
///// </summary>
///// <param name="extension">The out of proc extension object</param>
///// <returns>True only if the extension was disabled. False otherwise.</returns>
// public Task<bool> DisableExtensionIfWindowsFeatureNotAvailable(IExtensionWrapper extension);
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels;
/// <summary>
/// Encapsulates a navigation request within Command Palette view models.
/// </summary>
/// <param name="TargetViewModel">A view model that should be navigated to.</param>
/// <param name="NavigationToken"> A <see cref="CancellationToken"/> that can be used to cancel the pending navigation.</param>
public record AsyncNavigationRequest(object? TargetViewModel, CancellationToken NavigationToken);

View File

@@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\CoreCommonProps.props" />
<PropertyGroup>
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Common" />
<PackageReference Include="CommunityToolkit.Mvvm" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@@ -1,72 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Core.ViewModels.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.ViewModels.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Show details.
/// </summary>
public static string ShowDetailsCommand {
get {
return ResourceManager.GetString("ShowDetailsCommand", resourceCulture);
}
}
}
}

View File

@@ -1,124 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ShowDetailsCommand" xml:space="preserve">
<value>Show details</value>
<comment>Name for the command that shows details of an item</comment>
</data>
</root>

View File

@@ -0,0 +1,26 @@
// 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 Microsoft.CmdPal.Core.Common;
/// <summary>
/// Represents the packaging flavor of the application.
/// </summary>
public enum AppPackagingFlavor
{
/// <summary>
/// Application is packaged as a Windows MSIX package.
/// </summary>
Packaged,
/// <summary>
/// Application is running unpackaged (native executable).
/// </summary>
Unpackaged,
/// <summary>
/// Application is running as unpackaged portable (self-contained distribution).
/// </summary>
UnpackagedPortable,
}

View File

@@ -0,0 +1,167 @@
// 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 ManagedCommon;
using Microsoft.Extensions.Logging;
namespace Microsoft.CmdPal.Common;
// Adapter implementing Microsoft.Extensions.Logging.ILogger,
// delegating to ManagedCommon.Logger.
public sealed partial class CmdPalLogger : ILogger
{
private static readonly AsyncLocal<Stack<object>> _scopeStack = new();
private readonly LogLevel _minLevel;
public string CurrentVersionLogDirectoryPath => Logger.CurrentVersionLogDirectoryPath;
public CmdPalLogger(LogLevel minLevel = LogLevel.Information)
{
_minLevel = minLevel;
// Ensure underlying logger initialized (idempotent if already done elsewhere).
Logger.InitializeLogger("\\CmdPal\\Logs\\");
}
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None && logLevel >= _minLevel;
public IDisposable? BeginScope<TState>(TState state)
where TState : notnull
{
var stack = _scopeStack.Value;
if (stack is null)
{
stack = new Stack<object>();
_scopeStack.Value = stack;
}
stack.Push(state);
return new Scope(stack);
}
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 = formatter(state, exception);
if (string.IsNullOrEmpty(message) && exception is null)
{
return;
}
var scopeSuffix = BuildScopeSuffix();
var eventPrefix = eventId.Id != 0 ? $"[{eventId.Id}/{eventId.Name}] " : string.Empty;
var finalMessage = $"{eventPrefix}{message}{scopeSuffix}";
switch (logLevel)
{
case LogLevel.Trace:
// Existing stack: Trace logs an empty line; append message via Debug.
Logger.LogTrace();
if (!string.IsNullOrEmpty(message))
{
Logger.LogDebug(finalMessage);
}
if (exception is not null)
{
Logger.LogError(exception.Message, exception);
}
break;
case LogLevel.Debug:
Logger.LogDebug(finalMessage);
if (exception is not null)
{
Logger.LogError(exception.Message, exception);
}
break;
case LogLevel.Information:
Logger.LogInfo(finalMessage);
if (exception is not null)
{
Logger.LogError(exception.Message, exception);
}
break;
case LogLevel.Warning:
Logger.LogWarning(finalMessage);
if (exception is not null)
{
Logger.LogError(exception.Message, exception);
}
break;
case LogLevel.Error:
case LogLevel.Critical:
if (exception is not null)
{
Logger.LogError(finalMessage, exception);
}
else
{
Logger.LogError(finalMessage);
}
break;
case LogLevel.None:
default:
break;
}
}
private static string BuildScopeSuffix()
{
var stack = _scopeStack.Value;
if (stack is null || stack.Count == 0)
{
return string.Empty;
}
// Show most-recent first.
return $" [Scopes: {string.Join(" => ", stack.ToArray())}]";
}
private sealed partial class Scope : IDisposable
{
private readonly Stack<object> _stack;
private bool _disposed;
public Scope(Stack<object> stack) => _stack = stack;
public void Dispose()
{
if (_disposed)
{
return;
}
if (_stack.Count > 0)
{
_stack.Pop();
}
_disposed = true;
}
}
}

View File

@@ -5,7 +5,7 @@
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Provides utility methods for building diagnostic and error messages.

View File

@@ -7,7 +7,7 @@ using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.Common;
namespace Microsoft.CmdPal.Common;
public partial class ExtensionHostInstance
{

View File

@@ -0,0 +1,27 @@
// 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.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Core.Common.Helpers;
/// <summary>
/// Represents an item that can provide precomputed fuzzy matching targets for its title and subtitle.
/// </summary>
public interface IPrecomputedListItem
{
/// <summary>
/// Gets the fuzzy matching target for the item's title.
/// </summary>
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
/// <returns>The fuzzy target for the title.</returns>
FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher);
/// <summary>
/// Gets the fuzzy matching target for the item's subtitle.
/// </summary>
/// <param name="matcher">The precomputed fuzzy matcher used to build the target.</param>
/// <returns>The fuzzy target for the subtitle.</returns>
FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher);
}

View File

@@ -4,7 +4,7 @@
using System.Threading;
namespace Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Thread-safe boolean implementation using atomic operations

View File

@@ -0,0 +1,142 @@
// 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.Buffers;
using System.Diagnostics;
using Microsoft.CmdPal.Core.Common.Text;
namespace Microsoft.CmdPal.Core.Common.Helpers;
public static partial class InternalListHelpers
{
public static RoScored<T>[] FilterListWithScores<T>(
IEnumerable<T>? items,
in FuzzyQuery query,
in ScoringFunction<T> scoreFunction)
{
if (items == null)
{
return [];
}
// Try to get initial capacity hint
var initialCapacity = items switch
{
ICollection<T> col => col.Count,
IReadOnlyCollection<T> rc => rc.Count,
_ => 64,
};
var buffer = ArrayPool<RoScored<T>>.Shared.Rent(initialCapacity);
var count = 0;
try
{
foreach (var item in items)
{
var score = scoreFunction(in query, item);
if (score <= 0)
{
continue;
}
if (count == buffer.Length)
{
GrowBuffer(ref buffer, count);
}
buffer[count++] = new RoScored<T>(item, score);
}
Array.Sort(buffer, 0, count, default(RoScoredDescendingComparer<T>));
var result = GC.AllocateUninitializedArray<RoScored<T>>(count);
buffer.AsSpan(0, count).CopyTo(result);
return result;
}
finally
{
ArrayPool<RoScored<T>>.Shared.Return(buffer);
}
}
private static void GrowBuffer<T>(ref RoScored<T>[] buffer, int count)
{
var newBuffer = ArrayPool<RoScored<T>>.Shared.Rent(buffer.Length * 2);
buffer.AsSpan(0, count).CopyTo(newBuffer);
ArrayPool<RoScored<T>>.Shared.Return(buffer);
buffer = newBuffer;
}
public static T[] FilterList<T>(IEnumerable<T> items, in FuzzyQuery query, ScoringFunction<T> scoreFunction)
{
// Try to get initial capacity hint
var initialCapacity = items switch
{
ICollection<T> col => col.Count,
IReadOnlyCollection<T> rc => rc.Count,
_ => 64,
};
var buffer = ArrayPool<RoScored<T>>.Shared.Rent(initialCapacity);
var count = 0;
try
{
foreach (var item in items)
{
var score = scoreFunction(in query, item);
if (score <= 0)
{
continue;
}
if (count == buffer.Length)
{
GrowBuffer(ref buffer, count);
}
buffer[count++] = new RoScored<T>(item, score);
}
Array.Sort(buffer, 0, count, default(RoScoredDescendingComparer<T>));
var result = GC.AllocateUninitializedArray<T>(count);
for (var i = 0; i < count; i++)
{
result[i] = buffer[i].Item;
}
return result;
}
finally
{
ArrayPool<RoScored<T>>.Shared.Return(buffer);
}
}
private readonly struct RoScoredDescendingComparer<T> : IComparer<RoScored<T>>
{
public int Compare(RoScored<T> x, RoScored<T> y) => y.Score.CompareTo(x.Score);
}
}
public delegate int ScoringFunction<in T>(in FuzzyQuery query, T item);
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public readonly struct RoScored<T>
{
public readonly int Score;
public readonly T Item;
public RoScored(T item, int score)
{
Score = score;
Item = item;
}
private string GetDebuggerDisplay()
{
return "Score = " + Score + ", Item = " + Item;
}
}

View File

@@ -7,7 +7,7 @@ using System.Threading;
using Microsoft.UI.Dispatching;
namespace Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Common.Helpers;
public static partial class NativeEventWaiter
{

View File

@@ -6,7 +6,7 @@ using System.Runtime.CompilerServices;
using Windows.Win32;
using Windows.Win32.Storage.FileSystem;
namespace Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Common.Helpers;
public static class PathHelper
{

View File

@@ -6,7 +6,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// An async gate that ensures only one operation runs at a time.

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// An async gate that ensures only one value computation runs at a time.

View File

@@ -0,0 +1,94 @@
// 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.Diagnostics;
using Microsoft.Extensions.Logging;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Helper class for retrieving application version information safely.
/// </summary>
internal static class VersionHelper
{
/// <summary>
/// Gets the application version as a string in the format "Major.Minor.Build.Revision".
/// Falls back to assembly version if packaged version is unavailable, and returns a default value if both fail.
/// </summary>
/// <returns>The application version string, or a fallback value if retrieval fails.</returns>
public static string GetAppVersionSafe()
{
if (TryGetPackagedVersion(out var version))
{
return version;
}
if (TryGetAssemblyVersion(out version))
{
return version;
}
return "?";
}
/// <summary>
/// Attempts to retrieve the application version from the package manifest.
/// </summary>
/// <param name="version">The version string if successful, or an empty string if unsuccessful.</param>
/// <returns>True if the version was retrieved successfully; otherwise, false.</returns>
private static bool TryGetPackagedVersion(out string version, ILogger logger)
{
version = string.Empty;
try
{
// Package.Current throws InvalidOperationException if the app is not packaged
var v = Package.Current.Id.Version;
version = $"{v.Major}.{v.Minor}.{v.Build}.{v.Revision}";
return true;
}
catch (InvalidOperationException)
{
return false;
}
catch (Exception ex)
{
Log_FailedToGetVersion(logger, ex);
return false;
}
}
/// <summary>
/// Attempts to retrieve the application version from the executable file.
/// </summary>
/// <param name="version">The version string if successful, or an empty string if unsuccessful.</param>
/// <returns>True if the version was retrieved successfully; otherwise, false.</returns>
private static bool TryGetAssemblyVersion(out string version, ILogger logger)
{
version = string.Empty;
try
{
var processPath = Environment.ProcessPath;
if (string.IsNullOrEmpty(processPath))
{
return false;
}
var info = FileVersionInfo.GetVersionInfo(processPath);
version = $"{info.FileMajorPart}.{info.FileMinorPart}.{info.FileBuildPart}.{info.FilePrivatePart}";
return true;
}
catch (Exception ex)
{
Log_FailedToGetVersionFromExe(logger, ex);
return false;
}
}
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to get version from the package")]
static partial void Log_FailedToGetVersion(ILogger logger, Exception ex);
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to get version from the executable")]
static partial void Log_FailedToGetVersionFromExe(ILogger logger, Exception ex);
}

View File

@@ -7,7 +7,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.Core.Common.Helpers;
namespace Microsoft.CmdPal.Common.Helpers;
/// <summary>
/// Well-known key chords used in the Command Palette and extensions.

View File

@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\CoreCommonProps.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Common</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,164 @@
// 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.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization.Metadata;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.Logging;
namespace Microsoft.CmdPal.Common;
public partial class PersistenceService
{
private static bool TryParseJsonObject(string json, ILogger logger, [NotNullWhen(true)] out JsonObject? obj)
{
obj = null;
try
{
obj = JsonNode.Parse(json) as JsonObject;
return obj is not null;
}
catch (Exception ex)
{
Log_PersistenceParseFailure(logger, ex);
return false;
}
}
private static bool TryReadSavedObject(string filePath, ILogger logger, [NotNullWhen(true)] out JsonObject? saved)
{
saved = null;
string oldContent;
try
{
if (!File.Exists(filePath))
{
saved = new JsonObject();
return true;
}
oldContent = File.ReadAllText(filePath);
}
catch (Exception ex)
{
Log_PersistenceReadFileFailure(logger, filePath, ex);
return false;
}
if (string.IsNullOrWhiteSpace(oldContent))
{
Log_FileEmpty(logger, filePath);
return false;
}
return TryParseJsonObject(oldContent, logger, out saved);
}
public static T LoadObject<T>(string filePath, JsonTypeInfo<T> typeInfo, ILogger logger)
where T : new()
{
if (string.IsNullOrEmpty(filePath))
{
throw new InvalidOperationException($"You must set a valid file path before loading {typeof(T).Name}");
}
if (!File.Exists(filePath))
{
Log_FileDoesntExist(logger, typeof(T).Name, filePath);
return new T();
}
try
{
var jsonContent = File.ReadAllText(filePath);
var loaded = JsonSerializer.Deserialize(jsonContent, typeInfo);
return loaded ?? new T();
}
catch (Exception ex)
{
Log_PersistenceReadFailure(logger, typeof(T).Name, filePath, ex);
return new T();
}
}
public static void SaveObject<T>(
T model,
string filePath,
JsonTypeInfo<T> typeInfo,
JsonSerializerOptions optionsForWrite,
Action<JsonObject>? beforeWriteMutation,
Action<T>? afterWriteCallback,
ILogger logger)
{
if (string.IsNullOrEmpty(filePath))
{
throw new InvalidOperationException($"You must set a valid file path before saving {typeof(T).Name}");
}
try
{
var json = JsonSerializer.Serialize(model, typeInfo);
if (!TryParseJsonObject(json, logger, out var newObj))
{
Log_SerializationError(logger, typeof(T).Name);
return;
}
if (!TryReadSavedObject(filePath, logger, out var savedObj))
{
savedObj = new JsonObject();
}
foreach (var kvp in newObj)
{
savedObj[kvp.Key] = kvp.Value?.DeepClone();
}
beforeWriteMutation?.Invoke(savedObj);
var serialized = savedObj.ToJsonString(optionsForWrite);
File.WriteAllText(filePath, serialized);
afterWriteCallback?.Invoke(model);
}
catch (Exception ex)
{
Log_PersistenceSaveFailure(logger, typeof(T).Name, filePath, ex);
}
}
public static string SettingsJsonPath(string fileName)
{
var directory = Utilities.BaseSettingsPath("Microsoft.CommandPalette");
Directory.CreateDirectory(directory);
// now, the settings is just next to the exe
return Path.Combine(directory, fileName);
}
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to save {typeName} to '{filePath}'.")]
static partial void Log_PersistenceSaveFailure(ILogger logger, string typeName, string filePath, Exception exception);
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to read {typeName} from '{filePath}'.")]
static partial void Log_PersistenceReadFailure(ILogger logger, string typeName, string filePath, Exception exception);
[LoggerMessage(Level = LogLevel.Debug, Message = "Failed to serialize {typeName} to JsonObject.")]
static partial void Log_SerializationError(ILogger logger, string typeName);
[LoggerMessage(Level = LogLevel.Debug, Message = "The provided {typeName} file does not exist ({filePath})")]
static partial void Log_FileDoesntExist(ILogger logger, string typeName, string filePath);
[LoggerMessage(Level = LogLevel.Debug, Message = "The file at '{filePath}' is empty.")]
static partial void Log_FileEmpty(ILogger logger, string filePath);
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to read file at '{filePath}'.")]
static partial void Log_PersistenceReadFileFailure(ILogger logger, string filePath, Exception exception);
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to parse persisted JSON.")]
static partial void Log_PersistenceParseFailure(ILogger logger, Exception exception);
}

View File

@@ -8,7 +8,7 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Core.Common.Properties {
namespace Microsoft.CmdPal.Common.Properties {
using System;
@@ -22,7 +22,7 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
@@ -36,10 +36,10 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.Common.Properties.Resources", typeof(Resources).Assembly);
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Common.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
@@ -51,7 +51,7 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
@@ -67,7 +67,7 @@ namespace Microsoft.CmdPal.Core.Common.Properties {
///
///(While youre at it, give the details below a quick skim — just to make sure theres nothing personal youd prefer not to share. Its rare, but sometimes little surprises sneak in.).
/// </summary>
internal static string ErrorReport_Global_Preamble {
public static string ErrorReport_Global_Preamble {
get {
return ResourceManager.GetString("ErrorReport_Global_Preamble", resourceCulture);
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.Logging;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Core.Common.Services;
/// <summary>
/// Implementation of IApplicationInfoService providing application-wide information.
/// </summary>
public sealed partial class ApplicationInfoService : IApplicationInfoService
{
private readonly Lazy<string> _configDirectory = new(() => Utilities.BaseSettingsPath("Microsoft.CmdPal"));
private readonly Lazy<bool> _isElevated;
private readonly Lazy<string> _logDirectory;
private readonly Lazy<AppPackagingFlavor> _packagingFlavor;
private readonly ILogger _logger;
private Func<string>? _getLogDirectory;
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationInfoService"/> class.
/// The log directory delegate can be set later via <see cref="SetLogDirectory(Func{string})"/>.
/// </summary>
public ApplicationInfoService(ILogger logger)
{
_logger = logger;
_packagingFlavor = new Lazy<AppPackagingFlavor>(DeterminePackagingFlavor);
_isElevated = new Lazy<bool>(DetermineElevationStatus);
_logDirectory = new Lazy<string>(() => _getLogDirectory?.Invoke() ?? "Not available");
}
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationInfoService"/> class with an optional log directory provider.
/// </summary>
/// <param name="getLogDirectory">Optional delegate to retrieve the log directory path. If not provided, the log directory will be unavailable.</param>
public ApplicationInfoService(
Func<string>? getLogDirectory,
ILogger logger)
: this(logger)
{
_getLogDirectory = getLogDirectory;
_logger = logger;
}
/// <summary>
/// Sets the log directory delegate to be used for retrieving the log directory path.
/// This allows deferred initialization of the logger path.
/// </summary>
/// <param name="getLogDirectory">Delegate to retrieve the log directory path.</param>
public void SetLogDirectory(Func<string> getLogDirectory)
{
ArgumentNullException.ThrowIfNull(getLogDirectory);
_getLogDirectory = getLogDirectory;
}
public string AppVersion => VersionHelper.GetAppVersionSafe();
public AppPackagingFlavor PackagingFlavor => _packagingFlavor.Value;
public string LogDirectory => _logDirectory.Value;
public string ConfigDirectory => _configDirectory.Value;
public bool IsElevated => _isElevated.Value;
public string GetApplicationInfoSummary()
{
return $"""
Application:
App version: {AppVersion}
Packaging flavor: {PackagingFlavor}
Is elevated: {(IsElevated ? "yes" : "no")}
Environment:
OS version: {RuntimeInformation.OSDescription}
OS architecture: {RuntimeInformation.OSArchitecture}
Runtime identifier: {RuntimeInformation.RuntimeIdentifier}
Framework: {RuntimeInformation.FrameworkDescription}
Process architecture: {RuntimeInformation.ProcessArchitecture}
Culture: {CultureInfo.CurrentCulture.Name}
UI culture: {CultureInfo.CurrentUICulture.Name}
Paths:
Log directory: {LogDirectory}
Config directory: {ConfigDirectory}
""";
}
private AppPackagingFlavor DeterminePackagingFlavor()
{
// Try to determine if running as packaged
try
{
// If this doesn't throw, we're packaged
_ = Package.Current.Id.Version;
return AppPackagingFlavor.Packaged;
}
catch (InvalidOperationException)
{
// Not packaged, check if portable
// For now, we don't support portable yet, so return Unpackaged
// In the future, check for a marker file or environment variable
return AppPackagingFlavor.Unpackaged;
}
catch (Exception ex)
{
Log_FailedToDeterminePackagingFlavor(ex);
return AppPackagingFlavor.Unpackaged;
}
}
private static bool DetermineElevationStatus()
{
try
{
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
return isElevated;
}
catch (Exception)
{
return false;
}
}
[LoggerMessage(Level = LogLevel.Error, Message = "Failed to determine packaging flavor")]
partial void Log_FailedToDeterminePackagingFlavor(Exception ex);
}

View File

@@ -0,0 +1,49 @@
// 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 Microsoft.CmdPal.Core.Common.Services;
/// <summary>
/// Provides access to application-wide information such as version, packaging flavor, and directory paths.
/// </summary>
public interface IApplicationInfoService
{
/// <summary>
/// Gets the application version as a string in the format "Major.Minor.Build.Revision".
/// </summary>
string AppVersion { get; }
/// <summary>
/// Gets the packaging flavor of the application.
/// </summary>
AppPackagingFlavor PackagingFlavor { get; }
/// <summary>
/// Gets the directory path where application logs are stored.
/// </summary>
string LogDirectory { get; }
/// <summary>
/// Gets the directory path where application configuration files are stored.
/// </summary>
string ConfigDirectory { get; }
/// <summary>
/// Gets a value indicating whether the application is running with administrator privileges.
/// </summary>
bool IsElevated { get; }
/// <summary>
/// Gets a formatted summary of application information suitable for logging.
/// </summary>
/// <returns>A formatted string containing application information.</returns>
string GetApplicationInfoSummary();
/// <summary>
/// Sets the log directory delegate to be used for retrieving the log directory path.
/// This allows deferred initialization of the logger path.
/// </summary>
/// <param name="getLogDirectory">Delegate to retrieve the log directory path.</param>
void SetLogDirectory(Func<string> getLogDirectory);
}

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.Common.Services;
namespace Microsoft.CmdPal.Common.Services;
public interface IRunHistoryService
{

View File

@@ -2,20 +2,27 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Principal;
using Microsoft.CmdPal.Core.Common.Services.Sanitizer;
using Windows.ApplicationModel;
namespace Microsoft.CmdPal.Core.Common.Services.Reports;
public sealed class ErrorReportBuilder : IErrorReportBuilder
{
private readonly ErrorReportSanitizer _sanitizer = new();
private readonly IApplicationInfoService _appInfoService;
private static string Preamble => Properties.Resources.ErrorReport_Global_Preamble;
/// <summary>
/// Initializes a new instance of the <see cref="ErrorReportBuilder"/> class.
/// </summary>
/// <param name="appInfoService">Optional application info service. If not provided, a default instance is created.</param>
public ErrorReportBuilder(IApplicationInfoService? appInfoService = null)
{
_appInfoService = appInfoService ?? new ApplicationInfoService(null);
}
public string BuildReport(Exception exception, string context, bool redactPii = true)
{
ArgumentNullException.ThrowIfNull(exception);
@@ -24,6 +31,9 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
var sanitizedMessage = redactPii ? _sanitizer.Sanitize(exceptionMessage) : exceptionMessage;
var sanitizedFormattedException = redactPii ? _sanitizer.Sanitize(exception.ToString()) : exception.ToString();
var applicationInfoSummary = GetAppInfoSafe();
var applicationInfoSummarySanitized = redactPii ? _sanitizer.Sanitize(applicationInfoSummary) : applicationInfoSummary;
// Note:
// - do not localize technical part of the report, we need to ensure it can be read by developers
// - keep timestamp format should be consistent with the log (makes it easier to search)
@@ -38,18 +48,7 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
HRESULT: 0x{exception.HResult:X8} ({exception.HResult})
Context: {context ?? "N/A"}
Application:
App version: {GetAppVersionSafe()}
Is elevated: {GetElevationStatus()}
Environment:
OS version: {RuntimeInformation.OSDescription}
OS architecture: {RuntimeInformation.OSArchitecture}
Runtime identifier: {RuntimeInformation.RuntimeIdentifier}
Framework: {RuntimeInformation.FrameworkDescription}
Process architecture: {RuntimeInformation.ProcessArchitecture}
Culture: {CultureInfo.CurrentCulture.Name}
UI culture: {CultureInfo.CurrentUICulture.Name}
{applicationInfoSummarySanitized}
Stack Trace:
{exception.StackTrace}
@@ -66,31 +65,17 @@ public sealed class ErrorReportBuilder : IErrorReportBuilder
""";
}
private static string GetElevationStatus()
private string? GetAppInfoSafe()
{
// Note: do not localize technical part of the report, we need to ensure it can be read by developers
try
{
var isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
return isElevated ? "yes" : "no";
return _appInfoService.GetApplicationInfoSummary();
}
catch (Exception)
catch (Exception ex)
{
return "Failed to determine elevation status";
}
}
private static string GetAppVersionSafe()
{
// Note: do not localize technical part of the report, we need to ensure it can be read by developers
try
{
var version = Package.Current.Id.Version;
return $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
}
catch (Exception)
{
return "Failed to retrieve app version";
// Getting application info should never throw, but if it does, we don't want it to prevent the report from being generated
var message = CoalesceExceptionMessage(ex);
return $"Failed to get application info summary: {message}";
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class BloomFilter : IBloomFilter
{
public ulong Compute(string input)
{
ulong bloom = 0;
foreach (var ch in input)
{
if (SymbolClassifier.Classify(ch) == SymbolKind.WordSeparator)
{
continue;
}
var h = (uint)ch * 0x45d9f3b;
bloom |= 1UL << (int)(h & 31);
bloom |= 1UL << (int)(((h >> 16) & 31) + 32);
if (bloom == ulong.MaxValue)
{
break;
}
}
return bloom;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MightContain(ulong candidateBloom, ulong queryBloom)
{
return (candidateBloom & queryBloom) == queryBloom;
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class FuzzyMatcherProvider : IFuzzyMatcherProvider
{
private readonly IBloomFilter _bloomCalculator = new BloomFilter();
private readonly IStringFolder _normalizer = new StringFolder();
private IPrecomputedFuzzyMatcher _current;
public FuzzyMatcherProvider(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
{
_current = CreateMatcher(core, pinyin);
}
public IPrecomputedFuzzyMatcher Current => Volatile.Read(ref _current);
public void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null)
{
Volatile.Write(ref _current, CreateMatcher(core, pinyin));
}
private IPrecomputedFuzzyMatcher CreateMatcher(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin)
{
return pinyin is null || !IsPinyinEnabled(pinyin)
? new PrecomputedFuzzyMatcher(core, _normalizer, _bloomCalculator)
: new PrecomputedFuzzyMatcherWithPinyin(core, pinyin, _normalizer, _bloomCalculator);
}
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o)
{
return o.Mode switch
{
PinyinMode.Off => false,
PinyinMode.On => true,
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
_ => false,
};
}
private static bool IsSimplifiedChineseUi()
{
var culture = CultureInfo.CurrentUICulture;
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,65 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public readonly struct FuzzyQuery
{
public readonly string Original;
public readonly string Folded;
public readonly ulong Bloom;
public readonly int EffectiveLength;
public readonly bool IsAllLowercaseAsciiOrNonLetter;
public readonly string? SecondaryOriginal;
public readonly string? SecondaryFolded;
public readonly ulong SecondaryBloom;
public readonly int SecondaryEffectiveLength;
public readonly bool SecondaryIsAllLowercaseAsciiOrNonLetter;
public int Length => Folded.Length;
public bool HasSecondary => SecondaryFolded is not null;
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
public FuzzyQuery(
string original,
string folded,
ulong bloom,
int effectiveLength,
bool isAllLowercaseAsciiOrNonLetter,
string? secondaryOriginal = null,
string? secondaryFolded = null,
ulong secondaryBloom = 0,
int secondaryEffectiveLength = 0,
bool secondaryIsAllLowercaseAsciiOrNonLetter = true)
{
Original = original;
Folded = folded;
Bloom = bloom;
EffectiveLength = effectiveLength;
IsAllLowercaseAsciiOrNonLetter = isAllLowercaseAsciiOrNonLetter;
SecondaryOriginal = secondaryOriginal;
SecondaryFolded = secondaryFolded;
SecondaryBloom = secondaryBloom;
SecondaryEffectiveLength = secondaryEffectiveLength;
SecondaryIsAllLowercaseAsciiOrNonLetter = secondaryIsAllLowercaseAsciiOrNonLetter;
}
}

View File

@@ -0,0 +1,46 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public readonly struct FuzzyTarget
{
public readonly string Original;
public readonly string Folded;
public readonly ulong Bloom;
public readonly string? SecondaryOriginal;
public readonly string? SecondaryFolded;
public readonly ulong SecondaryBloom;
public int Length => Folded.Length;
public bool HasSecondary => SecondaryFolded is not null;
public int SecondaryLength => SecondaryFolded?.Length ?? 0;
public ReadOnlySpan<char> OriginalSpan => Original.AsSpan();
public ReadOnlySpan<char> FoldedSpan => Folded.AsSpan();
public ReadOnlySpan<char> SecondaryOriginalSpan => SecondaryOriginal.AsSpan();
public ReadOnlySpan<char> SecondaryFoldedSpan => SecondaryFolded.AsSpan();
public FuzzyTarget(
string original,
string folded,
ulong bloom,
string? secondaryOriginal = null,
string? secondaryFolded = null,
ulong secondaryBloom = 0)
{
Original = original;
Folded = folded;
Bloom = bloom;
SecondaryOriginal = secondaryOriginal;
SecondaryFolded = secondaryFolded;
SecondaryBloom = secondaryBloom;
}
}

View File

@@ -0,0 +1,34 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public struct FuzzyTargetCache
{
private string? _lastRaw;
private uint _schemaId;
private FuzzyTarget _target;
public FuzzyTarget GetOrUpdate(IPrecomputedFuzzyMatcher matcher, string? raw)
{
raw ??= string.Empty;
if (_schemaId == matcher.SchemaId && string.Equals(_lastRaw, raw, StringComparison.Ordinal))
{
return _target;
}
_target = matcher.PrecomputeTarget(raw);
_schemaId = matcher.SchemaId;
_lastRaw = raw;
return _target;
}
public void Invalidate()
{
_lastRaw = null;
_target = default;
_schemaId = 0;
}
}

View File

@@ -0,0 +1,12 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public interface IBloomFilter
{
ulong Compute(string input);
bool MightContain(ulong candidateBloom, ulong queryBloom);
}

View File

@@ -0,0 +1,12 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public interface IFuzzyMatcherProvider
{
IPrecomputedFuzzyMatcher Current { get; }
void UpdateSettings(PrecomputedFuzzyMatcherOptions core, PinyinFuzzyMatcherOptions? pinyin = null);
}

View File

@@ -0,0 +1,16 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public interface IPrecomputedFuzzyMatcher
{
uint SchemaId { get; }
FuzzyQuery PrecomputeQuery(string? input);
FuzzyTarget PrecomputeTarget(string? input);
int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target);
}

View File

@@ -0,0 +1,10 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public interface IStringFolder
{
string Fold(string input, bool removeDiacritics);
}

View File

@@ -0,0 +1,13 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public sealed class PinyinFuzzyMatcherOptions
{
public PinyinMode Mode { get; init; } = PinyinMode.AutoSimplifiedChineseUi;
/// <summary>Remove IME syllable separators (') for query secondary variant.</summary>
public bool RemoveApostrophesForQuery { get; init; } = true;
}

View File

@@ -0,0 +1,12 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public enum PinyinMode
{
Off = 0,
AutoSimplifiedChineseUi = 1,
On = 2,
}

View File

@@ -0,0 +1,575 @@
// 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.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class PrecomputedFuzzyMatcher : IPrecomputedFuzzyMatcher
{
private const int NoMatchScore = 0;
private const int StackallocThresholdChars = 512;
private const int FolderSchemaVersion = 1;
private const int BloomSchemaVersion = 1;
private readonly PrecomputedFuzzyMatcherOptions _options;
private readonly IStringFolder _stringFolder;
private readonly IBloomFilter _bloom;
public PrecomputedFuzzyMatcher(
PrecomputedFuzzyMatcherOptions? options = null,
IStringFolder? normalization = null,
IBloomFilter? bloomCalculator = null)
{
_options = options ?? PrecomputedFuzzyMatcherOptions.Default;
_bloom = bloomCalculator ?? new BloomFilter();
_stringFolder = normalization ?? new StringFolder();
SchemaId = ComputeSchemaId(_options);
}
public uint SchemaId { get; }
public FuzzyQuery PrecomputeQuery(string? input) => PrecomputeQuery(input, null);
public FuzzyTarget PrecomputeTarget(string? input) => PrecomputeTarget(input, null);
public int Score(in FuzzyQuery query, in FuzzyTarget target)
{
var qFold = query.FoldedSpan;
var tLen = target.Length;
if (query.EffectiveLength == 0 || tLen == 0)
{
return NoMatchScore;
}
var skipWordSeparators = _options.SkipWordSeparators;
var bestScore = 0;
// 1. Primary → Primary
if (tLen >= query.EffectiveLength && _bloom.MightContain(target.Bloom, query.Bloom))
{
if (CanMatchSubsequence(qFold, target.FoldedSpan, skipWordSeparators))
{
bestScore = ScoreNonContiguous(
qRaw: query.OriginalSpan,
qFold: qFold,
qEffectiveLen: query.EffectiveLength,
tRaw: target.OriginalSpan,
tFold: target.FoldedSpan,
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.IsAllLowercaseAsciiOrNonLetter);
}
}
// 2. Secondary → Secondary
if (query.HasSecondary && target.HasSecondary)
{
var qSecFold = query.SecondaryFoldedSpan;
if (target.SecondaryLength >= query.SecondaryEffectiveLength &&
_bloom.MightContain(target.SecondaryBloom, query.SecondaryBloom) &&
CanMatchSubsequence(qSecFold, target.SecondaryFoldedSpan, skipWordSeparators))
{
var score = ScoreNonContiguous(
qRaw: query.SecondaryOriginalSpan,
qFold: qSecFold,
qEffectiveLen: query.SecondaryEffectiveLength,
tRaw: target.SecondaryOriginalSpan,
tFold: target.SecondaryFoldedSpan,
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.SecondaryIsAllLowercaseAsciiOrNonLetter);
if (score > bestScore)
{
bestScore = score;
}
}
}
// 3. Primary query → Secondary target
if (target.HasSecondary &&
target.SecondaryLength >= query.EffectiveLength &&
_bloom.MightContain(target.SecondaryBloom, query.Bloom))
{
if (CanMatchSubsequence(qFold, target.SecondaryFoldedSpan, skipWordSeparators))
{
var score = ScoreNonContiguous(
qRaw: query.OriginalSpan,
qFold: qFold,
qEffectiveLen: query.EffectiveLength,
tRaw: target.SecondaryOriginalSpan,
tFold: target.SecondaryFoldedSpan,
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.IsAllLowercaseAsciiOrNonLetter);
if (score > bestScore)
{
bestScore = score;
}
}
}
// 4. Secondary query → Primary target
if (query.HasSecondary &&
tLen >= query.SecondaryEffectiveLength &&
_bloom.MightContain(target.Bloom, query.SecondaryBloom))
{
var qSecFold = query.SecondaryFoldedSpan;
if (CanMatchSubsequence(qSecFold, target.FoldedSpan, skipWordSeparators))
{
var score = ScoreNonContiguous(
qRaw: query.SecondaryOriginalSpan,
qFold: qSecFold,
qEffectiveLen: query.SecondaryEffectiveLength,
tRaw: target.OriginalSpan,
tFold: target.FoldedSpan,
ignoreSameCaseBonusForThisQuery: _options.IgnoreSameCaseBonusIfQueryIsAllLowercase && query.SecondaryIsAllLowercaseAsciiOrNonLetter);
if (score > bestScore)
{
bestScore = score;
}
}
}
return bestScore;
}
private FuzzyQuery PrecomputeQuery(string? input, string? secondaryInput)
{
input ??= string.Empty;
var folded = _stringFolder.Fold(input, _options.RemoveDiacritics);
var bloom = _bloom.Compute(folded);
var effectiveLength = _options.SkipWordSeparators
? folded.Length - CountWordSeparators(folded)
: folded.Length;
var isAllLowercase = IsAllLowercaseAsciiOrNonLetter(input);
string? secondaryOriginal = null;
string? secondaryFolded = null;
ulong secondaryBloom = 0;
var secondaryEffectiveLength = 0;
var secondaryIsAllLowercase = true;
if (!string.IsNullOrEmpty(secondaryInput))
{
secondaryOriginal = secondaryInput;
secondaryFolded = _stringFolder.Fold(secondaryInput, _options.RemoveDiacritics);
secondaryBloom = _bloom.Compute(secondaryFolded);
secondaryEffectiveLength = _options.SkipWordSeparators
? secondaryFolded.Length - CountWordSeparators(secondaryFolded)
: secondaryFolded.Length;
secondaryIsAllLowercase = IsAllLowercaseAsciiOrNonLetter(secondaryInput);
}
return new FuzzyQuery(
original: input,
folded: folded,
bloom: bloom,
effectiveLength: effectiveLength,
isAllLowercaseAsciiOrNonLetter: isAllLowercase,
secondaryOriginal: secondaryOriginal,
secondaryFolded: secondaryFolded,
secondaryBloom: secondaryBloom,
secondaryEffectiveLength: secondaryEffectiveLength,
secondaryIsAllLowercaseAsciiOrNonLetter: secondaryIsAllLowercase);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int CountWordSeparators(string s)
{
var count = 0;
foreach (var c in s)
{
if (SymbolClassifier.Classify(c) == SymbolKind.WordSeparator)
{
count++;
}
}
return count;
}
}
internal FuzzyTarget PrecomputeTarget(string? input, string? secondaryInput)
{
input ??= string.Empty;
var folded = _stringFolder.Fold(input, _options.RemoveDiacritics);
var bloom = _bloom.Compute(folded);
string? secondaryFolded = null;
ulong secondaryBloom = 0;
if (!string.IsNullOrEmpty(secondaryInput))
{
secondaryFolded = _stringFolder.Fold(secondaryInput, _options.RemoveDiacritics);
secondaryBloom = _bloom.Compute(secondaryFolded);
}
return new FuzzyTarget(
input,
folded,
bloom,
secondaryInput,
secondaryFolded,
secondaryBloom);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAllLowercaseAsciiOrNonLetter(string s)
{
foreach (var c in s)
{
if ((uint)(c - 'A') <= ('Z' - 'A'))
{
return false;
}
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanMatchSubsequence(
ReadOnlySpan<char> qFold,
ReadOnlySpan<char> tFold,
bool skipWordSeparators)
{
var qi = 0;
var ti = 0;
while (qi < qFold.Length && ti < tFold.Length)
{
var qChar = qFold[qi];
if (skipWordSeparators && SymbolClassifier.Classify(qChar) == SymbolKind.WordSeparator)
{
qi++;
continue;
}
if (qChar == tFold[ti])
{
qi++;
}
ti++;
}
// Skip trailing word separators in query
if (skipWordSeparators)
{
while (qi < qFold.Length && SymbolClassifier.Classify(qFold[qi]) == SymbolKind.WordSeparator)
{
qi++;
}
}
return qi == qFold.Length;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
[SkipLocalsInit]
private int ScoreNonContiguous(
scoped in ReadOnlySpan<char> qRaw,
scoped in ReadOnlySpan<char> qFold,
int qEffectiveLen,
scoped in ReadOnlySpan<char> tRaw,
scoped in ReadOnlySpan<char> tFold,
bool ignoreSameCaseBonusForThisQuery)
{
Debug.Assert(qRaw.Length == qFold.Length, "Original and folded spans are traversed in lockstep: requires qRaw.Length == qFold.Length");
Debug.Assert(tRaw.Length == tFold.Length, "Original and folded spans are traversed in lockstep: requires tRaw.Length == tFold.Length");
Debug.Assert(qEffectiveLen <= qFold.Length, "Effective length must be less than or equal to folded length");
var qLen = qFold.Length;
var tLen = tFold.Length;
// Copy options to local variables to avoid repeated field accesses
var charMatchBonus = _options.CharMatchBonus;
var sameCaseBonus = ignoreSameCaseBonusForThisQuery ? 0 : _options.SameCaseBonus;
var consecutiveMultiplier = _options.ConsecutiveMultiplier;
var camelCaseBonus = _options.CamelCaseBonus;
var startOfWordBonus = _options.StartOfWordBonus;
var pathSeparatorBonus = _options.PathSeparatorBonus;
var wordSeparatorBonus = _options.WordSeparatorBonus;
var separatorAlignmentBonus = _options.SeparatorAlignmentBonus;
var exactSeparatorBonus = _options.ExactSeparatorBonus;
var skipWordSeparators = _options.SkipWordSeparators;
// DP buffer: two rows of length tLen
var bufferSize = tLen * 2;
int[]? rented = null;
try
{
scoped Span<int> buffer;
if (bufferSize <= StackallocThresholdChars)
{
buffer = stackalloc int[bufferSize];
}
else
{
rented = ArrayPool<int>.Shared.Rent(bufferSize);
buffer = rented.AsSpan(0, bufferSize);
}
var scores = buffer[..tLen];
var seqLens = buffer.Slice(tLen, tLen);
scores.Clear();
seqLens.Clear();
ref var scores0 = ref MemoryMarshal.GetReference(scores);
ref var seqLens0 = ref MemoryMarshal.GetReference(seqLens);
ref var qRaw0 = ref MemoryMarshal.GetReference(qRaw);
ref var qFold0 = ref MemoryMarshal.GetReference(qFold);
ref var tRaw0 = ref MemoryMarshal.GetReference(tRaw);
ref var tFold0 = ref MemoryMarshal.GetReference(tFold);
var qiEffective = 0;
for (var qi = 0; qi < qLen; qi++)
{
var qCharFold = Unsafe.Add(ref qFold0, qi);
var qCharKind = SymbolClassifier.Classify(qCharFold);
if (skipWordSeparators && qCharKind == SymbolKind.WordSeparator)
{
continue;
}
// Hoisted values
var qRawIsUpper = char.IsUpper(Unsafe.Add(ref qRaw0, qi));
// row computation
var leftScore = 0;
var diagScore = 0;
var diagSeqLen = 0;
// limit ti to ensure enough remaining characters to match the rest of the query
var tiMax = tLen - qEffectiveLen + qiEffective;
for (var ti = 0; ti <= tiMax; ti++)
{
var upScore = Unsafe.Add(ref scores0, ti);
var upSeqLen = Unsafe.Add(ref seqLens0, ti);
var charScore = 0;
if (diagScore != 0 || qiEffective == 0)
{
charScore = ComputeCharScore(
qi,
ti,
qCharFold,
qCharKind,
diagSeqLen,
qRawIsUpper,
ref tRaw0,
ref qFold0,
ref tFold0);
}
var candidateScore = diagScore + charScore;
if (charScore != 0 && candidateScore >= leftScore)
{
Unsafe.Add(ref scores0, ti) = candidateScore;
Unsafe.Add(ref seqLens0, ti) = diagSeqLen + 1;
leftScore = candidateScore;
}
else
{
Unsafe.Add(ref scores0, ti) = leftScore;
Unsafe.Add(ref seqLens0, ti) = 0;
/* leftScore remains unchanged */
}
diagScore = upScore;
diagSeqLen = upSeqLen;
}
// Early exit: no match possible
if (leftScore == 0)
{
return NoMatchScore;
}
// Advance effective query index
// Only counts non-separator characters if skipWordSeparators is enabled
qiEffective++;
if (qiEffective == qEffectiveLen)
{
return leftScore;
}
}
return scores[tLen - 1];
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
int ComputeCharScore(
int qi,
int ti,
char qCharFold,
SymbolKind qCharKind,
int seqLen,
bool qCharRawCurrIsUpper,
ref char tRaw0,
ref char qFold0,
ref char tFold0)
{
// Match check:
// - exact folded char match always ok
// - otherwise, allow equivalence only for word separators (e.g. '_' matches '-')
var tCharFold = Unsafe.Add(ref tFold0, ti);
if (qCharFold != tCharFold)
{
if (!skipWordSeparators)
{
return 0;
}
if (qCharKind != SymbolKind.WordSeparator ||
SymbolClassifier.Classify(tCharFold) != SymbolKind.WordSeparator)
{
return 0;
}
}
// 0. Base char match bonus
var score = charMatchBonus;
// 1. Consecutive match bonus
if (seqLen > 0)
{
score += seqLen * consecutiveMultiplier;
}
// 2. Same case bonus
// Early outs to appease the branch predictor
if (sameCaseBonus != 0)
{
var tCharRawCurr = Unsafe.Add(ref tRaw0, ti);
var tCharRawCurrIsUpper = char.IsUpper(tCharRawCurr);
if (qCharRawCurrIsUpper == tCharRawCurrIsUpper)
{
score += sameCaseBonus;
}
if (ti == 0)
{
score += startOfWordBonus;
return score;
}
var tPrevFold = Unsafe.Add(ref tFold0, ti - 1);
var tPrevKind = SymbolClassifier.Classify(tPrevFold);
if (tPrevKind != SymbolKind.Other)
{
score += tPrevKind == SymbolKind.PathSeparator
? pathSeparatorBonus
: wordSeparatorBonus;
if (skipWordSeparators && seqLen == 0 && qi > 0)
{
var qPrevFold = Unsafe.Add(ref qFold0, qi - 1);
var qPrevKind = SymbolClassifier.Classify(qPrevFold);
if (qPrevKind == SymbolKind.WordSeparator)
{
score += separatorAlignmentBonus;
if (tPrevKind == SymbolKind.WordSeparator && qPrevFold == tPrevFold)
{
score += exactSeparatorBonus;
}
}
}
return score;
}
if (tCharRawCurrIsUpper && seqLen == 0)
{
score += camelCaseBonus;
return score;
}
return score;
}
else
{
if (ti == 0)
{
score += startOfWordBonus;
return score;
}
var tPrevFold = Unsafe.Add(ref tFold0, ti - 1);
var tPrevKind = SymbolClassifier.Classify(tPrevFold);
if (tPrevKind != SymbolKind.Other)
{
score += tPrevKind == SymbolKind.PathSeparator
? pathSeparatorBonus
: wordSeparatorBonus;
if (skipWordSeparators && seqLen == 0 && qi > 0)
{
var qPrevFold = Unsafe.Add(ref qFold0, qi - 1);
var qPrevKind = SymbolClassifier.Classify(qPrevFold);
if (qPrevKind == SymbolKind.WordSeparator)
{
score += separatorAlignmentBonus;
if (tPrevKind == SymbolKind.WordSeparator && qPrevFold == tPrevFold)
{
score += exactSeparatorBonus;
}
}
}
return score;
}
if (camelCaseBonus != 0 && seqLen == 0 && char.IsUpper(Unsafe.Add(ref tRaw0, ti)))
{
score += camelCaseBonus;
return score;
}
return score;
}
}
}
finally
{
if (rented is not null)
{
ArrayPool<int>.Shared.Return(rented);
}
}
}
// Schema ID is for cache invalidation of precomputed targets.
// Only includes options that affect folding/bloom, not scoring.
private static uint ComputeSchemaId(PrecomputedFuzzyMatcherOptions o)
{
const uint fnvOffset = 2166136261;
const uint fnvPrime = 16777619;
var h = fnvOffset;
h = unchecked((h ^ FolderSchemaVersion) * fnvPrime);
h = unchecked((h ^ BloomSchemaVersion) * fnvPrime);
h = unchecked((h ^ (uint)(o.RemoveDiacritics ? 1 : 0)) * fnvPrime);
return h;
}
}

View File

@@ -0,0 +1,40 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
public sealed class PrecomputedFuzzyMatcherOptions
{
public static PrecomputedFuzzyMatcherOptions Default { get; } = new();
/*
* Bonuses
*/
public int CharMatchBonus { get; init; } = 1;
public int SameCaseBonus { get; init; } = 1;
public int ConsecutiveMultiplier { get; init; } = 5;
public int CamelCaseBonus { get; init; } = 2;
public int StartOfWordBonus { get; init; } = 8;
public int PathSeparatorBonus { get; init; } = 5;
public int WordSeparatorBonus { get; init; } = 4;
public int SeparatorAlignmentBonus { get; init; } = 2;
public int ExactSeparatorBonus { get; init; } = 1;
/*
* Settings
*/
public bool RemoveDiacritics { get; init; } = true;
public bool SkipWordSeparators { get; init; } = true;
public bool IgnoreSameCaseBonusIfQueryIsAllLowercase { get; init; } = true;
}

View File

@@ -0,0 +1,177 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Runtime.CompilerServices;
using ToolGood.Words.Pinyin;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class PrecomputedFuzzyMatcherWithPinyin : IPrecomputedFuzzyMatcher
{
private readonly IBloomFilter _bloom;
private readonly PrecomputedFuzzyMatcher _core;
private readonly IStringFolder _stringFolder;
private readonly PinyinFuzzyMatcherOptions _pinyin;
public PrecomputedFuzzyMatcherWithPinyin(
PrecomputedFuzzyMatcherOptions coreOptions,
PinyinFuzzyMatcherOptions pinyinOptions,
IStringFolder stringFolder,
IBloomFilter bloom)
{
_pinyin = pinyinOptions;
_stringFolder = stringFolder;
_bloom = bloom;
_core = new PrecomputedFuzzyMatcher(coreOptions, stringFolder, bloom);
SchemaId = CombineSchema(_core.SchemaId, _pinyin);
}
public uint SchemaId { get; }
public FuzzyQuery PrecomputeQuery(string? input)
{
input ??= string.Empty;
var primary = _core.PrecomputeQuery(input);
// Fast exit if effectively off (provider should already filter, but keep robust)
if (!IsPinyinEnabled(_pinyin))
{
return primary;
}
// Match legacy: remove apostrophes for query secondary
var queryForPinyin = _pinyin.RemoveApostrophesForQuery ? RemoveApostrophesIfAny(input) : input;
var pinyin = WordsHelper.GetPinyin(queryForPinyin);
if (string.IsNullOrEmpty(pinyin))
{
return primary;
}
var secondary = _core.PrecomputeQuery(pinyin);
return new FuzzyQuery(
primary.Original,
primary.Folded,
primary.Bloom,
primary.EffectiveLength,
primary.IsAllLowercaseAsciiOrNonLetter,
secondary.Original,
secondary.Folded,
secondary.Bloom,
secondary.EffectiveLength,
secondary.SecondaryIsAllLowercaseAsciiOrNonLetter);
}
public FuzzyTarget PrecomputeTarget(string? input)
{
input ??= string.Empty;
var primary = _core.PrecomputeTarget(input);
if (!IsPinyinEnabled(_pinyin))
{
return primary;
}
// Match legacy: only compute target pinyin when target contains Chinese
if (!ContainsToolGoodChinese(input))
{
return primary;
}
var pinyin = WordsHelper.GetPinyin(input);
if (string.IsNullOrEmpty(pinyin))
{
return primary;
}
var secondary = _core.PrecomputeTarget(pinyin);
return new FuzzyTarget(
primary.Original,
primary.Folded,
primary.Bloom,
secondary.Original,
secondary.Folded,
secondary.Bloom);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int Score(scoped in FuzzyQuery query, scoped in FuzzyTarget target)
=> _core.Score(in query, in target);
private static bool IsPinyinEnabled(PinyinFuzzyMatcherOptions o) => o.Mode switch
{
PinyinMode.Off => false,
PinyinMode.On => true,
PinyinMode.AutoSimplifiedChineseUi => IsSimplifiedChineseUi(),
_ => false,
};
private static bool IsSimplifiedChineseUi()
{
var culture = CultureInfo.CurrentUICulture;
return culture.Name.StartsWith("zh-CN", StringComparison.OrdinalIgnoreCase)
|| culture.Name.StartsWith("zh-Hans", StringComparison.OrdinalIgnoreCase);
}
private static bool ContainsToolGoodChinese(string s)
{
return WordsHelper.HasChinese(s);
}
private static string RemoveApostrophesIfAny(string input)
{
var first = input.IndexOf('\'');
if (first < 0)
{
return input;
}
var removeCount = 1;
for (var i = first + 1; i < input.Length; i++)
{
if (input[i] == '\'')
{
removeCount++;
}
}
return string.Create(input.Length - removeCount, input, static (dst, src) =>
{
var di = 0;
for (var i = 0; i < src.Length; i++)
{
var c = src[i];
if (c == '\'')
{
continue;
}
dst[di++] = c;
}
});
}
private static uint CombineSchema(uint coreSchemaId, PinyinFuzzyMatcherOptions p)
{
const uint fnvOffset = 2166136261;
const uint fnvPrime = 16777619;
var h = fnvOffset;
h = unchecked((h ^ coreSchemaId) * fnvPrime);
h = unchecked((h ^ (uint)p.Mode) * fnvPrime);
h = unchecked((h ^ (p.RemoveApostrophesForQuery ? 1u : 0u)) * fnvPrime);
// bump if you change formatting/conversion behavior
const uint pinyinAlgoVersion = 1;
h = unchecked((h ^ pinyinAlgoVersion) * fnvPrime);
return h;
}
}

View File

@@ -0,0 +1,163 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
namespace Microsoft.CmdPal.Core.Common.Text;
public sealed class StringFolder : IStringFolder
{
// Cache for diacritic-stripped uppercase characters.
// Benign race: worst case is redundant computation writing the same value.
// 0 = uncached, else cachedChar + 1
private static readonly ushort[] StripCacheUpper = new ushort[char.MaxValue + 1];
public string Fold(string input, bool removeDiacritics)
{
if (string.IsNullOrEmpty(input))
{
return string.Empty;
}
if (!removeDiacritics || Ascii.IsValid(input))
{
if (IsAlreadyFoldedAndSlashNormalized(input))
{
return input;
}
return string.Create(input.Length, input, static (dst, src) =>
{
for (var i = 0; i < src.Length; i++)
{
var c = src[i];
dst[i] = c == '\\' ? '/' : char.ToUpperInvariant(c);
}
});
}
return string.Create(input.Length, input, static (dst, src) =>
{
for (var i = 0; i < src.Length; i++)
{
var c = src[i];
var upper = c == '\\' ? '/' : char.ToUpperInvariant(c);
dst[i] = StripDiacriticsFromUpper(upper);
}
});
}
private static bool IsAlreadyFoldedAndSlashNormalized(string input)
{
var sawNonAscii = false;
for (var i = 0; i < input.Length; i++)
{
var c = input[i];
if (c == '\\')
{
return false;
}
if ((uint)(c - 'a') <= 'z' - 'a')
{
return false;
}
if (c > 0x7F)
{
sawNonAscii = true;
}
}
if (sawNonAscii)
{
for (var i = 0; i < input.Length; i++)
{
var c = input[i];
if (c <= 0x7F)
{
continue;
}
var cat = CharUnicodeInfo.GetUnicodeCategory(c);
if (cat is UnicodeCategory.LowercaseLetter or UnicodeCategory.TitlecaseLetter)
{
return false;
}
}
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static char StripDiacriticsFromUpper(char upper)
{
if (upper <= 0x7F)
{
return upper;
}
// Never attempt normalization on lone UTF-16 surrogates.
if (char.IsSurrogate(upper))
{
return upper;
}
var cachedPlus1 = StripCacheUpper[upper];
if (cachedPlus1 != 0)
{
return (char)(cachedPlus1 - 1);
}
var mapped = StripDiacriticsSlow(upper);
StripCacheUpper[upper] = (ushort)(mapped + 1);
return mapped;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static char StripDiacriticsSlow(char upper)
{
try
{
var baseChar = FirstNonMark(upper, NormalizationForm.FormD);
if (baseChar == '\0' || baseChar == upper)
{
var kd = FirstNonMark(upper, NormalizationForm.FormKD);
if (kd != '\0')
{
baseChar = kd;
}
}
return char.ToUpperInvariant(baseChar == '\0' ? upper : baseChar);
}
catch
{
// Absolute safety: if globalization tables ever throw for some reason,
// degrade gracefully rather than failing hard.
return upper;
}
static char FirstNonMark(char c, NormalizationForm form)
{
var normalized = c.ToString().Normalize(form);
foreach (var ch in normalized)
{
var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
if (cat is not (UnicodeCategory.NonSpacingMark or UnicodeCategory.SpacingCombiningMark or UnicodeCategory.EnclosingMark))
{
return ch;
}
}
return '\0';
}
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
namespace Microsoft.CmdPal.Core.Common.Text;
internal static class SymbolClassifier
{
// Embedded in .data section - no allocation, no static constructor
private static ReadOnlySpan<byte> Lookup =>
[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-15
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
2, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 2, 2, 1, // 32-47: space=2, "=2, '=2, -=2, .=2, /=1
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, // 48-63: :=2
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 64-79
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, // 80-95: _=2
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 96-111
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 112-127
];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SymbolKind Classify(char c)
{
return c > 0x7F ? SymbolKind.Other : (SymbolKind)Lookup[c];
}
}

View File

@@ -0,0 +1,12 @@
// 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 Microsoft.CmdPal.Core.Common.Text;
internal enum SymbolKind : byte
{
Other = 0,
PathSeparator = 1,
WordSeparator = 2,
}

View File

@@ -3,45 +3,44 @@
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.ViewModels.Messages;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class AliasManager : ObservableObject
{
private readonly TopLevelCommandManager _topLevelCommandManager;
// private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly SettingsService _settingsService;
// REMEMBER, CommandAlias.SearchPrefix is what we use as keys
private readonly Dictionary<string, CommandAlias> _aliases;
private Dictionary<string, CommandAlias> Aliases => _settingsService.CurrentSettings.Aliases;
public AliasManager(TopLevelCommandManager tlcManager, SettingsModel settings)
// TopLevelCommandManager tlcManager,
public AliasManager(SettingsService settingsService)
{
_topLevelCommandManager = tlcManager;
_aliases = settings.Aliases;
// _topLevelCommandManager = tlcManager;
_settingsService = settingsService;
if (_aliases.Count == 0)
if (Aliases.Count == 0)
{
PopulateDefaultAliases();
}
}
private void AddAlias(CommandAlias a) => _aliases.Add(a.SearchPrefix, a);
private void AddAlias(CommandAlias a) => Aliases.Add(a.SearchPrefix, a);
public bool CheckAlias(string searchText)
{
if (_aliases.TryGetValue(searchText, out var alias))
if (Aliases.TryGetValue(searchText, out var alias))
{
try
{
var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
if (topLevelCommand is not null)
{
WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(topLevelCommand.GetPerformCommandMessage());
return true;
}
// var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId);
// if (topLevelCommand is not null)
// {
// WeakReferenceMessenger.Default.Send<ClearSearchMessage>();
// WeakReferenceMessenger.Default.Send<PerformCommandMessage>(topLevelCommand.GetPerformCommandMessage());
// return true;
// }
}
catch
{
@@ -65,7 +64,7 @@ public partial class AliasManager : ObservableObject
public string? KeysFromId(string commandId)
{
return _aliases
return Aliases
.Where(kv => kv.Value.CommandId == commandId)
.Select(kv => kv.Value.Alias)
.FirstOrDefault();
@@ -73,7 +72,7 @@ public partial class AliasManager : ObservableObject
public CommandAlias? AliasFromId(string commandId)
{
return _aliases
return Aliases
.Where(kv => kv.Value.CommandId == commandId)
.Select(kv => kv.Value)
.FirstOrDefault();
@@ -89,7 +88,7 @@ public partial class AliasManager : ObservableObject
// If we already have _this exact alias_, do nothing
if (newAlias is not null &&
_aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
Aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias))
{
if (existingAlias.CommandId == commandId)
{
@@ -98,7 +97,7 @@ public partial class AliasManager : ObservableObject
}
List<CommandAlias> toRemove = [];
foreach (var kv in _aliases)
foreach (var kv in Aliases)
{
// Look for the old aliases for the command, and remove it
if (kv.Value.CommandId == commandId)
@@ -112,18 +111,18 @@ public partial class AliasManager : ObservableObject
toRemove.Add(kv.Value);
// Remove alias from other TopLevelViewModels it may be assigned to
var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
if (topLevelCommand is not null)
{
topLevelCommand.AliasText = string.Empty;
}
// var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
// if (topLevelCommand is not null)
// {
// topLevelCommand.AliasText = string.Empty;
// }
}
}
foreach (var alias in toRemove)
{
// REMEMBER, SearchPrefix is what we use as keys
_aliases.Remove(alias.SearchPrefix);
Aliases.Remove(alias.SearchPrefix);
}
if (newAlias is not null)

View File

@@ -1,18 +1,20 @@
// Copyright (c) Microsoft Corporation
// 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.ObjectModel;
using System.Diagnostics;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.Logging;
using Windows.Foundation;
namespace Microsoft.CmdPal.Core.ViewModels;
namespace Microsoft.CmdPal.UI.ViewModels;
public abstract partial class AppExtensionHost : IExtensionHost
{
private readonly ILogger _logger;
private static readonly GlobalLogPageContext _globalLogPageContext = new();
private static ulong _hostingHwnd;
@@ -27,6 +29,11 @@ public abstract partial class AppExtensionHost : IExtensionHost
public static void SetHostHwnd(ulong hostHwnd) => _hostingHwnd = hostHwnd;
public AppExtensionHost(ILogger logger)
{
_logger = logger;
}
public void DebugLog(string message)
{
#if DEBUG
@@ -60,7 +67,7 @@ public abstract partial class AppExtensionHost : IExtensionHost
return Task.CompletedTask.AsAsyncAction();
}
CoreLogger.LogDebug(message.Message);
Log_Message(message.Message);
_ = Task.Run(() =>
{
@@ -158,6 +165,9 @@ public abstract partial class AppExtensionHost : IExtensionHost
}
public abstract string? GetExtensionDisplayName();
[LoggerMessage(Level = LogLevel.Debug, Message = "{message}")]
partial void Log_Message(string message);
}
public interface IAppHostService

View File

@@ -2,25 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class AppStateModel : ObservableObject
{
[JsonIgnore]
public static readonly string FilePath;
public event TypedEventHandler<AppStateModel, object?>? StateChanged;
///////////////////////////////////////////////////////////////////////////
// STATE HERE
// Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)!
@@ -28,144 +15,4 @@ public partial class AppStateModel : ObservableObject
public RecentCommandsManager RecentCommands { get; set; } = new();
public List<string> RunHistory { get; set; } = [];
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
static AppStateModel()
{
FilePath = StateJsonPath();
}
public static AppStateModel LoadState()
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(LoadState)}");
}
if (!File.Exists(FilePath))
{
Debug.WriteLine("The provided settings file does not exist");
return new();
}
try
{
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
return loaded ?? new();
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
return new();
}
public static void SaveState(AppStateModel model)
{
if (string.IsNullOrEmpty(FilePath))
{
throw new InvalidOperationException($"You must set a valid {nameof(FilePath)} before calling {nameof(SaveState)}");
}
try
{
// Serialize the main dictionary to JSON and save it to the file
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel!);
// validate JSON
if (JsonNode.Parse(settingsJson) is not JsonObject newSettings)
{
Logger.LogError("Failed to parse app state as a JsonObject.");
return;
}
// read previous settings
if (!TryReadSavedState(out var savedSettings))
{
savedSettings = new JsonObject();
}
// merge new settings into old ones
foreach (var item in newSettings)
{
savedSettings[item.Key] = item.Value?.DeepClone();
}
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel!.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
// have a file change watcher on the settings file, and
// reload the settings then
model.StateChanged?.Invoke(model, null);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save application state to {FilePath}:", ex);
}
}
private static bool TryReadSavedState([NotNullWhen(true)] out JsonObject? savedSettings)
{
savedSettings = null;
// read existing content from the file
string oldContent;
try
{
if (File.Exists(FilePath))
{
oldContent = File.ReadAllText(FilePath);
}
else
{
// file doesn't exist (might not have been created yet), so consider this a success
// and return empty settings
savedSettings = new JsonObject();
return true;
}
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to read app state file {FilePath}:\n{ex}");
return false;
}
// detect empty file, just for sake of logging
if (string.IsNullOrWhiteSpace(oldContent))
{
Logger.LogInfo($"App state file is empty: {FilePath}");
return false;
}
// is it valid JSON?
try
{
savedSettings = JsonNode.Parse(oldContent) as JsonObject;
return savedSettings != null;
}
catch (Exception ex)
{
Logger.LogWarning($"Failed to parse app state from {FilePath}:\n{ex}");
return false;
}
}
internal static string StateJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the settings is just next to the exe
return Path.Combine(directory, "state.json");
}
}

View File

@@ -0,0 +1,50 @@
// 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.CmdPal.Common;
using Microsoft.Extensions.Logging;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class AppStateService
{
private readonly ILogger _logger;
private readonly string _filePath;
private AppStateModel _appStateModel;
public event TypedEventHandler<AppStateModel, object?>? StateChanged;
public AppStateModel CurrentSettings => _appStateModel;
public AppStateService(ILogger logger)
{
_logger = logger;
_filePath = PersistenceService.SettingsJsonPath("state.json");
_appStateModel = LoadState();
}
private AppStateModel LoadState()
{
return PersistenceService.LoadObject<AppStateModel>(_filePath, JsonSerializationContext.Default.AppStateModel!, _logger);
}
public void SaveSettings(AppStateModel model)
{
PersistenceService.SaveObject(
model,
_filePath,
JsonSerializationContext.Default.AppStateModel,
JsonSerializationContext.Default.Options,
null,
afterWriteCallback: m => FinalizeStateSave(m),
_logger);
}
private void FinalizeStateSave(AppStateModel model)
{
_appStateModel = model;
StateChanged?.Invoke(model, null);
}
}

View File

@@ -18,6 +18,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
{
private static readonly Color DefaultTintColor = Color.FromArgb(255, 0, 120, 212);
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
// row 0
@@ -85,7 +87,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
Color.FromArgb(255, 126, 115, 95), // #7e735f
];
private readonly SettingsModel _settings;
private readonly SettingsService _settingsService;
private readonly UISettings _uiSettings;
private readonly IThemeService _themeService;
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
@@ -94,22 +96,24 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
private ElementTheme? _elementThemeOverride;
private Color _currentSystemAccentColor;
private SettingsModel Settings => _settingsService.CurrentSettings;
public ObservableCollection<Color> Swatches => WindowsColorSwatches;
public int ThemeIndex
{
get => (int)_settings.Theme;
get => (int)Settings.Theme;
set => Theme = (UserTheme)value;
}
public UserTheme Theme
{
get => _settings.Theme;
get => Settings.Theme;
set
{
if (_settings.Theme != value)
if (Settings.Theme != value)
{
_settings.Theme = value;
Settings.Theme = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ThemeIndex));
Save();
@@ -119,19 +123,22 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public ColorizationMode ColorizationMode
{
get => _settings.ColorizationMode;
get => Settings.ColorizationMode;
set
{
if (_settings.ColorizationMode != value)
if (Settings.ColorizationMode != value)
{
_settings.ColorizationMode = value;
Settings.ColorizationMode = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ColorizationModeIndex));
OnPropertyChanged(nameof(IsCustomTintVisible));
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
OnPropertyChanged(nameof(IsColorIntensityVisible));
OnPropertyChanged(nameof(IsImageTintIntensityVisible));
OnPropertyChanged(nameof(EffectiveTintIntensity));
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
OnPropertyChanged(nameof(IsNoBackgroundVisible));
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
OnPropertyChanged(nameof(IsResetButtonVisible));
if (value == ColorizationMode.WindowsAccentColor)
{
@@ -147,18 +154,18 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int ColorizationModeIndex
{
get => (int)_settings.ColorizationMode;
get => (int)Settings.ColorizationMode;
set => ColorizationMode = (ColorizationMode)value;
}
public Color ThemeColor
{
get => _settings.CustomThemeColor;
get => Settings.CustomThemeColor;
set
{
if (_settings.CustomThemeColor != value)
if (Settings.CustomThemeColor != value)
{
_settings.CustomThemeColor = value;
Settings.CustomThemeColor = value;
OnPropertyChanged();
@@ -174,23 +181,36 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int ColorIntensity
{
get => _settings.CustomThemeColorIntensity;
get => Settings.CustomThemeColorIntensity;
set
{
_settings.CustomThemeColorIntensity = value;
Settings.CustomThemeColorIntensity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveTintIntensity));
Save();
}
}
public int BackgroundImageTintIntensity
{
get => _settings.BackgroundImageTintIntensity;
set
{
_settings.BackgroundImageTintIntensity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveTintIntensity));
Save();
}
}
public string BackgroundImagePath
{
get => _settings.BackgroundImagePath ?? string.Empty;
get => Settings.BackgroundImagePath ?? string.Empty;
set
{
if (_settings.BackgroundImagePath != value)
if (Settings.BackgroundImagePath != value)
{
_settings.BackgroundImagePath = value;
Settings.BackgroundImagePath = value;
OnPropertyChanged();
if (BackgroundImageOpacity == 0)
@@ -205,12 +225,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageOpacity
{
get => _settings.BackgroundImageOpacity;
get => Settings.BackgroundImageOpacity;
set
{
if (_settings.BackgroundImageOpacity != value)
if (Settings.BackgroundImageOpacity != value)
{
_settings.BackgroundImageOpacity = value;
Settings.BackgroundImageOpacity = value;
OnPropertyChanged();
Save();
}
@@ -219,12 +239,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageBrightness
{
get => _settings.BackgroundImageBrightness;
get => Settings.BackgroundImageBrightness;
set
{
if (_settings.BackgroundImageBrightness != value)
if (Settings.BackgroundImageBrightness != value)
{
_settings.BackgroundImageBrightness = value;
Settings.BackgroundImageBrightness = value;
OnPropertyChanged();
Save();
}
@@ -233,12 +253,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public int BackgroundImageBlurAmount
{
get => _settings.BackgroundImageBlurAmount;
get => Settings.BackgroundImageBlurAmount;
set
{
if (_settings.BackgroundImageBlurAmount != value)
if (Settings.BackgroundImageBlurAmount != value)
{
_settings.BackgroundImageBlurAmount = value;
Settings.BackgroundImageBlurAmount = value;
OnPropertyChanged();
Save();
}
@@ -247,12 +267,12 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public BackgroundImageFit BackgroundImageFit
{
get => _settings.BackgroundImageFit;
get => Settings.BackgroundImageFit;
set
{
if (_settings.BackgroundImageFit != value)
if (Settings.BackgroundImageFit != value)
{
_settings.BackgroundImageFit = value;
Settings.BackgroundImageFit = value;
OnPropertyChanged();
OnPropertyChanged(nameof(BackgroundImageFitIndex));
Save();
@@ -279,29 +299,131 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
};
}
public int BackdropOpacity
{
get => _settings.BackdropOpacity;
set
{
if (_settings.BackdropOpacity != value)
{
_settings.BackdropOpacity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(EffectiveBackdropStyle));
OnPropertyChanged(nameof(EffectiveImageOpacity));
Save();
}
}
}
public int BackdropStyleIndex
{
get => (int)_settings.BackdropStyle;
set
{
var newStyle = (BackdropStyle)value;
if (_settings.BackdropStyle != newStyle)
{
_settings.BackdropStyle = newStyle;
OnPropertyChanged();
OnPropertyChanged(nameof(IsBackdropOpacityVisible));
OnPropertyChanged(nameof(IsMicaBackdropDescriptionVisible));
OnPropertyChanged(nameof(IsBackgroundSettingsEnabled));
OnPropertyChanged(nameof(IsBackgroundNotAvailableVisible));
if (!IsBackgroundSettingsEnabled)
{
IsColorizationDetailsExpanded = false;
}
Save();
}
}
}
/// <summary>
/// Gets whether the backdrop opacity slider should be visible.
/// </summary>
public bool IsBackdropOpacityVisible =>
BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether the backdrop description (for styles without options) should be visible.
/// </summary>
public bool IsMicaBackdropDescriptionVisible =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsOpacity;
/// <summary>
/// Gets whether background/colorization settings are available.
/// </summary>
public bool IsBackgroundSettingsEnabled =>
BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
/// <summary>
/// Gets whether the "not available" message should be shown (inverse of IsBackgroundSettingsEnabled).
/// </summary>
public bool IsBackgroundNotAvailableVisible =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization;
public BackdropStyle? EffectiveBackdropStyle
{
get
{
// Return style when transparency/blur is visible (not fully opaque Acrylic)
// - Clear/Mica/MicaAlt/AcrylicThin always show their effect
// - Acrylic shows effect only when opacity < 100
if (_settings.BackdropStyle != BackdropStyle.Acrylic || _settings.BackdropOpacity < 100)
{
return _settings.BackdropStyle;
}
return null;
}
}
public double EffectiveImageOpacity =>
EffectiveBackdropStyle is not null
? (BackgroundImageOpacity / 100f) * Math.Sqrt(_settings.BackdropOpacity / 100.0)
: (BackgroundImageOpacity / 100f);
[ObservableProperty]
public partial bool IsColorizationDetailsExpanded { get; set; }
public bool IsCustomTintVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintVisible => Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
public bool IsCustomTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
public bool IsColorIntensityVisible => _settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor;
public bool IsBackgroundControlsVisible => _settings.ColorizationMode is ColorizationMode.Image;
public bool IsImageTintIntensityVisible => _settings.ColorizationMode is ColorizationMode.Image;
public bool IsNoBackgroundVisible => _settings.ColorizationMode is ColorizationMode.None;
/// <summary>
/// Gets the effective tint intensity for the preview, based on the current colorization mode.
/// </summary>
public int EffectiveTintIntensity => _settings.ColorizationMode is ColorizationMode.Image
? _settings.BackgroundImageTintIntensity
: _settings.CustomThemeColorIntensity;
public bool IsCustomTintIntensityVisible => Settings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
public bool IsAccentColorControlsVisible => _settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public bool IsBackgroundControlsVisible => Settings.ColorizationMode is ColorizationMode.Image;
public AcrylicBackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
public bool IsNoBackgroundVisible => Settings.ColorizationMode is ColorizationMode.None;
public bool IsAccentColorControlsVisible => Settings.ColorizationMode is ColorizationMode.WindowsAccentColor;
public bool IsResetButtonVisible => _settings.ColorizationMode is ColorizationMode.Image;
public BackdropParameters EffectiveBackdrop { get; private set; } = new(Colors.Black, Colors.Black, 0.5f, 0.5f);
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
public Color EffectiveThemeColor => ColorizationMode switch
{
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
_ => Colors.Transparent,
};
public Color EffectiveThemeColor =>
!BackdropStyles.Get(_settings.BackdropStyle).SupportsColorization
? Colors.Transparent
: ColorizationMode switch
{
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
_ => Colors.Transparent,
};
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
@@ -309,17 +431,19 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
public ImageSource? EffectiveBackgroundImageSource =>
ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
!BackdropStyles.Get(_settings.BackdropStyle).SupportsBackgroundImage
? null
: ColorizationMode is ColorizationMode.Image
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
: null;
public AppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
public AppearanceSettingsViewModel(IThemeService themeService, SettingsService settingsService)
{
_themeService = themeService;
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
_settings = settings;
_settingsService = settingsService;
_uiSettings = new UISettings();
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
@@ -327,7 +451,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
Reapply();
IsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None;
SettingsIsColorizationDetailsExpanded = _settings.ColorizationMode != ColorizationMode.None && IsBackgroundSettingsEnabled;
}
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
@@ -348,7 +472,7 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
private void Save()
{
SettingsModel.SaveSettings(_settings);
_settingsService.SaveSettings(Settings);
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
}
@@ -357,6 +481,8 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
// Theme services recalculates effective color and opacity based on current settings.
EffectiveBackdrop = _themeService.Current.BackdropParameters;
OnPropertyChanged(nameof(EffectiveBackdrop));
OnPropertyChanged(nameof(EffectiveBackdropStyle));
OnPropertyChanged(nameof(EffectiveImageOpacity));
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
OnPropertyChanged(nameof(EffectiveThemeColor));
@@ -379,7 +505,28 @@ public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDis
BackgroundImageBlurAmount = 0;
BackgroundImageFit = BackgroundImageFit.UniformToFill;
BackgroundImageOpacity = 100;
ColorIntensity = 0;
BackgroundImageTintIntensity = 0;
}
[RelayCommand]
private void ResetAppearanceSettings()
{
// Reset theme
Theme = UserTheme.Default;
// Reset backdrop settings
BackdropStyleIndex = (int)BackdropStyle.Acrylic;
BackdropOpacity = 100;
// Reset background image settings
BackgroundImagePath = string.Empty;
ResetBackgroundImageProperties();
// Reset colorization
ColorizationMode = ColorizationMode.None;
ThemeColor = DefaultTintColor;
ColorIntensity = 100;
BackgroundImageTintIntensity = 0;
}
public void Dispose()

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.Extensions.Logging;
namespace Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Encapsulates a navigation request within Command Palette view models.
/// </summary>
/// <param name="TargetViewModel">A view model that should be navigated to.</param>
/// <param name="NavigationToken"> A <see cref="CancellationToken"/> that can be used to cancel the pending navigation.</param>
public record AsyncNavigationRequest(object? TargetViewModel, CancellationToken NavigationToken);
#pragma warning disable SA1402 // File may only contain a single type
public record AsyncListPageNavigationRequest(object? TargetViewModel, SettingsService SettingsService, ILogger Logger, CancellationToken NavigationToken)
: AsyncNavigationRequest(TargetViewModel, NavigationToken);
public record AsyncContentPageNavigationRequest(object? TargetViewModel, object? ImageProvider, CancellationToken NavigationToken)
: AsyncNavigationRequest(TargetViewModel, NavigationToken);
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,41 @@
// 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 Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Specifies the type of system backdrop controller to use.
/// </summary>
public enum BackdropControllerKind
{
/// <summary>
/// Solid color with alpha transparency (TransparentTintBackdrop).
/// </summary>
Solid,
/// <summary>
/// Desktop Acrylic with default blur (DesktopAcrylicKind.Default).
/// </summary>
Acrylic,
/// <summary>
/// Desktop Acrylic with thinner blur (DesktopAcrylicKind.Thin).
/// </summary>
AcrylicThin,
/// <summary>
/// Mica effect (MicaKind.Base).
/// </summary>
Mica,
/// <summary>
/// Mica alternate/darker variant (MicaKind.BaseAlt).
/// </summary>
MicaAlt,
/// <summary>
/// Custom backdrop implementation.
/// </summary>
Custom,
}

View File

@@ -0,0 +1,36 @@
// 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 Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Specifies the visual backdrop style for the window.
/// </summary>
public enum BackdropStyle
{
/// <summary>
/// Standard desktop acrylic with blur effect.
/// </summary>
Acrylic,
/// <summary>
/// Solid color with alpha transparency (no blur).
/// </summary>
Clear,
/// <summary>
/// Mica effect that samples the desktop wallpaper.
/// </summary>
Mica,
/// <summary>
/// Thinner acrylic variant with more transparency.
/// </summary>
AcrylicThin,
/// <summary>
/// Mica alternate variant (darker).
/// </summary>
MicaAlt,
}

View File

@@ -0,0 +1,77 @@
// 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 Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Configuration parameters for a backdrop style.
/// </summary>
public sealed record BackdropStyleConfig
{
/// <summary>
/// Gets the type of system backdrop controller to use.
/// </summary>
public required BackdropControllerKind ControllerKind { get; init; }
/// <summary>
/// Gets the base tint opacity before user adjustments.
/// </summary>
public required float BaseTintOpacity { get; init; }
/// <summary>
/// Gets the base luminosity opacity before user adjustments.
/// </summary>
public required float BaseLuminosityOpacity { get; init; }
/// <summary>
/// Gets the brush type to use for preview approximation.
/// </summary>
public required PreviewBrushKind PreviewBrush { get; init; }
/// <summary>
/// Gets the fixed opacity for styles that don't support user adjustment (e.g., Mica).
/// When <see cref="SupportsOpacity"/> is false, this value is used as the effective opacity.
/// </summary>
public float FixedOpacity { get; init; }
/// <summary>
/// Gets 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.
/// </summary>
public bool SupportsBackgroundImage { get; init; } = true;
/// <summary>
/// Gets whether this backdrop style supports opacity adjustment.
/// </summary>
public bool SupportsOpacity { get; init; } = true;
/// <summary>
/// Computes the effective tint opacity based on this style's configuration.
/// </summary>
/// <param name="userOpacity">User's backdrop opacity setting (0-1 normalized).</param>
/// <param name="baseTintOpacityOverride">Optional override for base tint opacity (used by colorful theme).</param>
/// <returns>The effective opacity to apply.</returns>
public float ComputeEffectiveOpacity(float userOpacity, float? baseTintOpacityOverride = null)
{
// For styles that don't support opacity (Mica), use FixedOpacity
if (!SupportsOpacity && FixedOpacity > 0)
{
return FixedOpacity;
}
// For Solid: only user opacity matters (controls alpha of solid color)
if (ControllerKind == BackdropControllerKind.Solid)
{
return userOpacity;
}
// For blur effects: multiply base opacity with user opacity
var baseTint = baseTintOpacityOverride ?? BaseTintOpacity;
return baseTint * userOpacity;
}
}

View File

@@ -0,0 +1,65 @@
// 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 Microsoft.CmdPal.UI.ViewModels;
/// <summary>
/// Central registry of backdrop style configurations.
/// </summary>
public static class BackdropStyles
{
private static readonly Dictionary<BackdropStyle, BackdropStyleConfig> Configs = new()
{
[BackdropStyle.Acrylic] = new()
{
ControllerKind = BackdropControllerKind.Acrylic,
BaseTintOpacity = 0.5f,
BaseLuminosityOpacity = 0.9f,
PreviewBrush = PreviewBrushKind.Acrylic,
},
[BackdropStyle.AcrylicThin] = new()
{
ControllerKind = BackdropControllerKind.AcrylicThin,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 0.85f,
PreviewBrush = PreviewBrushKind.Acrylic,
},
[BackdropStyle.Mica] = new()
{
ControllerKind = BackdropControllerKind.Mica,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
FixedOpacity = 0.96f,
SupportsOpacity = false,
},
[BackdropStyle.MicaAlt] = new()
{
ControllerKind = BackdropControllerKind.MicaAlt,
BaseTintOpacity = 0.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
FixedOpacity = 0.98f,
SupportsOpacity = false,
},
[BackdropStyle.Clear] = new()
{
ControllerKind = BackdropControllerKind.Solid,
BaseTintOpacity = 1.0f,
BaseLuminosityOpacity = 1.0f,
PreviewBrush = PreviewBrushKind.Solid,
},
};
/// <summary>
/// Gets the configuration for the specified backdrop style.
/// </summary>
public static BackdropStyleConfig Get(BackdropStyle style) =>
Configs.TryGetValue(style, out var config) ? config : Configs[BackdropStyle.Acrylic];
/// <summary>
/// Gets all registered backdrop styles.
/// </summary>
public static IEnumerable<BackdropStyle> All => Configs.Keys;
}

View File

@@ -4,15 +4,18 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.Logging;
using Windows.System;
namespace Microsoft.CmdPal.Core.ViewModels;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>
{
private readonly ILogger _logger;
public ICommandBarContext? SelectedItem
{
get => field;
@@ -48,8 +51,9 @@ public partial class CommandBarViewModel : ObservableObject,
[ObservableProperty]
public partial PageViewModel? CurrentPage { get; set; }
public CommandBarViewModel()
public CommandBarViewModel(ILogger logger)
{
_logger = logger;
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
}

View File

@@ -3,11 +3,11 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels;
namespace Microsoft.CmdPal.UI.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel

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