Compare commits

...

13 Commits

Author SHA1 Message Date
Clint Rutkas
8038df1b62 Update PowerToys download links to version 0.96.1 2025-11-28 23:29:59 -08:00
Jiří Polášek
0de60445ea CmdPal: Use Shell API to determine the default browser in WebSearch (#43339)
## Summary of the Pull Request

This PR introduces a new method for determining the default browser
using the Windows Shell API. The new provider selects the browser
associated with the HTTPS protocol (falling back to HTTP if necessary).
The original implementation is retained as a fallback for now, and the
codebase is prepared for future extensions (e.g., manual default-browser
selection).

As a flyby, it also fixes an issue where commands continued showing the
previous browser name if the user changed their default browser while
the Command Palette was running.

## One-liner for change log

Fixed default browser selection in the Web Search built-in extension.

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

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-11-27 09:31:10 -06:00
Jiří Polášek
47d4a65223 CmdPal: Add option to return to home automatically after a delay (#43551)
## Summary of the Pull Request

This PR replaces the Go home when activated setting with a new
Automatically return home option. This allows users to specify how long
the Command Palette should wait after being dismissed before
automatically returning to the home page. It also introduces migration
logic to transition from the old setting to the new one.

## Pictures? Pictures!

<img width="1337" height="762" alt="image"
src="https://github.com/user-attachments/assets/c649ef03-b3ee-40ba-ac67-485bc40efa73"
/>


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

- [x] Closes: #43355 
<!-- - [ ] 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
2025-11-27 09:24:47 -06:00
Kai Tao
1b72c0b969 Update check-spelling expect list (#43925)
<!-- 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

Spell no complain
2025-11-27 17:22:59 +08:00
Pratyush Nalam
9160c82fc2 Update Command Palette's Learn More string to be consistent with other utilities (#43898)
<!-- 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

In the PowerToys "What's new" window, every utility has the text "Learn
more about <utility name>" next to the "Settings" button. Examples
below:

<img width="247" height="38" alt="learnmore-fancyzones"
src="https://github.com/user-attachments/assets/fecdeb4b-e01c-438d-8d11-c056e613768e"
/>
<img width="258" height="40" alt="learnmore-textextractor"
src="https://github.com/user-attachments/assets/ffb0c801-5b89-46d1-b493-b57287303e65"
/>

The only exception is the Command Palette utility which just says "Learn
more".

<img width="152" height="32" alt="learnmore-cmdpal"
src="https://github.com/user-attachments/assets/232c11cd-b621-46eb-87f1-d3fc708d6286"
/>

This is an inconsistency and this PR fixes that string.

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

- [x] Closes: #43897
<!-- - [ ] 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
2025-11-27 00:45:06 +01:00
Mike Hall
452e0dcf51 Module Loader tool for rapid testing of modules (#43813)
## Summary of the Pull Request
ModuleLoader tool, a stand-alone Win32 executable for testing of
PowerToy modules without needing branch builds.

sample output from running the tool is below:

.\ModuleLoader.exe .\powertoys.cursorwrap.dll
PowerToys Module Loader v1.0
=============================

Loading module: .\powertoys.cursorwrap.dll
Detected module name: cursorwrap

Loading settings...
Trying settings path:
C:\Users\mikehall\AppData\Local\Microsoft\PowerToys\cursorwrap\settings.json
Settings file loaded (315 characters)
Settings loaded successfully.

Loading module DLL...
Module instance created successfully
Module DLL loaded successfully.
Module key: CursorWrap
Module name: CursorWrap

Applying settings to module...
Settings applied.

Registering module hotkeys...
Module reports 1 legacy hotkey(s)
  Registering hotkey 0: Win+Alt+U - OK
Hotkeys registered: 1

Enabling module...
Module enabled.

=============================
Module is now running!
=============================

Module Status:
  - Name: CursorWrap
  - Key: CursorWrap
  - Enabled: Yes
  - Hotkeys: 1 registered

Registered Hotkeys:
  Win+Alt+U

Press Ctrl+C to exit.
You can press the module's hotkey to toggle its functionality.

Note that this doesn't integrate with Powertoys settings UI - this is
purely to test Powertoys module functionality.

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

## Detailed Description of the Pull Request / Additional comments
See details above.

## Validation Steps Performed
ModuleLoader tested on Windows 11, Surface Laptop 7 Pro.
2025-11-26 22:08:34 +08:00
Jiří Polášek
2c9a9e9fca CmdPal: Improve Command Palette behavior in "Last position" mode (#43543)
## Summary of the Pull Request

This PR improves Command Palette behavior in “Last position” mode:
- Correctly handles DPI changes between monitors.
- Ensures the window is always visible — if it’s fully off-screen or has
less than 100px visible on any axis, it is re-centered.



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

- [x] Closes: #43398
- [ ] **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
2025-11-24 16:57:10 -06:00
leileizhang
09c8c1d79a [Hot Fix] Fix Image Resizer not working on Win10 (#43763)
<!-- 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
Windows 10 can’t launch the app using the Sparse Package. Remove the app
manifest so that Image Resizer can start properly on Windows 10.

We will figure out how to support Sparse Packages on Windows 10 in the
next release.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #43747 #43734 #43722 #43759
<!-- - [ ] 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
2025-11-24 10:42:35 +08:00
leileizhang
95c8a83f79 [Hotfix] Remove the properties in Prompt Execution Settings for OpenAI (#43766)
<!-- 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
Remove the properties in Prompt Execution Settings for OpenAI, as the
new models may not support them.

Will try to expose them in the UI so users can add them on their own in
the next release.

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

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

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2025-11-24 10:08:12 +08:00
Kai Tao
2830ea919c Advanced Paste: Adjust model parameter to make the result longer (#43768)
<!-- 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
Adjust model parameter to make the result longer
<!-- 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
2025-11-24 09:51:27 +08:00
Dave Rayment
725ad21952 [Awake] Fix issue with timed mode not expiring correctly (#43785)
<!-- 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
Resolves an issue with the timed mode's expiry not completing correctly.

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

- [x] Closes: #43775
<!-- - [ ] 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
This was because of my recent change to the timed mode. The `Subscribe`
method on the `Observable` interval accidentally wired the completion
logic to the **Error** handler instead of the **Completion** handler
because of the use of a discard `_` instead of an empty parameter list
`()`. As a result of the incorrect overload being called, Awake stayed
in the Timed state despite the timer reaching zero.

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Confirmed that the timed mode times out and exits upon expiry.
2025-11-24 09:12:54 +08:00
Erik Anderson
ebc3a139c5 Fix typo in AI settings card description (#43757)
## Summary of the Pull Request
The word "cloud" does not use a vowel sound, so the preceding word
should be "A" instead of "An".

## PR Checklist

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

Co-authored-by: Erik Anderson <erikan@ntdev.microsoft.com>
2025-11-21 18:34:34 +08:00
Jaylyn Barbee
28dba2633e [Light Switch][Dev Docs] Clarify LightSwitchService and LightSwitchStateManager roles (#43748)
Updated LightSwitch module documentation to clarify the role of
LightSwitchService and LightSwitchStateManager.

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
2025-11-20 15:22:40 -08:00
54 changed files with 3098 additions and 429 deletions

View File

@@ -2,8 +2,8 @@ AAAAs
abcdefghjkmnpqrstuvxyz
abgr
ABlocked
ABOUTBOX
ABORTIFHUNG
ABOUTBOX
Abug
Acceleratorkeys
ACCEPTFILES
@@ -97,8 +97,8 @@ ASSOCSTR
ASYNCWINDOWPLACEMENT
ASYNCWINDOWPOS
atl
ATX
ATRIOX
ATX
aumid
authenticode
AUTOBUDDY
@@ -117,10 +117,10 @@ azman
azureaiinference
azureinference
azureopenai
backticks
bbwe
BCIE
bck
backticks
BESTEFFORT
bezelled
bhid
@@ -148,8 +148,8 @@ bmi
BNumber
BODGY
BOklab
Bootstrappers
BOOTSTRAPPERINSTALLFOLDER
Bootstrappers
BOTTOMALIGN
boxmodel
BPBF
@@ -176,17 +176,16 @@ BYPOSITION
CALCRECT
CALG
callbackptr
cabstr
calpwstr
caub
Cangjie
CANRENAME
Carlseibert
Canvascustomlayout
CAPTUREBLT
CAPTURECHANGED
CARETBLINKING
Carlseibert
CAtl
caub
CBN
cch
CCHDEVICENAME
@@ -206,11 +205,9 @@ changecursor
CHILDACTIVATE
CHILDWINDOW
CHOOSEFONT
CIBUILD
cidl
CIELCh
cim
claude
CImage
cla
CLASSDC
@@ -264,7 +261,6 @@ CONFIGW
CONFLICTINGMODIFIERKEY
CONFLICTINGMODIFIERSHORTCUT
CONOUT
coreclr
constexpr
contentdialog
contentfiles
@@ -276,6 +272,7 @@ copiedcolorrepresentation
coppied
copyable
COPYPEN
coreclr
COREWINDOW
Corpor
cotaskmem
@@ -284,18 +281,18 @@ countof
covrun
cpcontrols
cph
cppcoreguidelines
cplusplus
CPower
cppcoreguidelines
cpptools
cppvsdbg
cppwinrt
createdump
creativecommons
CREATEPROCESS
CREATESCHEDULEDTASK
CREATESTRUCT
CREATEWINDOWFAILED
creativecommons
CRECT
CRH
critsec
@@ -331,7 +328,6 @@ CYSCREEN
CYSMICON
CYVIRTUALSCREEN
Czechia
cziplib
Dac
dacl
DAffine
@@ -355,9 +351,7 @@ Deact
debugbreak
decryptor
Dedup
dfx
Deduplicator
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER
DEFAULTCOLOR
DEFAULTFLAGS
@@ -404,7 +398,6 @@ DISPLAYFREQUENCY
displayname
DISPLAYORIENTATION
divyan
djwsxzxb
Dlg
DLGFRAME
DLGMODALFRAME
@@ -417,7 +410,6 @@ DONTVALIDATEPATH
dotnet
downsampled
downsampling
Downsampled
downscale
DPICHANGED
DPIs
@@ -531,7 +523,6 @@ EXTRINSICPROPERTIES
eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FNumber
FARPROC
fdx
fesf
@@ -563,8 +554,8 @@ FIXEDSYS
flac
flyouts
FMask
foundrylocal
fmtid
FNumber
FOF
FOFX
FOLDERID
@@ -575,6 +566,7 @@ FORCEMINIMIZE
FORMATDLGORD
formatetc
FORPARSING
foundrylocal
FRAMECHANGED
frm
FROMTOUCH
@@ -593,13 +585,13 @@ gdi
gdiplus
GDIPVER
GDISCALED
geolocator
GETCLIENTAREAANIMATION
GETCURSEL
GETDESKWALLPAPER
GETDLGCODE
GETDPISCALEDSIZE
getfilesiginforedist
geolocator
GETHOTKEY
GETICON
GETLBTEXT
@@ -610,11 +602,12 @@ GETSCREENSAVERRUNNING
GETSECKEY
GETSTICKYKEYS
GETTEXTLENGTH
GIFs
gitmodules
GHND
gitmodules
GMEM
GNumber
googleai
googlegemini
gpedit
gpo
GPOCA
@@ -631,8 +624,6 @@ GValue
gwl
GWLP
GWLSTYLE
googleai
googlegemini
hangeul
Hanzi
Hardlines
@@ -743,9 +734,7 @@ IDCANCEL
IDD
idk
idl
IIM
idlist
ifd
IDOK
IDOn
IDR
@@ -754,15 +743,16 @@ ietf
IEXPLORE
IFACEMETHOD
IFACEMETHODIMP
ifd
IGNOREUNKNOWN
IGo
iid
IIM
Iindex
Ijwhost
ILD
IMAGEHLP
IMAGERESIZERCONTEXTMENU
IPTC
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
@@ -798,7 +788,6 @@ INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
INSTALLLOCATION
INSTALLMESSAGE
INSTALLPROPERTY
installscopeperuser
INSTALLSTARTMENUSHORTCUT
INSTALLSTATE
Inste
@@ -811,6 +800,7 @@ invokecommand
ipcmanager
IPREVIEW
ipreviewhandlervisualssetfont
IPTC
irow
irprops
isbi
@@ -854,15 +844,14 @@ keyvault
KILLFOCUS
killrunner
kmph
ksa
kvp
Kybd
LARGEICON
lastcodeanalysissucceeded
LASTEXITCODE
LAYOUTRTL
LCh
lbl
LCh
lcid
LCIDTo
lcl
@@ -878,10 +867,10 @@ LExit
lhwnd
LIBFUZZER
LIBID
lightswitch
LIMITSIZE
LIMITTEXT
lindex
lightswitch
linkid
LINKOVERLAY
LINQTo
@@ -892,6 +881,7 @@ LLKH
llkhf
LMEM
LMENU
lng
LOADFROMFILE
LOBYTE
localappdata
@@ -901,17 +891,14 @@ LOCATIONCHANGE
LOCKTYPE
LOGFONT
LOGFONTW
logon
lon
LOGMSG
logon
LOGPIXELSX
LOGPIXELSY
lng
lon
longdate
LONGNAMES
lowlevel
lquadrant
LOWORD
lparam
LPBITMAPINFOHEADER
@@ -945,6 +932,7 @@ lpv
LPW
lpwcx
lpwndpl
lquadrant
LReader
LRESULT
LSTATUS
@@ -971,6 +959,7 @@ MAKELONG
MAKELPARAM
makepri
MAKEWPARAM
Malware
manifestdependency
MAPPEDTOSAMEKEY
MAPTOSAMESHORTCUT
@@ -993,8 +982,8 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metadatas
metadatamatters
Metadatas
metafile
mfc
Mgmt
@@ -1040,9 +1029,6 @@ mousepointer
mouseutils
MOVESIZEEND
MOVESIZESTART
muxx
muxxc
muxxh
MRM
MRT
mru
@@ -1075,6 +1061,9 @@ MTND
MULTIPLEUSE
multizone
muxc
muxx
muxxc
muxxh
MVPs
mvvm
MVVMTK
@@ -1157,7 +1146,6 @@ nonstd
NOOWNERZORDER
NOPARENTNOTIFY
NOPREFIX
NPU
NOREDIRECTIONBITMAP
NOREDRAW
NOREMOVE
@@ -1186,6 +1174,7 @@ nowarn
NOZORDER
NPH
npmjs
NPU
NResize
NTAPI
ntdll
@@ -1210,16 +1199,15 @@ oldpath
oldtheme
oleaut
OLECHAR
ollama
onebranch
onnx
OOBEUI
openas
opencode
OPENFILENAME
opensource
openxmlformats
ollama
Olllama
onnx
OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE
ORSCANS
@@ -1292,6 +1280,7 @@ pguid
phbm
phbmp
phicon
Photoshop
phwnd
pici
pidl
@@ -1314,7 +1303,6 @@ pnid
PNMLINK
Poc
Podcasts
Photoshop
POINTERID
POINTERUPDATE
Pokedex
@@ -1409,10 +1397,9 @@ pwsz
pwtd
QDC
qit
QNN
Qualcomm
QITAB
QITABENT
QNN
qoi
Quarternary
QUERYENDSESSION
@@ -1422,8 +1409,8 @@ quickaccent
QUNS
RAII
RAlt
RAquadrant
randi
RAquadrant
rasterization
Rasterize
RAWINPUTDEVICE
@@ -1450,9 +1437,7 @@ regfile
REGISTERCLASSFAILED
REGISTRYHEADER
REGISTRYPREVIEWEXT
registryroot
regkey
regroot
regsvr
REINSTALLMODE
releaseblog
@@ -1505,7 +1490,6 @@ rstringalpha
rstringdigit
rtb
RTLREADING
rtm
runas
rundll
rungameid
@@ -1562,8 +1546,8 @@ SETRULES
SETSCREENSAVEACTIVE
SETSTICKYKEYS
SETTEXT
settingscard
SETTINGCHANGE
settingscard
SETTINGSCHANGED
settingsheader
settingshotkeycontrol
@@ -1708,6 +1692,7 @@ stringtable
stringval
Strm
strret
STRSAFE
stscanf
sttngs
Stubless
@@ -1719,7 +1704,6 @@ sublang
SUBMODULEUPDATE
subresource
Superbar
suntimes
sut
svchost
SVGIn
@@ -1753,7 +1737,6 @@ SYSTEMMODAL
SYSTEMTIME
TARG
TARGETAPPHEADER
TARGETDIR
targetentrypoint
TARGETHEADER
targetver
@@ -1783,10 +1766,10 @@ textextractor
TEXTINCLUDE
tfopen
tgz
THEMECHANGED
themeresources
THH
THICKFRAME
THEMECHANGED
THISCOMPONENT
throughs
TILEDWINDOW
@@ -1883,7 +1866,6 @@ USEINSTALLERFORTEST
USESHOWWINDOW
USESTDHANDLES
USRDLL
utm
UType
uuidv
uwp
@@ -1956,11 +1938,11 @@ Wca
WCE
wcex
WClass
WCRAPI
wcsicmp
wcsncpy
wcsnicmp
WCT
WCRAPI
WDA
wdm
wdp
@@ -1988,6 +1970,7 @@ WINDOWPLACEMENT
WINDOWPOSCHANGED
WINDOWPOSCHANGING
WINDOWSBUILDNUMBER
windowsml
windowssearch
windowssettings
WINDOWSTYLES
@@ -2003,9 +1986,8 @@ Winhook
WINL
winlogon
winmd
WINNT
windowsml
winml
WINNT
winres
winrt
winsdk
@@ -2067,20 +2049,21 @@ WTSAT
Wubi
WUX
Wwanpp
xap
XAxis
XButton
xclip
xcopy
xap
XDeployment
XDimension
xdf
XDimension
XDocument
XElement
xfd
XFile
XIncrement
XLoc
xmp
XNamespace
Xoshiro
XPels
@@ -2091,23 +2074,22 @@ xsi
XSpeed
XStr
xstyler
xmp
XTimer
XUP
XVIRTUALSCREEN
xxxxxx
YAxis
ycombinator
YIncrement
YDimension
YIncrement
yinle
yinyue
YPels
YPos
YResolution
YSpeed
YTimer
YStr
YTimer
YVIRTUALSCREEN
ZEROINIT
zonability

View File

@@ -53,17 +53,17 @@ Go to the [PowerToys GitHub releases][github-release-link], click Assets to reve
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysUserSetup-0.96.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.0/PowerToysSetup-0.96.0-arm64.exe
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-arm64.exe
| Description | Filename |
|----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.96.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.96.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.96.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.96.0-arm64.exe][ptMachineArm64] |
| Per user - x64 | [PowerToysUserSetup-0.96.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.96.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.96.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.96.1-arm64.exe][ptMachineArm64] |
</details>

View File

@@ -33,9 +33,12 @@ The **Light Switch** module lets users automatically transition between light an
> **Note:** Using the shortcut overrides the current schedule until the next transition event.
* **LightSwitchService**
Reads settings and applies theming. Runs a check every minute to ensure the state is correct.
* **LightSwitchService.cpp**
is the heart beat of the module. Controls ticking every minute and depending on user actions (manual override, settings changing, etc) triggers the state manager to perform the corresponding operation.
* **LightSwitchStateManager.cpp**
handles updating the state based on the signals sent by LightSwitchService.
* **SettingsXAML/LightSwitch**
Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.

View File

@@ -64,7 +64,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
return new OpenAIClient(
new ApiKeyCredential("none"),
new OpenAIClientOptions { Endpoint = endpointUri })
new OpenAIClientOptions { Endpoint = endpointUri, NetworkTimeout = TimeSpan.FromMinutes(5) })
.GetChatClient(modelId)
.AsIChatClient();
}

View File

@@ -215,7 +215,6 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
return new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
Temperature = 0.01,
};
}
}

View File

@@ -146,6 +146,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
var options = new ChatOptions
{
ModelId = modelReference,
MaxOutputTokens = 2048,
};
if (!string.IsNullOrWhiteSpace(systemPrompt))

View File

@@ -157,8 +157,6 @@ namespace AdvancedPaste.Services.CustomActions
{
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
{
Temperature = 0.01,
MaxTokens = 2000,
FunctionChoiceBehavior = null,
},
_ => new PromptExecutionSettings(),

View File

@@ -350,7 +350,7 @@ namespace Awake.Core
TrayHelper.TimedIcon,
TrayIconAction.Update);
},
_ => HandleTimerCompletion("timed"),
() => HandleTimerCompletion("timed"),
_tokenSource.Token);
}

View File

@@ -6,7 +6,9 @@ using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
@@ -15,6 +17,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SettingsModel : ObservableObject
{
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
[JsonIgnore]
public static readonly string FilePath;
@@ -30,8 +34,6 @@ public partial class SettingsModel : ObservableObject
public bool ShowAppDetails { get; set; }
public bool HotkeyGoesHome { get; set; }
public bool BackspaceGoesBack { get; set; }
public bool SingleClickActivates { get; set; }
@@ -56,6 +58,8 @@ public partial class SettingsModel : ObservableObject
public WindowPosition? LastWindowPosition { get; set; }
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
// END SETTINGS
///////////////////////////////////////////////////////////////////////////
@@ -98,12 +102,29 @@ public partial class SettingsModel : ObservableObject
{
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel) ?? new();
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
var migratedAny = false;
try
{
if (JsonNode.Parse(jsonContent) is JsonObject root)
{
migratedAny |= ApplyMigrations(root, loaded);
}
}
catch (Exception ex)
{
Debug.WriteLine($"Migration check failed: {ex}");
}
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
Debug.WriteLine("Loaded settings file");
return loaded ?? new();
if (migratedAny)
{
SaveSettings(loaded);
}
return loaded;
}
catch (Exception ex)
{
@@ -113,6 +134,51 @@ public partial class SettingsModel : ObservableObject
return new();
}
private static bool ApplyMigrations(JsonObject root, SettingsModel model)
{
var migrated = false;
// Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)
// The old 'HotkeyGoesHome' boolean indicated whether the "go home" action should happen immediately (true) or never (false).
// The new 'AutoGoHomeInterval' uses a TimeSpan: 'TimeSpan.Zero' means immediate, 'Timeout.InfiniteTimeSpan' means never.
migrated |= TryMigrate(
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
root,
model,
nameof(AutoGoHomeInterval),
DeprecatedHotkeyGoesHomeKey,
(settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
JsonSerializationContext.Default.Boolean);
return migrated;
}
private static bool TryMigrate<T>(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action<SettingsModel, T> apply, JsonTypeInfo<T> jsonTypeInfo)
{
try
{
// If new key already present, skip migration
if (root.ContainsKey(newKey) && root[newKey] is not null)
{
return false;
}
// If old key present, try to deserialize and apply
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
{
var value = oldNode.Deserialize<T>(jsonTypeInfo);
apply(model, value!);
return true;
}
}
catch (Exception ex)
{
Logger.LogError($"Error during migration {migrationName}.", ex);
}
return false;
}
public static void SaveSettings(SettingsModel model)
{
if (string.IsNullOrEmpty(FilePath))
@@ -139,6 +205,9 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value?.DeepClone();
}
// Remove deprecated keys
savedSettings.Remove(DeprecatedHotkeyGoesHomeKey);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized);

View File

@@ -11,6 +11,19 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SettingsViewModel : INotifyPropertyChanged
{
private static readonly List<TimeSpan> AutoGoHomeIntervals =
[
Timeout.InfiniteTimeSpan,
TimeSpan.Zero,
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(20),
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(60),
TimeSpan.FromSeconds(90),
TimeSpan.FromSeconds(120),
TimeSpan.FromSeconds(180),
];
private readonly SettingsModel _settings;
private readonly IServiceProvider _serviceProvider;
@@ -58,16 +71,6 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public bool HotkeyGoesHome
{
get => _settings.HotkeyGoesHome;
set
{
_settings.HotkeyGoesHome = value;
Save();
}
}
public bool BackspaceGoesBack
{
get => _settings.BackspaceGoesBack;
@@ -138,6 +141,25 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
public int AutoGoBackIntervalIndex
{
get
{
var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval);
return index >= 0 ? index : 0;
}
set
{
if (value >= 0 && value < AutoGoHomeIntervals.Count)
{
_settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
}
Save();
}
}
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsExtensionsViewModel Extensions { get; }

View File

@@ -2,21 +2,52 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Graphics;
namespace Microsoft.CmdPal.UI.ViewModels;
public sealed class WindowPosition
{
/// <summary>
/// Gets or sets left position in device pixels.
/// </summary>
public int X { get; set; }
/// <summary>
/// Gets or sets top position in device pixels.
/// </summary>
public int Y { get; set; }
/// <summary>
/// Gets or sets width in device pixels.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Gets or sets height in device pixels.
/// </summary>
public int Height { get; set; }
/// <summary>
/// Gets or sets width of the screen in device pixels where the window is located.
/// </summary>
public int ScreenWidth { get; set; }
/// <summary>
/// Gets or sets height of the screen in device pixels where the window is located.
/// </summary>
public int ScreenHeight { get; set; }
/// <summary>
/// Gets or sets DPI (dots per inch) of the display where the window is located.
/// </summary>
public int Dpi { get; set; }
/// <summary>
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
/// </summary>
public RectInt32 ToPhysicalWindowRectangle()
{
return new RectInt32(X, Y, Width, Height);
}
}

View File

@@ -18,6 +18,7 @@ using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input;
@@ -33,6 +34,8 @@ using Windows.UI.WindowManagement;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.HiDpi;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
@@ -48,10 +51,14 @@ public sealed partial class MainWindow : WindowEx,
IRecipient<QuitMessage>,
IDisposable
{
private const int DefaultWidth = 800;
private const int DefaultHeight = 480;
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
private readonly uint WM_TASKBAR_RESTART;
private readonly HWND _hwnd;
private readonly DispatcherTimer _autoGoHomeTimer;
private readonly WNDPROC? _hotkeyWndProc;
private readonly WNDPROC? _originalWndProc;
private readonly List<TopLevelHotkey> _hotkeys = [];
@@ -62,6 +69,7 @@ public sealed partial class MainWindow : WindowEx,
private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource;
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
private WindowPosition _currentWindowPosition = new();
@@ -69,6 +77,9 @@ public sealed partial class MainWindow : WindowEx,
{
InitializeComponent();
_autoGoHomeTimer = new DispatcherTimer();
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
unsafe
@@ -135,6 +146,15 @@ public sealed partial class MainWindow : WindowEx,
HideWindow();
}
private void OnAutoGoHomeTimerOnTick(object? s, object e)
{
_autoGoHomeTimer.Stop();
// BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon)
// and prevent the user from opening its context menu.
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
}
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{
if (e.Key == VirtualKey.GoBack)
@@ -173,22 +193,8 @@ public sealed partial class MainWindow : WindowEx,
return;
}
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
var workArea = displayArea.WorkArea;
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
var targetPoint = new PointInt32
{
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
};
AppWindow.Move(targetPoint);
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
private void PositionCentered(DisplayArea displayArea)
@@ -207,12 +213,16 @@ public sealed partial class MainWindow : WindowEx,
private void UpdateWindowPositionInMemory()
{
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
_currentWindowPosition = new WindowPosition
{
X = AppWindow.Position.X,
Y = AppWindow.Position.Y,
Width = AppWindow.Size.Width,
Height = AppWindow.Size.Height,
Dpi = (int)this.GetDpiForWindow(),
ScreenWidth = displayArea.WorkArea.Width,
ScreenHeight = displayArea.WorkArea.Height,
};
}
@@ -224,6 +234,9 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
_autoGoHomeInterval = settings.AutoGoHomeInterval;
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
}
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
@@ -283,6 +296,8 @@ public sealed partial class MainWindow : WindowEx,
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
{
StopAutoGoHome();
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
// Remember, IsIconic == "minimized", which is entirely different state
@@ -300,8 +315,8 @@ public sealed partial class MainWindow : WindowEx,
if (target == MonitorBehavior.ToLast)
{
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
else
{
@@ -330,6 +345,114 @@ public sealed partial class MainWindow : WindowEx,
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
}
/// <summary>
/// Ensures that the window rectangle is visible on-screen.
/// </summary>
/// <param name="windowRect">The window rectangle in physical pixels.</param>
/// <param name="originalScreen">The desktop area the window was positioned on.</param>
/// <param name="originalDpi">The window's original DPI.</param>
/// <returns>
/// A window rectangle in physical pixels, moved to the nearest display and resized
/// if the DPI has changed.
/// </returns>
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
{
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
if (displayArea is null)
{
return windowRect;
}
var workArea = displayArea.WorkArea;
if (workArea.Width <= 0 || workArea.Height <= 0)
{
// Fallback, nothing reasonable to do
return windowRect;
}
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
if (originalDpi <= 0)
{
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
}
var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0;
if (hasInvalidSize)
{
windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight);
}
// If we have a DPI change, scale the window rectangle accordingly
if (effectiveDpi != originalDpi)
{
var scalingFactor = effectiveDpi / (double)originalDpi;
windowRect = new RectInt32(
(int)Math.Round(windowRect.X * scalingFactor),
(int)Math.Round(windowRect.Y * scalingFactor),
(int)Math.Round(windowRect.Width * scalingFactor),
(int)Math.Round(windowRect.Height * scalingFactor));
}
var targetWidth = Math.Min(windowRect.Width, workArea.Width);
var targetHeight = Math.Min(windowRect.Height, workArea.Height);
// Ensure at least some minimum visible area (e.g., 100 pixels)
// This helps prevent the window from being entirely offscreen, regardless of display scaling.
const int minimumVisibleSize = 100;
var isOffscreen =
windowRect.X + minimumVisibleSize > workArea.X + workArea.Width ||
windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X ||
windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height ||
windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y;
// if the work area size has changed, re-center the window
var workAreaSizeChanged =
originalScreen.Width != workArea.Width ||
originalScreen.Height != workArea.Height;
int targetX;
int targetY;
var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize;
if (recenter)
{
targetX = workArea.X + ((workArea.Width - targetWidth) / 2);
targetY = workArea.Y + ((workArea.Height - targetHeight) / 2);
}
else
{
targetX = windowRect.X;
targetY = windowRect.Y;
}
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
}
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
{
var effectiveDpi = 96;
var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
if (!hMonitor.IsNull)
{
var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
if (hr == 0)
{
effectiveDpi = (int)dpiX;
}
else
{
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
}
}
if (effectiveDpi <= 0)
{
effectiveDpi = 96;
}
return effectiveDpi;
}
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
{
// Leaving a note here, in case we ever need it:
@@ -429,6 +552,25 @@ public sealed partial class MainWindow : WindowEx,
// If the window was not cloaked, then leave it hidden.
// Sure, it's not ideal, but at least it's not visible.
}
// Start auto-go-home timer
RestartAutoGoHome();
}
private void StopAutoGoHome()
{
_autoGoHomeTimer.Stop();
}
private void RestartAutoGoHome()
{
if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan)
{
return;
}
_autoGoHomeTimer.Stop();
_autoGoHomeTimer.Start();
}
private bool Cloak()
@@ -479,6 +621,9 @@ public sealed partial class MainWindow : WindowEx,
Y = _currentWindowPosition.Y,
Width = _currentWindowPosition.Width,
Height = _currentWindowPosition.Height,
Dpi = _currentWindowPosition.Dpi,
ScreenWidth = _currentWindowPosition.ScreenWidth,
ScreenHeight = _currentWindowPosition.ScreenHeight,
};
SettingsModel.SaveSettings(settings);

View File

@@ -345,7 +345,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
// Depending on the settings, either
// * Go home, or
// * Select the search text (if we should remain open on this page)
if (settings.HotkeyGoesHome)
if (settings.AutoGoHomeInterval == TimeSpan.Zero)
{
GoHome(false);
}

View File

@@ -51,8 +51,18 @@
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" />
<controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Never" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Immediately" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After10Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After20Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After30Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After60Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After90Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After120Seconds" />
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After180Seconds" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE933;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />

View File

@@ -344,12 +344,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Description" xml:space="preserve">
<value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value>
</data>
<data name="Settings_GeneralPage_GoHome_SettingsCard.Header" xml:space="preserve">
<value>Go home when activated</value>
</data>
<data name="Settings_GeneralPage_GoHome_SettingsCard.Description" xml:space="preserve">
<value>Automatically opens the home page upon activation</value>
</data>
<data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Header" xml:space="preserve">
<value>Highlight search on activate</value>
</data>
@@ -523,4 +517,37 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="GlobalErrorHandler_CrashMessageBox_Caption" xml:space="preserve">
<value>Command Palette - Fatal error</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_Never.Content" xml:space="preserve">
<value>Never</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_Immediately.Content" xml:space="preserve">
<value>Immediately</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After10Seconds.Content" xml:space="preserve">
<value>10 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After20Seconds.Content" xml:space="preserve">
<value>20 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After30Seconds.Content" xml:space="preserve">
<value>30 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After60Seconds.Content" xml:space="preserve">
<value>60 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After90Seconds.Content" xml:space="preserve">
<value>90 seconds</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After120Seconds.Content" xml:space="preserve">
<value>2 minutes</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_Item_After180Seconds.Content" xml:space="preserve">
<value>3 minutes</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Header" xml:space="preserve">
<value>Automatically return home</value>
</data>
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Description" xml:space="preserve">
<value>Automatically returns to home page after a period of inactivity when Command Palette is closed</value>
</data>
</root>

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.
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
public class MockBrowserInfoService : IBrowserInfoService
{
public BrowserInfo GetDefaultBrowser() => new() { Name = "mocked browser", Path = "C:\\mockery\\mock.exe" };
}

View File

@@ -26,8 +26,9 @@ public class QueryTests : CommandPaletteUnitTestBase
{
// Setup
var settings = new MockSettingsInterface();
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings);
var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText(string.Empty, query);
@@ -55,8 +56,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings);
var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText("abcdef", string.Empty);
@@ -90,8 +92,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings);
var page = new WebSearchListPage(settings, browserInfoService);
mockHistoryItems.Add(new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)));
@@ -123,8 +126,9 @@ public class QueryTests : CommandPaletteUnitTestBase
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings);
var page = new WebSearchListPage(settings, browserInfoService);
// Act
page.UpdateSearchText("abcdef", string.Empty);

View File

@@ -20,7 +20,9 @@ public class SettingsManagerTests : CommandPaletteUnitTestBase
{
// Setup
var settings = new MockSettingsInterface(historyItemCount: 5);
var page = new WebSearchListPage(settings);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings, browserInfoService);
var eventRaised = false;

View File

@@ -2,32 +2,28 @@
// 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.Ext.WebSearch.Helpers.Browser;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class OpenURLCommand : InvokableCommand
{
private readonly IBrowserInfoService _browserInfoService;
public string Url { get; internal set; } = string.Empty;
internal OpenURLCommand(string url)
internal OpenURLCommand(string url, IBrowserInfoService browserInfoService)
{
_browserInfoService = browserInfoService;
Url = url;
BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch;
Name = string.Empty;
}
public override CommandResult Invoke()
{
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{Url}"))
{
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen();
}
return CommandResult.Dismiss();
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return _browserInfoService.Open(Url) ? CommandResult.Dismiss() : CommandResult.KeepOpen();
}
}

View File

@@ -4,31 +4,31 @@
using System;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class SearchWebCommand : InvokableCommand
{
private readonly ISettingsInterface _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
public string Arguments { get; internal set; } = string.Empty;
public string Arguments { get; internal set; }
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager)
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{
Arguments = arguments;
BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch;
Name = Properties.Resources.open_in_default_browser;
Name = Resources.open_in_default_browser;
_settingsManager = settingsManager;
_browserInfoService = browserInfoService;
}
public override CommandResult Invoke()
{
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"? {Arguments}"))
if (!_browserInfoService.Open($"? {Arguments}"))
{
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen();

View File

@@ -5,9 +5,9 @@
using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
@@ -16,25 +16,34 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
private readonly SearchWebCommand _executeItem;
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle);
private string _title;
public FallbackExecuteSearchItem(SettingsManager settings)
: base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
private readonly IBrowserInfoService _browserInfoService;
public FallbackExecuteSearchItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
: base(new SearchWebCommand(string.Empty, settings, browserInfoService) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
{
_executeItem = (SearchWebCommand)this.Command!;
_executeItem = (SearchWebCommand)Command!;
_browserInfoService = browserInfoService;
Title = string.Empty;
Subtitle = string.Empty;
_executeItem.Name = string.Empty;
_title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
Icon = Icons.WebSearch;
}
private static string UpdateBrowserName(IBrowserInfoService browserInfoService)
{
var browserName = browserInfoService.GetDefaultBrowser()?.Name;
return string.IsNullOrWhiteSpace(browserName)
? Resources.open_in_default_browser
: string.Format(CultureInfo.CurrentCulture, PluginOpen, browserName);
}
public override void UpdateQuery(string query)
{
_executeItem.Arguments = query;
var isEmpty = string.IsNullOrEmpty(query);
_executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser;
Title = isEmpty ? string.Empty : _title;
_executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
}
}

View File

@@ -7,21 +7,26 @@ using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch;
internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
{
private readonly IBrowserInfoService _browserInfoService;
private readonly OpenURLCommand _executeItem;
private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url);
private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser);
public FallbackOpenURLItem(SettingsManager settings)
: base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title)
public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
: base(new OpenURLCommand(string.Empty, browserInfoService), Resources.open_url_fallback_title)
{
_executeItem = (OpenURLCommand)this.Command!;
ArgumentNullException.ThrowIfNull(browserInfoService);
_browserInfoService = browserInfoService;
_executeItem = (OpenURLCommand)Command!;
Title = string.Empty;
_executeItem.Name = string.Empty;
Subtitle = string.Empty;
@@ -39,7 +44,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
return;
}
var success = Uri.TryCreate(query, UriKind.Absolute, out var uri);
var success = Uri.TryCreate(query, UriKind.Absolute, out _);
// if url not contain schema, add http:// by default.
if (!success)
@@ -48,13 +53,15 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
}
_executeItem.Url = query;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.open_in_default_browser;
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Resources.open_in_default_browser;
Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
var browserName = _browserInfoService.GetDefaultBrowser()?.Name;
Subtitle = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
}
public static bool IsValidUrl(string url)
private static bool IsValidUrl(string url)
{
if (string.IsNullOrWhiteSpace(url))
{

View File

@@ -0,0 +1,14 @@
// 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.Ext.WebSearch.Helpers.Browser;
public record BrowserInfo
{
public required string Path { get; init; }
public required string Name { get; init; }
public string? ArgumentsPattern { get; init; }
}

View File

@@ -0,0 +1,32 @@
// 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.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Extension methods for <see cref="IBrowserInfoService"/>.
/// </summary>
/// <seealso cref="IBrowserInfoService"/>
internal static class BrowserInfoServiceExtensions
{
/// <summary>
/// Opens the specified URL in the system's default web browser.
/// </summary>
/// <param name="browserInfoService">The browser information service used to resolve the system's default browser.</param>
/// <param name="url">The URL to open.</param>
/// <returns>
/// <see langword="true"/> if a default browser is found and the URL launch command is issued successfully;
/// otherwise, <see langword="false"/>.
/// </returns>
/// <remarks>
/// Returns <see langword="false"/> if the default browser cannot be determined.
/// </remarks>
public static bool Open(this IBrowserInfoService browserInfoService, string url)
{
var defaultBrowser = browserInfoService.GetDefaultBrowser();
return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url);
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Threading;
using ManagedCommon;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Service to get information about the default browser.
/// </summary>
internal class DefaultBrowserInfoService : IBrowserInfoService
{
private static readonly IDefaultBrowserProvider[] Providers =
[
new ShellAssociationProvider(),
new LegacyRegistryAssociationProvider(),
new FallbackMsEdgeBrowserProvider(),
];
private readonly Lock _updateLock = new();
private readonly Dictionary<Type, string> _lastLoggedErrors = [];
private const long UpdateTimeout = 3000;
private long _lastUpdateTickCount = -UpdateTimeout;
private BrowserInfo? _defaultBrowser;
public BrowserInfo? GetDefaultBrowser()
{
try
{
UpdateIfTimePassed();
}
catch (Exception)
{
// exception is already logged at this point
}
return _defaultBrowser;
}
/// <summary>
/// Updates only if at least more than 3000ms has passed since the last update, to avoid multiple calls to <see cref="UpdateCore"/>.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
private void UpdateIfTimePassed()
{
lock (_updateLock)
{
var curTickCount = Environment.TickCount64;
if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
{
return;
}
var newDefaultBrowser = UpdateCore();
_defaultBrowser = newDefaultBrowser;
_lastUpdateTickCount = curTickCount;
}
}
/// <summary>
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
private BrowserInfo UpdateCore()
{
foreach (var provider in Providers)
{
try
{
var result = provider.GetDefaultBrowserInfo();
#if DEBUG
result = result with { Name = result.Name + " (" + provider.GetType().Name + ")" };
#endif
return result;
}
catch (Exception ex)
{
// since we run this fairly often, avoid logging the same error multiple times
var lastLoggedError = _lastLoggedErrors.GetValueOrDefault(provider.GetType());
var error = ex.ToString();
if (error != lastLoggedError)
{
_lastLoggedErrors[provider.GetType()] = error;
Logger.LogError($"Exception when retrieving browser using provider {provider.GetType()}", ex);
}
}
}
throw new InvalidOperationException("Unable to determine default browser");
}
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
/// <summary>
/// Provides functionality to retrieve information about the system's default web browser.
/// </summary>
public interface IBrowserInfoService
{
/// <summary>
/// Gets information about the system's default web browser.
/// </summary>
/// <returns></returns>
BrowserInfo? GetDefaultBrowser();
}

View File

@@ -0,0 +1,7 @@
// 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.Ext.WebSearch.Helpers.Browser.Providers;
internal record AssociatedApp(string? Command, string? FriendlyName);

View File

@@ -0,0 +1,154 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using Windows.Win32;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Base class for providers that determine the default browser via application associations.
/// </summary>
internal abstract class AssociationProviderBase : IDefaultBrowserProvider
{
protected abstract AssociatedApp? FindAssociation();
public BrowserInfo GetDefaultBrowserInfo()
{
var appAssociation = FindAssociation();
if (appAssociation is null)
{
throw new ArgumentNullException(nameof(appAssociation), "Could not determine default browser application.");
}
var commandPattern = appAssociation.Command;
var appAndArgs = SplitAppAndArgs(commandPattern);
if (string.IsNullOrEmpty(appAndArgs.Path))
{
throw new ArgumentOutOfRangeException(nameof(appAndArgs.Path), "Default browser program path could not be determined.");
}
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
if (!Path.Exists(appAndArgs.Path) && !Uri.TryCreate(appAndArgs.Path, UriKind.Absolute, out _))
{
throw new ArgumentException($"Command validation failed: {commandPattern}", nameof(commandPattern));
}
return new BrowserInfo
{
Path = appAndArgs.Path,
Name = appAssociation.FriendlyName ?? Path.GetFileNameWithoutExtension(appAndArgs.Path),
ArgumentsPattern = appAndArgs.Arguments,
};
}
private static (string? Path, string? Arguments) SplitAppAndArgs(string? commandPattern)
{
if (string.IsNullOrEmpty(commandPattern))
{
throw new ArgumentOutOfRangeException(nameof(commandPattern), "Default browser program command is not specified.");
}
commandPattern = GetIndirectString(commandPattern);
// HACK: for firefox installed through Microsoft store
// When installed through Microsoft Firefox the commandPattern does not have
// quotes for the path. As the Program Files does have a space
// the extracted path would be invalid, here we add the quotes to fix it
const string FirefoxExecutableName = "firefox.exe";
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") &&
!commandPattern.StartsWith('\"'))
{
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) +
FirefoxExecutableName.Length;
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
commandPattern = commandPattern.Insert(0, "\"");
}
if (commandPattern.StartsWith('\"'))
{
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
if (endQuoteIndex != -1)
{
return (commandPattern[1..endQuoteIndex], commandPattern[(endQuoteIndex + 1)..].Trim());
}
}
else
{
var spaceIndex = commandPattern.IndexOf(' ');
if (spaceIndex != -1)
{
return (commandPattern[..spaceIndex], commandPattern[(spaceIndex + 1)..].Trim());
}
}
return (null, null);
}
protected static string GetIndirectString(string str)
{
if (string.IsNullOrEmpty(str) || str[0] != '@')
{
return str;
}
const int initialCapacity = 128;
const int maxCapacity = 8192; // Reasonable upper limit
int hresult;
unsafe
{
// Try with stack allocation first for common cases
var stackBuffer = stackalloc char[initialCapacity];
fixed (char* pszSource = str)
{
hresult = PInvoke.SHLoadIndirectString(
pszSource,
stackBuffer,
initialCapacity,
null);
// S_OK (0) means success
if (hresult == 0)
{
return new string(stackBuffer);
}
// STRSAFE_E_INSUFFICIENT_BUFFER (0x8007007A) means buffer too small
// Try with progressively larger heap buffers
if (unchecked((uint)hresult) == 0x8007007A)
{
for (var capacity = initialCapacity * 2; capacity <= maxCapacity; capacity *= 2)
{
var heapBuffer = new char[capacity];
fixed (char* pBuffer = heapBuffer)
{
hresult = PInvoke.SHLoadIndirectString(
pszSource,
pBuffer,
(uint)capacity,
null);
if (hresult == 0)
{
return new string(pBuffer);
}
if (unchecked((uint)hresult) != 0x8007007A)
{
break; // Different error, stop retrying
}
}
}
}
}
}
throw new InvalidOperationException(
$"Could not load indirect string. HRESULT: 0x{unchecked((uint)hresult):X8}");
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Provides a fallback implementation of the default browser provider that returns information for Microsoft Edge.
/// </summary>
/// <remarks>This class is used when no other default browser provider is available. It supplies the path,
/// arguments pattern, and name for Microsoft Edge as the default browser information.</remarks>
internal sealed class FallbackMsEdgeBrowserProvider : IDefaultBrowserProvider
{
private const string MsEdgeArgumentsPattern = "--single-argument %1";
private const string MsEdgeName = "Microsoft Edge";
private static string MsEdgePath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
@"Microsoft\Edge\Application\msedge.exe");
public BrowserInfo GetDefaultBrowserInfo() => new()
{
Path = MsEdgePath,
ArgumentsPattern = MsEdgeArgumentsPattern,
Name = MsEdgeName,
};
}

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.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Retrieves information about the default browser.
/// </summary>
internal interface IDefaultBrowserProvider
{
BrowserInfo GetDefaultBrowserInfo();
}

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.
using System;
using Microsoft.Win32;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Provides the default web browser by reading registry keys. This is a legacy method and may not work on all systems.
/// </summary>
internal sealed class LegacyRegistryAssociationProvider : AssociationProviderBase
{
protected override AssociatedApp? FindAssociation()
{
var progId = GetRegistryValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId",
"ProgId")
?? GetRegistryValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
"ProgId");
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");
if (appName is not null)
{
appName = GetIndirectString(appName);
appName = appName
.Replace("URL", null, StringComparison.OrdinalIgnoreCase)
.Replace("HTML", null, StringComparison.OrdinalIgnoreCase)
.Replace("Document", null, StringComparison.OrdinalIgnoreCase)
.Replace("Web", null, StringComparison.OrdinalIgnoreCase)
.TrimEnd();
}
var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null);
return commandPattern is null ? null : new AssociatedApp(commandPattern, appName);
static string? GetRegistryValue(string registryLocation, string? valueName)
{
return Registry.GetValue(registryLocation, valueName, null) as string;
}
}
}

View File

@@ -0,0 +1,64 @@
// 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.Ext.WebSearch.Helpers.Browser.Providers;
/// <summary>
/// Retrieves the default web browser using the system shell functions.
/// </summary>
internal sealed class ShellAssociationProvider : AssociationProviderBase
{
private static readonly string[] Protocols = ["https", "http"];
protected override AssociatedApp FindAssociation()
{
foreach (var protocol in Protocols)
{
var command = AssocQueryStringSafe(NativeMethods.AssocStr.Command, protocol);
if (string.IsNullOrWhiteSpace(command))
{
continue;
}
var appName = AssocQueryStringSafe(NativeMethods.AssocStr.FriendlyAppName, protocol);
return new AssociatedApp(command, appName);
}
return new AssociatedApp(null, null);
}
private static unsafe string? AssocQueryStringSafe(NativeMethods.AssocStr what, string protocol)
{
uint cch = 0;
// First call: get required length (incl. null)
_ = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, null, ref cch);
if (cch == 0)
{
return null;
}
// Small buffers on stack; large on heap
var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch];
fixed (char* p = span)
{
var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch);
if (hr != 0 || cch == 0)
{
return null;
}
// cch includes the null terminator; slice it off
var len = (int)cch - 1;
if (len < 0)
{
len = 0;
}
return new string(span[..len]);
}
}
}

View File

@@ -1,215 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Text;
using System.Threading;
using ManagedCommon;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
/// <summary>
/// Contains information (e.g. path to executable, name...) about the default browser.
/// </summary>
public static class DefaultBrowserInfo
{
private static readonly Lock _updateLock = new();
/// <summary>Gets the path to the MS Edge browser executable.</summary>
public static string MSEdgePath => System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
@"Microsoft\Edge\Application\msedge.exe");
/// <summary>Gets the command line pattern of the MS Edge.</summary>
public const string MSEdgeArgumentsPattern = "--single-argument %1";
public const string MSEdgeName = "Microsoft Edge";
/// <summary>Gets the path to default browser's executable.</summary>
public static string? Path { get; private set; }
/// <summary>Gets <see cref="Path"/> since the icon is embedded in the executable.</summary>
public static string? IconPath => Path;
/// <summary>Gets the user-friendly name of the default browser.</summary>
public static string? Name { get; private set; }
/// <summary>Gets the command line pattern of the default browser.</summary>
public static string? ArgumentsPattern { get; private set; }
public static bool IsDefaultBrowserSet => !string.IsNullOrEmpty(Path);
public const long UpdateTimeout = 300;
private static long _lastUpdateTickCount = -UpdateTimeout;
private static bool _updatedOnce;
private static bool _errorLogged;
/// <summary>
/// Updates only if at least more than 300ms has passed since the last update, to avoid multiple calls to <see cref="Update"/>.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
public static void UpdateIfTimePassed()
{
var curTickCount = Environment.TickCount64;
if (curTickCount - _lastUpdateTickCount >= UpdateTimeout)
{
_lastUpdateTickCount = curTickCount;
Update();
}
}
/// <summary>
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
/// (because of multiple plugins calling update at the same time.)
/// </summary>
public static void Update()
{
lock (_updateLock)
{
if (!_updatedOnce)
{
// Log.Info("I've tried updating the chosen Web Browser info at least once.", typeof(DefaultBrowserInfo));
_updatedOnce = true;
}
try
{
var progId = GetRegistryValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId",
"ProgId")
?? GetRegistryValue(
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
"ProgId");
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");
if (appName is not null)
{
// Handle indirect strings:
if (appName.StartsWith('@'))
{
appName = GetIndirectString(appName);
}
appName = appName
.Replace("URL", null, StringComparison.OrdinalIgnoreCase)
.Replace("HTML", null, StringComparison.OrdinalIgnoreCase)
.Replace("Document", null, StringComparison.OrdinalIgnoreCase)
.Replace("Web", null, StringComparison.OrdinalIgnoreCase)
.TrimEnd();
}
Name = appName;
var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null);
if (string.IsNullOrEmpty(commandPattern))
{
throw new ArgumentOutOfRangeException(
nameof(commandPattern),
"Default browser program command is not specified.");
}
if (commandPattern.StartsWith('@'))
{
commandPattern = GetIndirectString(commandPattern);
}
// HACK: for firefox installed through Microsoft store
// When installed through Microsoft Firefox the commandPattern does not have
// quotes for the path. As the Program Files does have a space
// the extracted path would be invalid, here we add the quotes to fix it
const string FirefoxExecutableName = "firefox.exe";
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") && (!commandPattern.StartsWith('\"')))
{
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) + FirefoxExecutableName.Length;
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
commandPattern = commandPattern.Insert(0, "\"");
}
if (commandPattern.StartsWith('\"'))
{
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
if (endQuoteIndex != -1)
{
Path = commandPattern.Substring(1, endQuoteIndex - 1);
ArgumentsPattern = commandPattern.Substring(endQuoteIndex + 1).Trim();
}
}
else
{
var spaceIndex = commandPattern.IndexOf(' ');
if (spaceIndex != -1)
{
Path = commandPattern.Substring(0, spaceIndex);
ArgumentsPattern = commandPattern.Substring(spaceIndex + 1).Trim();
}
}
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
if (!System.IO.Path.Exists(Path) && !Uri.TryCreate(Path, UriKind.Absolute, out _))
{
throw new ArgumentException(
$"Command validation failed: {commandPattern}",
nameof(commandPattern));
}
if (string.IsNullOrEmpty(Path))
{
throw new ArgumentOutOfRangeException(
nameof(Path),
"Default browser program path could not be determined.");
}
}
catch (Exception)
{
// Fallback to MS Edge
Path = MSEdgePath;
Name = MSEdgeName;
ArgumentsPattern = MSEdgeArgumentsPattern;
if (!_errorLogged)
{
// Log.Exception("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.", e, typeof(DefaultBrowserInfo));
Logger.LogError("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.");
_errorLogged = true;
}
}
string? GetRegistryValue(string registryLocation, string? valueName)
{
return Microsoft.Win32.Registry.GetValue(registryLocation, valueName, null) as string;
}
string GetIndirectString(string str)
{
var stringBuilder = new StringBuilder(128);
unsafe
{
var buffer = stackalloc char[128];
var capacity = 128;
var firstChar = str[0];
var strPtr = &firstChar;
// S_OK == 0
fixed (char* pszSourceLocal = str)
{
if (global::Windows.Win32.PInvoke.SHLoadIndirectString(
pszSourceLocal,
buffer,
(uint)capacity,
default) == 0)
{
return new string(buffer);
}
}
}
throw new ArgumentNullException(nameof(str), "Could not load indirect string.");
}
}
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
internal static partial class NativeMethods
{
[LibraryImport("shlwapi.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = false)]
internal static unsafe partial int AssocQueryStringW(
AssocF flags,
AssocStr str,
string pszAssoc,
string? pszExtra,
char* pszOut,
ref uint pcchOut);
[Flags]
public enum AssocF : uint
{
None = 0,
IsProtocol = 0x00001000,
}
public enum AssocStr
{
Command = 1,
Executable,
FriendlyDocName,
FriendlyAppName,
NoOpen,
ShellNewValue,
DDECommand,
DDEIfExec,
DDEApplication,
DDETopic,
InfoTip,
QuickTip,
TileInfo,
ContentType,
DefaultIcon,
ShellExtension,
DropTarget,
DelegateExecute,
SupportedUriProtocols,
ProgId,
AppId,
AppPublisher,
AppIconReference, // sometimes present, but DefaultIcon is most common
}
}

View File

@@ -9,23 +9,24 @@ using System.Text;
using System.Threading;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{
private readonly ISettingsInterface _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
private readonly Lock _sync = new();
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private IListItem[] _allItems = [];
private List<ListItem> _historyItems = [];
public WebSearchListPage(ISettingsInterface settingsManager)
public WebSearchListPage(ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{
ArgumentNullException.ThrowIfNull(settingsManager);
@@ -35,6 +36,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager;
_browserInfoService = browserInfoService;
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
// It just looks viewer to have string twice on the page, and default placeholder is good enough
@@ -43,8 +45,8 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
EmptyContent = new CommandItem(new NoOpCommand())
{
Icon = Icon,
Title = Properties.Resources.plugin_description,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
Title = Resources.plugin_description,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
};
UpdateHistory();
@@ -67,7 +69,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
for (var index = items.Count - 1; index >= 0; index--)
{
var historyItem = items[index];
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager, _browserInfoService))
{
Icon = Icons.History,
Title = historyItem.SearchString,
@@ -82,7 +84,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
}
}
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager)
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{
ArgumentNullException.ThrowIfNull(query);
@@ -95,10 +97,10 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
if (!string.IsNullOrEmpty(query))
{
var searchTerm = query;
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager, browserInfoService))
{
Title = searchTerm,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
Icon = Icons.Search,
};
results.Add(result);
@@ -117,7 +119,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
historySnapshot = _historyItems;
}
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager);
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _browserInfoService);
lock (_sync)
{

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
// 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -69,6 +69,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to default browser.
/// </summary>
public static string default_browser {
get {
return ResourceManager.GetString("default_browser", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Web Search.
/// </summary>

View File

@@ -184,4 +184,7 @@
<data name="open_url_fallback_title" xml:space="preserve">
<value>Open URL</value>
</data>
<data name="default_browser" xml:space="preserve">
<value>default browser</value>
</data>
</root>

View File

@@ -5,6 +5,7 @@
using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -19,6 +20,7 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
private readonly ICommandItem[] _topLevelItems;
private readonly IFallbackCommandItem[] _fallbackCommands;
private readonly IBrowserInfoService _browserInfoService = new DefaultBrowserInfoService();
public WebSearchCommandsProvider()
{
@@ -27,10 +29,10 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
Icon = Icons.WebSearch;
Settings = _settingsManager.Settings;
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager, _browserInfoService);
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager, _browserInfoService);
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager, _browserInfoService)
{
MoreCommands =
[

View File

@@ -5,6 +5,7 @@
using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Pages;
using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions;
@@ -15,13 +16,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch;
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
{
private readonly SettingsManager _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
public WebSearchTopLevelCommandItem(SettingsManager settingsManager)
: base(new WebSearchListPage(settingsManager))
public WebSearchTopLevelCommandItem(SettingsManager settingsManager, IBrowserInfoService browserInfoService)
: base(new WebSearchListPage(settingsManager, browserInfoService))
{
Icon = Icons.WebSearch;
SetDefaultTitle();
_settingsManager = settingsManager;
_browserInfoService = browserInfoService;
}
private void SetDefaultTitle() => Title = Resources.command_item_title;
@@ -37,12 +40,12 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
if (string.IsNullOrEmpty(query))
{
SetDefaultTitle();
ReplaceCommand(new WebSearchListPage(_settingsManager));
ReplaceCommand(new WebSearchListPage(_settingsManager, _browserInfoService));
}
else
{
Title = query;
ReplaceCommand(new SearchWebCommand(query, _settingsManager));
ReplaceCommand(new SearchWebCommand(query, _settingsManager, _browserInfoService));
}
}

View File

@@ -24,13 +24,13 @@
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<!-- <PropertyGroup>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'">
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
</PropertyGroup>
</PropertyGroup> -->
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
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
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>
@@ -26,36 +26,36 @@
<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
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
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
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
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
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
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
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -3410,7 +3410,7 @@ Activate by holding the key for the character you want to add an accent to, then
<value>An AI powered tool to put your clipboard content into any format you need, focused towards developer workflows.</value>
</data>
<data name="AdvancedPaste_EnableAISettingsCardDescription.Text" xml:space="preserve">
<value>Transform your clipboard content with the power of AI. An cloud or local endpoint is required.</value>
<value>Transform your clipboard content with the power of AI. A cloud or local endpoint is required.</value>
</data>
<data name="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore.Content" xml:space="preserve">
<value>Learn more</value>
@@ -5164,7 +5164,7 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<comment>Command Palette is a product name, do not loc</comment>
</data>
<data name="LearnMore_CmdPal.Text" xml:space="preserve">
<value>Learn more</value>
<value>Learn more about Command Palette</value>
</data>
<data name="Shell_CmdPal.Content" xml:space="preserve">
<value>Command Palette</value>
@@ -5781,4 +5781,4 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<value>A modern UI built with Fluent Design</value>
<comment>Fluent Design is a product name, do not loc</comment>
</data>
</root>
</root>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="Microsoft.PowerToys.ModuleLoader"
type="win32"
/>
<description>PowerToys Module Loader - Standalone module testing utility</description>
<!-- Per-Monitor DPI Awareness V2 -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<!-- Request administrator execution level if needed -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<!-- Windows 10+ compatibility -->
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9b}"/>
</application>
</compatibility>
</assembly>

View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}</ProjectGuid>
<RootNamespace>ModuleLoader</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>ModuleLoader</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>ModuleLoader</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<TreatWarningAsError>false</TreatWarningAsError>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
</Link>
<Manifest>
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
</Manifest>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="src\main.cpp" />
<ClCompile Include="src\ModuleLoader.cpp" />
<ClCompile Include="src\SettingsLoader.cpp" />
<ClCompile Include="src\HotkeyManager.cpp" />
<ClCompile Include="src\ConsoleHost.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\ModuleLoader.h" />
<ClInclude Include="src\SettingsLoader.h" />
<ClInclude Include="src\HotkeyManager.h" />
<ClInclude Include="src\ConsoleHost.h" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\ModuleLoader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\SettingsLoader.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\HotkeyManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="src\ConsoleHost.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\ModuleLoader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\SettingsLoader.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\HotkeyManager.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="src\ConsoleHost.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="README.md" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,483 @@
# Sharing ModuleLoader and Modules
This guide explains how to share the ModuleLoader tool and PowerToy modules with others for testing purposes.
## Overview
The ModuleLoader is designed to be a **portable, standalone testing tool** that can be shared with module developers and testers. It has minimal dependencies and can work with any compatible PowerToy module DLL.
---
## What You Need to Share
### For Testing a Module (e.g., CursorWrap)
#### **Minimum Package** (Recommended for Quick Testing)
1. **ModuleLoader.exe** - The standalone loader application
- Location: `x64\Debug\ModuleLoader.exe` or `x64\Release\ModuleLoader.exe`
- No additional DLLs required (uses only Windows system libraries)
2. **The Module DLL** - The PowerToy module to test
- Example: `CursorWrap.dll` from `x64\Debug\` or `x64\Release\`
- Location varies by module (see module-specific locations below)
3. **settings.json** - Module configuration (place in same folder as the DLL)
- **NEW**: Settings can be placed alongside the module DLL for portable testing
- Location: Same directory as the module DLL (e.g., `settings.json` next to `CursorWrap.dll`)
- Falls back to: `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json` if not found locally
#### **Complete Standalone Package** (For Users Without PowerToys Installed)
1. **ModuleLoader.exe**
2. **Module DLL**
3. **Sample settings.json** - Pre-configured settings file
4. **Installation instructions** - See "Standalone Package Setup" section below
---
### Debug Builds
If you build the module in Debug configuration:
- The module will output debug messages via `OutputDebugString()`
- View these with [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) or Visual Studio Output window
- Example: CursorWrap outputs detailed topology and cursor wrapping debug info
---
## Module-Specific File Locations
### CursorWrap
```
Files to share:
- x64\Debug\CursorWrap.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\settings.json
Size: ~100KB
```
### MouseHighlighter
```
Files to share:
- x64\Debug\MouseHighlighter.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseHighlighter\settings.json
Size: ~150KB
```
### FindMyMouse
```
Files to share:
- x64\Debug\FindMyMouse.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\FindMyMouse\settings.json
Size: ~120KB
```
### MousePointerCrosshairs
```
Files to share:
- x64\Debug\MousePointerCrosshairs.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MousePointerCrosshairs\settings.json
Size: ~140KB
```
### MouseJump
```
Files to share:
- x64\Debug\MouseJump.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseJump\settings.json
Note: MouseJump is a UI-based module and may not work fully with ModuleLoader
Size: ~200KB
```
### AlwaysOnTop
```
Files to share:
- x64\Debug\AlwaysOnTop.dll (or Release)
- %LOCALAPPDATA%\Microsoft\PowerToys\AlwaysOnTop\settings.json
Size: ~100KB
```
---
## Dependency Analysis
### ModuleLoader.exe Dependencies
**Windows System Libraries Only** (automatically available on all Windows systems):
- `KERNEL32.dll` - Core Windows API
- `USER32.dll` - User interface functions
- `SHELL32.dll` - Shell functions
- `ole32.dll` - COM library
**No PowerToys dependencies required!** The ModuleLoader is completely standalone.
### Module DLL Dependencies (Typical)
Most PowerToy modules depend on:
- Windows system DLLs (automatically available)
- PowerToys common libraries (if any, they're typically statically linked)
- **Module settings** - Must be present in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
**Important**: Modules are generally **self-contained** and statically link most dependencies. You typically only need the module DLL itself.
---
## Creating a Standalone Package
### Step 1: Prepare the Files
Create a folder structure like this:
```
ModuleLoaderPackage\
??? ModuleLoader.exe
??? CursorWrap.dll (or other module)
??? settings.json (module settings - placed locally!)
```
**NEW Simplified Structure**: You can now place `settings.json` directly alongside the module DLL! The ModuleLoader will check this location first before looking in the standard PowerToys settings directories.
### Step 2: Extract Settings from Your Machine
```powershell
# Copy settings from your development machine
$moduleName = "CursorWrap" # Change as needed
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json"
Copy-Item $settingsPath ".\settings\$moduleName\settings.json"
```
### Step 3: Create Installation Instructions (README.txt)
```text
PowerToys Module Testing Package
=================================
This package contains the ModuleLoader tool for testing PowerToy modules.
Contents:
- ModuleLoader.exe : Standalone module loader
- modules\*.dll : PowerToy module(s) to test
- settings\*\*.json : Module configuration files
Setup (First Time):
-------------------
1. Create settings directory:
%LOCALAPPDATA%\Microsoft\PowerToys\
2. Copy settings:
Copy the entire "settings\<ModuleName>" folder to:
%LOCALAPPDATA%\Microsoft\PowerToys\
Example for CursorWrap:
Copy "settings\CursorWrap" to:
%LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\
Usage:
------
ModuleLoader.exe modules\CursorWrap.dll
The tool will:
- Load the module DLL
- Read settings from %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\
- Register hotkeys
- Enable the module
Press Ctrl+C to exit.
Press the module's hotkey to toggle functionality.
Requirements:
-------------
- Windows 10 1803 or later
- No PowerToys installation required!
Troubleshooting:
----------------
If you see "Settings file not found":
1. Make sure you copied the settings folder correctly
2. Check that the path is:
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
3. You can also run PowerToys once to generate default settings
Debug Logs:
-----------
Module logs are written to:
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\Logs\
For debug builds, use DebugView to see real-time output.
```
---
## Quick Distribution Methods
### Method 1: ZIP Archive
```powershell
# Create a complete package
$moduleName = "CursorWrap"
$packageName = "ModuleLoader-$moduleName-Package"
# Collect files
New-Item $packageName -ItemType Directory
Copy-Item "x64\Debug\ModuleLoader.exe" "$packageName\"
New-Item "$packageName\modules" -ItemType Directory
Copy-Item "x64\Debug\$moduleName.dll" "$packageName\modules\"
New-Item "$packageName\settings\$moduleName" -ItemType Directory -Force
Copy-Item "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json" "$packageName\settings\$moduleName\"
# Create README
@"
See README in the tools\module_loader folder for instructions
"@ | Out-File "$packageName\README.txt"
# Zip it
Compress-Archive -Path $packageName -DestinationPath "$packageName.zip"
```
### Method 2: Direct Share (Advanced Users)
For developers who already have PowerToys installed:
```powershell
# Just share the executables
Copy-Item "x64\Debug\ModuleLoader.exe" "\\ShareLocation\"
Copy-Item "x64\Debug\CursorWrap.dll" "\\ShareLocation\"
```
They can run: `ModuleLoader.exe CursorWrap.dll`
(Settings will be loaded from their existing PowerToys installation)
---
## Platform-Specific Notes
### x64 vs ARM64
**Important**: Match architectures!
- `x64\Debug\ModuleLoader.exe` ? Only works with `x64` module DLLs
- `ARM64\Debug\ModuleLoader.exe` ? Only works with `ARM64` module DLLs
**Distribution Tip**: Provide both architectures if targeting multiple platforms:
```
ModuleLoaderPackage\
??? x64\
? ??? ModuleLoader.exe
? ??? modules\CursorWrap.dll
??? ARM64\
? ??? ModuleLoader.exe
? ??? modules\CursorWrap.dll
??? settings\...
```
### Debug vs Release
**Debug builds**:
- Larger file size
- Include debug symbols
- Verbose logging via `OutputDebugString()`
- Recommended for testing/development
**Release builds**:
- Smaller file size
- Optimized performance
- Minimal logging
- Recommended for end-user testing
---
## Testing Checklist
Before sharing a module package:
- [ ] ModuleLoader.exe is included
- [ ] Module DLL is included (matching architecture)
- [ ] Sample settings.json is included
- [ ] README/instructions are included
- [ ] Tested on a clean machine (no PowerToys installed)
- [ ] Verified hotkeys work
- [ ] Verified Ctrl+C exits cleanly
- [ ] Confirmed settings path in documentation
---
## Advanced: Portable Package Script
Here's a complete PowerShell script to create a fully portable package:
```powershell
param(
[Parameter(Mandatory=$true)]
[string]$ModuleName,
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Debug",
[ValidateSet("x64", "ARM64")]
[string]$Platform = "x64"
)
$packageName = "ModuleLoader-$ModuleName-$Platform-$Configuration"
$packagePath = ".\$packageName"
Write-Host "Creating portable package: $packageName" -ForegroundColor Green
# Create structure
New-Item $packagePath -ItemType Directory -Force | Out-Null
New-Item "$packagePath\modules" -ItemType Directory -Force | Out-Null
New-Item "$packagePath\settings\$ModuleName" -ItemType Directory -Force | Out-Null
# Copy ModuleLoader
$loaderPath = "$Platform\$Configuration\ModuleLoader.exe"
if (Test-Path $loaderPath) {
Copy-Item $loaderPath "$packagePath\"
Write-Host "? Copied ModuleLoader.exe" -ForegroundColor Green
} else {
Write-Host "? ModuleLoader.exe not found at $loaderPath" -ForegroundColor Red
exit 1
}
# Copy Module DLL
$modulePath = "$Platform\$Configuration\$ModuleName.dll"
if (Test-Path $modulePath) {
Copy-Item $modulePath "$packagePath\modules\"
Write-Host "? Copied $ModuleName.dll" -ForegroundColor Green
} else {
Write-Host "? $ModuleName.dll not found at $modulePath" -ForegroundColor Red
exit 1
}
# Copy Settings
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleName\settings.json"
if (Test-Path $settingsPath) {
Copy-Item $settingsPath "$packagePath\settings\$ModuleName\"
Write-Host "? Copied settings.json" -ForegroundColor Green
} else {
Write-Host "? Settings not found at $settingsPath - creating placeholder" -ForegroundColor Yellow
@"
{
"name": "$ModuleName",
"version": "1.0"
}
"@ | Out-File "$packagePath\settings\$ModuleName\settings.json"
}
# Create README
@"
PowerToys $ModuleName Testing Package
======================================
Configuration: $Configuration
Platform: $Platform
Setup Instructions:
-------------------
1. Copy the 'settings\$ModuleName' folder to:
%LOCALAPPDATA%\Microsoft\PowerToys\
2. Run:
ModuleLoader.exe modules\$ModuleName.dll
3. Press Ctrl+C to exit
Logs are written to:
%LOCALAPPDATA%\Microsoft\PowerToys\$ModuleName\Logs\
For more information, see:
https://github.com/microsoft/PowerToys/tree/main/tools/module_loader
"@ | Out-File "$packagePath\README.txt"
# Create ZIP
$zipPath = "$packageName.zip"
Compress-Archive -Path $packagePath -DestinationPath $zipPath -Force
Write-Host "? Created $zipPath" -ForegroundColor Green
# Show summary
Write-Host "`nPackage Contents:" -ForegroundColor Cyan
Get-ChildItem $packagePath -Recurse | ForEach-Object {
Write-Host " $($_.FullName.Replace($packagePath, ''))"
}
Write-Host "`nPackage ready: $zipPath" -ForegroundColor Green
Write-Host "Size: $([math]::Round((Get-Item $zipPath).Length / 1KB, 2)) KB"
```
**Usage**:
```powershell
.\CreateModulePackage.ps1 -ModuleName "CursorWrap" -Configuration Release -Platform x64
```
---
## FAQ
### Q: Can I share just ModuleLoader.exe and the module DLL?
**A**: Yes, but the recipient must have PowerToys installed (or manually create the settings file).
### Q: Does the tester need PowerToys installed?
**A**: No, if you provide the complete package with settings. ModuleLoader is fully standalone.
### Q: What if settings.json doesn't exist?
**A**: ModuleLoader will show an error. Either:
1. Run PowerToys once with the module enabled to generate settings
2. Manually create a minimal settings.json file
3. Include a sample settings.json in your package
### Q: Can I test modules on a virtual machine?
**A**: Yes! This is a great use case. Just copy the package to the VM - no PowerToys installation needed.
### Q: Do I need to include PDB files?
**A**: Only for debugging. For normal testing, just the EXE and DLL are sufficient.
### Q: Can I distribute this to end users?
**A**: ModuleLoader is a **development/testing tool**, not intended for end-user distribution. For production use, direct users to install PowerToys.
---
## Security Considerations
When sharing module DLLs:
1. **Verify Source**: Only share modules you built from trusted source code
2. **Scan for Malware**: Run antivirus scans on the package before sharing
3. **HTTPS Only**: Use secure channels (HTTPS, OneDrive, SharePoint) for distribution
4. **Hash Verification**: Consider providing SHA256 hashes for file integrity:
```powershell
Get-FileHash ModuleLoader.exe -Algorithm SHA256
Get-FileHash modules\CursorWrap.dll -Algorithm SHA256
```
---
## Example Package (CursorWrap)
Here's what a complete CursorWrap testing package looks like:
```
ModuleLoader-CursorWrap-x64-Debug.zip (220 KB)
?
??? ModuleLoader-CursorWrap-x64-Debug\
??? ModuleLoader.exe (160 KB)
??? README.txt (2 KB)
??? modules\
? ??? CursorWrap.dll (55 KB)
??? settings\
??? CursorWrap\
??? settings.json (3 KB)
```
**Total package size**: ~220 KB (compressed)
---
## Support
For issues with ModuleLoader, see:
- [ModuleLoader README](./README.md)
- [PowerToys Documentation](https://aka.ms/PowerToysOverview)
- [PowerToys GitHub Issues](https://github.com/microsoft/PowerToys/issues)
---
## License
ModuleLoader is part of PowerToys and is licensed under the MIT License.
See the LICENSE file in the PowerToys repository root for details.

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "ConsoleHost.h"
#include <iostream>
bool ConsoleHost::s_exitRequested = false;
ConsoleHost::ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager)
: m_moduleLoader(moduleLoader)
, m_hotkeyManager(hotkeyManager)
{
}
ConsoleHost::~ConsoleHost()
{
}
BOOL WINAPI ConsoleHost::ConsoleCtrlHandler(DWORD ctrlType)
{
switch (ctrlType)
{
case CTRL_C_EVENT:
case CTRL_BREAK_EVENT:
case CTRL_CLOSE_EVENT:
std::wcout << L"\nCtrl+C received, shutting down...\n";
s_exitRequested = true;
// Post a quit message to break the message loop
PostQuitMessage(0);
return TRUE;
default:
return FALSE;
}
}
void ConsoleHost::Run()
{
// Install console control handler
if (!SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE))
{
std::wcerr << L"Warning: Failed to set console control handler\n";
}
s_exitRequested = false;
// Message loop
MSG msg;
while (!s_exitRequested)
{
// Wait for a message with a timeout so we can check s_exitRequested
DWORD result = MsgWaitForMultipleObjects(0, nullptr, FALSE, 100, QS_ALLINPUT);
if (result == WAIT_OBJECT_0)
{
// Process all pending messages
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
s_exitRequested = true;
break;
}
if (msg.message == WM_HOTKEY)
{
m_hotkeyManager.HandleHotkey(static_cast<int>(msg.wParam), m_moduleLoader);
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
// Remove console control handler
SetConsoleCtrlHandler(ConsoleCtrlHandler, FALSE);
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <Windows.h>
#include "ModuleLoader.h"
#include "HotkeyManager.h"
/// <summary>
/// Console host that runs the message loop and handles Ctrl+C
/// </summary>
class ConsoleHost
{
public:
ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager);
~ConsoleHost();
// Prevent copying
ConsoleHost(const ConsoleHost&) = delete;
ConsoleHost& operator=(const ConsoleHost&) = delete;
/// <summary>
/// Run the message loop until Ctrl+C is pressed
/// </summary>
void Run();
private:
ModuleLoader& m_moduleLoader;
HotkeyManager& m_hotkeyManager;
static bool s_exitRequested;
/// <summary>
/// Console control handler (for Ctrl+C)
/// </summary>
static BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType);
};

View File

@@ -0,0 +1,279 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "HotkeyManager.h"
#include <iostream>
#include <sstream>
HotkeyManager::HotkeyManager()
: m_nextHotkeyId(1) // Start from 1
, m_hotkeyExRegistered(false)
, m_hotkeyExId(0)
{
}
HotkeyManager::~HotkeyManager()
{
UnregisterAll();
}
UINT HotkeyManager::ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const
{
UINT modifiers = MOD_NOREPEAT; // Prevent repeat events
if (win) modifiers |= MOD_WIN;
if (ctrl) modifiers |= MOD_CONTROL;
if (alt) modifiers |= MOD_ALT;
if (shift) modifiers |= MOD_SHIFT;
return modifiers;
}
bool HotkeyManager::RegisterModuleHotkeys(ModuleLoader& moduleLoader)
{
if (!moduleLoader.IsLoaded())
{
std::wcerr << L"Error: Module not loaded\n";
return false;
}
bool anyRegistered = false;
// First, try the newer GetHotkeyEx() API
auto hotkeyEx = moduleLoader.GetHotkeyEx();
if (hotkeyEx.has_value())
{
std::wcout << L"Module has HotkeyEx activation hotkey\n";
UINT modifiers = hotkeyEx->modifiersMask | MOD_NOREPEAT;
UINT vkCode = hotkeyEx->vkCode;
if (vkCode != 0)
{
int hotkeyId = m_nextHotkeyId++;
std::wcout << L" Registering HotkeyEx: ";
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
{
m_hotkeyExRegistered = true;
m_hotkeyExId = hotkeyId;
std::wcout << L" - OK (Activation/Toggle)\n";
anyRegistered = true;
}
else
{
DWORD error = GetLastError();
std::wcout << L" - FAILED (Error: " << error << L")\n";
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
{
std::wcout << L" (Hotkey is already registered by another application)\n";
}
}
}
}
// Also check the legacy get_hotkeys() API
size_t hotkeyCount = moduleLoader.GetHotkeys(nullptr, 0);
if (hotkeyCount > 0)
{
std::wcout << L"Module reports " << hotkeyCount << L" legacy hotkey(s)\n";
// Allocate buffer and get the hotkeys
std::vector<PowertoyModuleIface::Hotkey> hotkeys(hotkeyCount);
size_t actualCount = moduleLoader.GetHotkeys(hotkeys.data(), hotkeyCount);
// Register each hotkey
for (size_t i = 0; i < actualCount; i++)
{
const auto& hotkey = hotkeys[i];
UINT modifiers = ConvertModifiers(hotkey.win, hotkey.ctrl, hotkey.alt, hotkey.shift);
UINT vkCode = hotkey.key;
if (vkCode == 0)
{
std::wcout << L" Skipping hotkey " << i << L" (no key code)\n";
continue;
}
int hotkeyId = m_nextHotkeyId++;
std::wcout << L" Registering hotkey " << i << L": ";
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
{
HotkeyInfo info;
info.id = hotkeyId;
info.moduleHotkeyId = i;
info.modifiers = modifiers;
info.vkCode = vkCode;
info.description = ModifiersToString(modifiers) + L"+" + VKeyToString(vkCode);
m_registeredHotkeys.push_back(info);
std::wcout << L" - OK\n";
anyRegistered = true;
}
else
{
DWORD error = GetLastError();
std::wcout << L" - FAILED (Error: " << error << L")\n";
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
{
std::wcout << L" (Hotkey is already registered by another application)\n";
}
}
}
}
if (!anyRegistered && hotkeyCount == 0 && !hotkeyEx.has_value())
{
std::wcout << L"Module has no hotkeys\n";
}
return anyRegistered;
}
void HotkeyManager::UnregisterAll()
{
for (const auto& hotkey : m_registeredHotkeys)
{
UnregisterHotKey(nullptr, hotkey.id);
}
m_registeredHotkeys.clear();
if (m_hotkeyExRegistered)
{
UnregisterHotKey(nullptr, m_hotkeyExId);
m_hotkeyExRegistered = false;
m_hotkeyExId = 0;
}
}
bool HotkeyManager::HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader)
{
// Check if it's the HotkeyEx activation hotkey
if (m_hotkeyExRegistered && hotkeyId == m_hotkeyExId)
{
std::wcout << L"\nActivation hotkey triggered (HotkeyEx)\n";
moduleLoader.OnHotkeyEx();
std::wcout << L"Module toggled via activation hotkey\n";
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
return true;
}
// Check legacy hotkeys
for (const auto& hotkey : m_registeredHotkeys)
{
if (hotkey.id == hotkeyId)
{
std::wcout << L"\nHotkey triggered: " << hotkey.description << L"\n";
bool result = moduleLoader.OnHotkey(hotkey.moduleHotkeyId);
std::wcout << L"Module handled hotkey: " << (result ? L"Swallowed" : L"Not swallowed") << L"\n";
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
return true;
}
}
return false;
}
void HotkeyManager::PrintHotkeys() const
{
for (const auto& hotkey : m_registeredHotkeys)
{
std::wcout << L" " << hotkey.description << L"\n";
}
}
std::wstring HotkeyManager::ModifiersToString(UINT modifiers) const
{
std::wstringstream ss;
bool first = true;
if (modifiers & MOD_WIN)
{
if (!first) ss << L"+";
ss << L"Win";
first = false;
}
if (modifiers & MOD_CONTROL)
{
if (!first) ss << L"+";
ss << L"Ctrl";
first = false;
}
if (modifiers & MOD_ALT)
{
if (!first) ss << L"+";
ss << L"Alt";
first = false;
}
if (modifiers & MOD_SHIFT)
{
if (!first) ss << L"+";
ss << L"Shift";
first = false;
}
return ss.str();
}
std::wstring HotkeyManager::VKeyToString(UINT vkCode) const
{
// Handle special keys
switch (vkCode)
{
case VK_SPACE: return L"Space";
case VK_RETURN: return L"Enter";
case VK_ESCAPE: return L"Esc";
case VK_TAB: return L"Tab";
case VK_BACK: return L"Backspace";
case VK_DELETE: return L"Del";
case VK_INSERT: return L"Ins";
case VK_HOME: return L"Home";
case VK_END: return L"End";
case VK_PRIOR: return L"PgUp";
case VK_NEXT: return L"PgDn";
case VK_LEFT: return L"Left";
case VK_RIGHT: return L"Right";
case VK_UP: return L"Up";
case VK_DOWN: return L"Down";
case VK_F1: return L"F1";
case VK_F2: return L"F2";
case VK_F3: return L"F3";
case VK_F4: return L"F4";
case VK_F5: return L"F5";
case VK_F6: return L"F6";
case VK_F7: return L"F7";
case VK_F8: return L"F8";
case VK_F9: return L"F9";
case VK_F10: return L"F10";
case VK_F11: return L"F11";
case VK_F12: return L"F12";
}
// For alphanumeric keys, use MapVirtualKey
wchar_t keyName[256];
UINT scanCode = MapVirtualKeyW(vkCode, MAPVK_VK_TO_VSC);
if (GetKeyNameTextW(scanCode << 16, keyName, 256) > 0)
{
return keyName;
}
// Fallback to hex code
std::wstringstream ss;
ss << L"0x" << std::hex << vkCode;
return ss.str();
}

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <Windows.h>
#include <string>
#include <vector>
#include <map>
#include "ModuleLoader.h"
/// <summary>
/// Manages hotkey registration using RegisterHotKey API
/// </summary>
class HotkeyManager
{
public:
HotkeyManager();
~HotkeyManager();
// Prevent copying
HotkeyManager(const HotkeyManager&) = delete;
HotkeyManager& operator=(const HotkeyManager&) = delete;
/// <summary>
/// Register all hotkeys from a module
/// </summary>
/// <param name="moduleLoader">Module to get hotkeys from</param>
/// <returns>True if at least one hotkey was registered</returns>
bool RegisterModuleHotkeys(ModuleLoader& moduleLoader);
/// <summary>
/// Unregister all hotkeys
/// </summary>
void UnregisterAll();
/// <summary>
/// Handle a WM_HOTKEY message
/// </summary>
/// <param name="hotkeyId">ID from the WM_HOTKEY message</param>
/// <param name="moduleLoader">Module to trigger the hotkey on</param>
/// <returns>True if the hotkey was handled</returns>
bool HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader);
/// <summary>
/// Get the number of registered hotkeys
/// </summary>
/// <returns>Number of registered hotkeys</returns>
size_t GetRegisteredCount() const { return m_registeredHotkeys.size() + (m_hotkeyExRegistered ? 1 : 0); }
/// <summary>
/// Print registered hotkeys to console
/// </summary>
void PrintHotkeys() const;
private:
struct HotkeyInfo
{
int id = 0;
size_t moduleHotkeyId = 0;
UINT modifiers = 0;
UINT vkCode = 0;
std::wstring description;
};
std::vector<HotkeyInfo> m_registeredHotkeys;
int m_nextHotkeyId;
bool m_hotkeyExRegistered;
int m_hotkeyExId;
/// <summary>
/// Convert modifier bools to RegisterHotKey modifiers
/// </summary>
UINT ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const;
/// <summary>
/// Get a string representation of modifiers
/// </summary>
std::wstring ModifiersToString(UINT modifiers) const;
/// <summary>
/// Get a string representation of a virtual key code
/// </summary>
std::wstring VKeyToString(UINT vkCode) const;
};

View File

@@ -0,0 +1,183 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "ModuleLoader.h"
#include <iostream>
#include <stdexcept>
ModuleLoader::ModuleLoader()
: m_hModule(nullptr)
, m_module(nullptr)
{
}
ModuleLoader::~ModuleLoader()
{
if (m_module)
{
try
{
m_module->destroy();
}
catch (...)
{
// Ignore exceptions during cleanup
}
m_module = nullptr;
}
if (m_hModule)
{
FreeLibrary(m_hModule);
m_hModule = nullptr;
}
}
bool ModuleLoader::Load(const std::wstring& dllPath)
{
if (m_hModule || m_module)
{
std::wcerr << L"Error: Module already loaded\n";
return false;
}
m_dllPath = dllPath;
// Load the DLL
m_hModule = LoadLibraryW(dllPath.c_str());
if (!m_hModule)
{
DWORD error = GetLastError();
std::wcerr << L"Error: Failed to load DLL. Error code: " << error << L"\n";
return false;
}
// Get the powertoy_create function
using powertoy_create_func = PowertoyModuleIface* (*)();
auto create_func = reinterpret_cast<powertoy_create_func>(
GetProcAddress(m_hModule, "powertoy_create"));
if (!create_func)
{
std::wcerr << L"Error: DLL does not export 'powertoy_create' function\n";
FreeLibrary(m_hModule);
m_hModule = nullptr;
return false;
}
// Create the module instance
m_module = create_func();
if (!m_module)
{
std::wcerr << L"Error: powertoy_create() returned nullptr\n";
FreeLibrary(m_hModule);
m_hModule = nullptr;
return false;
}
std::wcout << L"Module instance created successfully\n";
return true;
}
void ModuleLoader::Enable()
{
if (!m_module)
{
throw std::runtime_error("Module not loaded");
}
m_module->enable();
}
void ModuleLoader::Disable()
{
if (!m_module)
{
return;
}
m_module->disable();
}
bool ModuleLoader::IsEnabled() const
{
if (!m_module)
{
return false;
}
return m_module->is_enabled();
}
void ModuleLoader::SetConfig(const std::wstring& configJson)
{
if (!m_module)
{
throw std::runtime_error("Module not loaded");
}
m_module->set_config(configJson.c_str());
}
std::wstring ModuleLoader::GetModuleName() const
{
if (!m_module)
{
return L"<not loaded>";
}
const wchar_t* name = m_module->get_name();
return name ? name : L"<unknown>";
}
std::wstring ModuleLoader::GetModuleKey() const
{
if (!m_module)
{
return L"<not loaded>";
}
const wchar_t* key = m_module->get_key();
return key ? key : L"<unknown>";
}
size_t ModuleLoader::GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize)
{
if (!m_module)
{
return 0;
}
return m_module->get_hotkeys(buffer, bufferSize);
}
bool ModuleLoader::OnHotkey(size_t hotkeyId)
{
if (!m_module)
{
return false;
}
return m_module->on_hotkey(hotkeyId);
}
std::optional<PowertoyModuleIface::HotkeyEx> ModuleLoader::GetHotkeyEx()
{
if (!m_module)
{
return std::nullopt;
}
return m_module->GetHotkeyEx();
}
void ModuleLoader::OnHotkeyEx()
{
if (!m_module)
{
return;
}
m_module->OnHotkeyEx();
}

View File

@@ -0,0 +1,102 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <Windows.h>
#include <string>
#include <vector>
#include <powertoy_module_interface.h>
/// <summary>
/// Wrapper class for loading and managing a PowerToy module DLL
/// </summary>
class ModuleLoader
{
public:
ModuleLoader();
~ModuleLoader();
// Prevent copying
ModuleLoader(const ModuleLoader&) = delete;
ModuleLoader& operator=(const ModuleLoader&) = delete;
/// <summary>
/// Load a PowerToy module DLL
/// </summary>
/// <param name="dllPath">Path to the module DLL</param>
/// <returns>True if successful, false otherwise</returns>
bool Load(const std::wstring& dllPath);
/// <summary>
/// Enable the loaded module
/// </summary>
void Enable();
/// <summary>
/// Disable the loaded module
/// </summary>
void Disable();
/// <summary>
/// Check if the module is enabled
/// </summary>
/// <returns>True if enabled, false otherwise</returns>
bool IsEnabled() const;
/// <summary>
/// Set configuration for the module
/// </summary>
/// <param name="configJson">JSON configuration string</param>
void SetConfig(const std::wstring& configJson);
/// <summary>
/// Get the module's localized name
/// </summary>
/// <returns>Module name</returns>
std::wstring GetModuleName() const;
/// <summary>
/// Get the module's non-localized key
/// </summary>
/// <returns>Module key</returns>
std::wstring GetModuleKey() const;
/// <summary>
/// Get the module's hotkeys
/// </summary>
/// <param name="buffer">Buffer to store hotkeys</param>
/// <param name="bufferSize">Size of the buffer</param>
/// <returns>Number of hotkeys returned</returns>
size_t GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize);
/// <summary>
/// Trigger a hotkey callback on the module
/// </summary>
/// <param name="hotkeyId">ID of the hotkey to trigger</param>
/// <returns>True if the key press should be swallowed</returns>
bool OnHotkey(size_t hotkeyId);
/// <summary>
/// Check if the module is loaded
/// </summary>
/// <returns>True if loaded, false otherwise</returns>
bool IsLoaded() const { return m_module != nullptr; }
/// <summary>
/// Get the module's activation hotkey (newer HotkeyEx API)
/// </summary>
/// <returns>Optional HotkeyEx struct</returns>
std::optional<PowertoyModuleIface::HotkeyEx> GetHotkeyEx();
/// <summary>
/// Trigger the newer-style hotkey callback on the module
/// </summary>
void OnHotkeyEx();
private:
HMODULE m_hModule;
PowertoyModuleIface* m_module;
std::wstring m_dllPath;
};

View File

@@ -0,0 +1,182 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "SettingsLoader.h"
#include <iostream>
#include <fstream>
#include <sstream>
#include <filesystem>
#include <Shlobj.h>
SettingsLoader::SettingsLoader()
{
}
SettingsLoader::~SettingsLoader()
{
}
std::wstring SettingsLoader::GetPowerToysSettingsRoot() const
{
// Get %LOCALAPPDATA%
PWSTR localAppDataPath = nullptr;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &localAppDataPath);
if (FAILED(hr) || !localAppDataPath)
{
std::wcerr << L"Error: Failed to get LOCALAPPDATA path\n";
return L"";
}
std::wstring result(localAppDataPath);
CoTaskMemFree(localAppDataPath);
// Append PowerToys directory
result += L"\\Microsoft\\PowerToys";
return result;
}
std::wstring SettingsLoader::GetSettingsPath(const std::wstring& moduleName) const
{
std::wstring root = GetPowerToysSettingsRoot();
if (root.empty())
{
return L"";
}
// Construct path: %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
std::wstring settingsPath = root + L"\\" + moduleName + L"\\settings.json";
return settingsPath;
}
std::wstring SettingsLoader::ReadFileContents(const std::wstring& filePath) const
{
std::wifstream file(filePath, std::ios::binary);
if (!file.is_open())
{
std::wcerr << L"Error: Could not open file: " << filePath << L"\n";
return L"";
}
// Read the entire file
std::wstringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
std::wstring SettingsLoader::LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath)
{
const std::wstring powerToysPrefix = L"PowerToys.";
// Build list of possible module name variations to try
std::vector<std::wstring> moduleNameVariants;
// Try exact name first
moduleNameVariants.push_back(moduleName);
// If doesn't start with "PowerToys.", try adding it
if (moduleName.find(powerToysPrefix) != 0)
{
moduleNameVariants.push_back(powerToysPrefix + moduleName);
}
// If starts with "PowerToys.", try without it
else
{
moduleNameVariants.push_back(moduleName.substr(powerToysPrefix.length()));
}
// FIRST: Try same directory as the module DLL
if (!moduleDllPath.empty())
{
std::filesystem::path dllPath(moduleDllPath);
std::filesystem::path dllDirectory = dllPath.parent_path();
std::wstring localSettingsPath = (dllDirectory / L"settings.json").wstring();
std::wcout << L"Trying settings path (module directory): " << localSettingsPath << L"\n";
if (std::filesystem::exists(localSettingsPath))
{
std::wstring contents = ReadFileContents(localSettingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded from module directory (" << contents.size() << L" characters)\n";
return contents;
}
}
}
// SECOND: Try standard PowerToys settings locations
for (const auto& variant : moduleNameVariants)
{
std::wstring settingsPath = GetSettingsPath(variant);
std::wcout << L"Trying settings path: " << settingsPath << L"\n";
// Check if file exists (case-sensitive path)
if (std::filesystem::exists(settingsPath))
{
std::wstring contents = ReadFileContents(settingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
return contents;
}
}
else
{
// Try case-insensitive search in the parent directory
std::wstring root = GetPowerToysSettingsRoot();
if (!root.empty() && std::filesystem::exists(root))
{
try
{
// Search for a directory that matches case-insensitively
for (const auto& entry : std::filesystem::directory_iterator(root))
{
if (entry.is_directory())
{
std::wstring dirName = entry.path().filename().wstring();
// Case-insensitive comparison
if (_wcsicmp(dirName.c_str(), variant.c_str()) == 0)
{
std::wstring actualSettingsPath = entry.path().wstring() + L"\\settings.json";
std::wcout << L"Found case-insensitive match: " << actualSettingsPath << L"\n";
if (std::filesystem::exists(actualSettingsPath))
{
std::wstring contents = ReadFileContents(actualSettingsPath);
if (!contents.empty())
{
std::wcout << L"Settings file loaded (" << contents.size() << L" characters)\n";
return contents;
}
}
}
}
}
}
catch (const std::filesystem::filesystem_error& e)
{
std::wcerr << L"Error searching directory: " << e.what() << L"\n";
}
}
}
}
std::wcerr << L"Error: Settings file not found in any expected location:\n";
if (!moduleDllPath.empty())
{
std::filesystem::path dllPath(moduleDllPath);
std::filesystem::path dllDirectory = dllPath.parent_path();
std::wcerr << L" - " << (dllDirectory / L"settings.json").wstring() << L" (module directory)\n";
}
for (const auto& variant : moduleNameVariants)
{
std::wcerr << L" - " << GetSettingsPath(variant) << L"\n";
}
return L"";
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#pragma once
#include <Windows.h>
#include <string>
/// <summary>
/// Utility class for discovering and loading PowerToy module settings
/// </summary>
class SettingsLoader
{
public:
SettingsLoader();
~SettingsLoader();
/// <summary>
/// Load settings for a PowerToy module
/// </summary>
/// <param name="moduleName">Name of the module (e.g., "CursorWrap")</param>
/// <param name="moduleDllPath">Full path to the module DLL (for checking local settings.json)</param>
/// <returns>JSON settings string, or empty string if not found</returns>
std::wstring LoadSettings(const std::wstring& moduleName, const std::wstring& moduleDllPath);
/// <summary>
/// Get the settings file path for a module
/// </summary>
/// <param name="moduleName">Name of the module</param>
/// <returns>Full path to the settings.json file</returns>
std::wstring GetSettingsPath(const std::wstring& moduleName) const;
private:
/// <summary>
/// Get the PowerToys root settings directory
/// </summary>
/// <returns>Path to %LOCALAPPDATA%\Microsoft\PowerToys</returns>
std::wstring GetPowerToysSettingsRoot() const;
/// <summary>
/// Read a text file into a string
/// </summary>
/// <param name="filePath">Path to the file</param>
/// <returns>File contents as a string</returns>
std::wstring ReadFileContents(const std::wstring& filePath) const;
};

View File

@@ -0,0 +1,244 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include <Windows.h>
#include <Tlhelp32.h>
#include <iostream>
#include <string>
#include <filesystem>
#include "ModuleLoader.h"
#include "SettingsLoader.h"
#include "HotkeyManager.h"
#include "ConsoleHost.h"
namespace
{
void PrintUsage()
{
std::wcout << L"PowerToys Module Loader - Standalone utility for loading and testing PowerToy modules\n\n";
std::wcout << L"Usage: ModuleLoader.exe <module_dll_path>\n\n";
std::wcout << L"Arguments:\n";
std::wcout << L" module_dll_path Path to the PowerToy module DLL (e.g., CursorWrap.dll)\n\n";
std::wcout << L"Behavior:\n";
std::wcout << L" - Automatically discovers settings from %%LOCALAPPDATA%%\\Microsoft\\PowerToys\\<ModuleName>\\settings.json\n";
std::wcout << L" - Loads and enables the module\n";
std::wcout << L" - Registers module hotkeys\n";
std::wcout << L" - Runs until Ctrl+C is pressed\n\n";
std::wcout << L"Examples:\n";
std::wcout << L" ModuleLoader.exe x64\\Debug\\modules\\CursorWrap.dll\n";
std::wcout << L" ModuleLoader.exe \"C:\\Program Files\\PowerToys\\modules\\MouseHighlighter.dll\"\n\n";
std::wcout << L"Notes:\n";
std::wcout << L" - Only non-UI modules are supported\n";
std::wcout << L" - Module must have a valid settings.json file\n";
std::wcout << L" - Debug output is written to module's log directory\n";
}
std::wstring ExtractModuleName(const std::wstring& dllPath)
{
std::filesystem::path path(dllPath);
std::wstring filename = path.stem().wstring();
// Remove "PowerToys." prefix if present (case-insensitive)
const std::wstring powerToysPrefix = L"PowerToys.";
if (filename.length() >= powerToysPrefix.length())
{
// Check if filename starts with "PowerToys." (case-insensitive)
if (_wcsnicmp(filename.c_str(), powerToysPrefix.c_str(), powerToysPrefix.length()) == 0)
{
filename = filename.substr(powerToysPrefix.length());
}
}
// Common PowerToys module naming patterns
// Remove common suffixes if present
const std::wstring suffixes[] = { L"Module", L"ModuleInterface", L"Interface" };
for (const auto& suffix : suffixes)
{
if (filename.size() > suffix.size())
{
size_t pos = filename.rfind(suffix);
if (pos != std::wstring::npos && pos + suffix.size() == filename.size())
{
filename = filename.substr(0, pos);
break;
}
}
}
return filename;
}
}
int wmain(int argc, wchar_t* argv[])
{
std::wcout << L"PowerToys Module Loader v1.0\n";
std::wcout << L"=============================\n\n";
// Check if PowerToys.exe is running
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
bool powerToysRunning = false;
if (Process32FirstW(hSnapshot, &pe32))
{
do
{
if (_wcsicmp(pe32.szExeFile, L"PowerToys.exe") == 0)
{
powerToysRunning = true;
break;
}
} while (Process32NextW(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
if (powerToysRunning)
{
// Display warning with VT100 colors
// Yellow background (43m), black text (30m), bold (1m)
std::wcout << L"\033[1;43;30m WARNING \033[0m PowerToys.exe is currently running!\n\n";
// Red text for important message
std::wcout << L"\033[1;31m";
std::wcout << L"Running ModuleLoader while PowerToys is active may cause conflicts:\n";
std::wcout << L" - Duplicate hotkey registrations\n";
std::wcout << L" - Conflicting module instances\n";
std::wcout << L" - Unexpected behavior\n";
std::wcout << L"\033[0m\n"; // Reset color
// Cyan text for recommendation
std::wcout << L"\033[1;36m";
std::wcout << L"RECOMMENDATION: Exit PowerToys before continuing.\n";
std::wcout << L"\033[0m\n"; // Reset color
// Yellow text for prompt
std::wcout << L"\033[1;33m";
std::wcout << L"Do you want to continue anyway? (y/N): ";
std::wcout << L"\033[0m"; // Reset color
wchar_t response = L'\0';
std::wcin >> response;
if (response != L'y' && response != L'Y')
{
std::wcout << L"\nExiting. Please close PowerToys and try again.\n";
return 1;
}
std::wcout << L"\n";
}
}
// Parse command-line arguments
if (argc < 2)
{
std::wcerr << L"Error: Missing required argument <module_dll_path>\n\n";
PrintUsage();
return 1;
}
const std::wstring dllPath = argv[1];
// Validate DLL exists
if (!std::filesystem::exists(dllPath))
{
std::wcerr << L"Error: Module DLL not found: " << dllPath << L"\n";
return 1;
}
std::wcout << L"Loading module: " << dllPath << L"\n";
// Extract module name from DLL path
std::wstring moduleName = ExtractModuleName(dllPath);
std::wcout << L"Detected module name: " << moduleName << L"\n\n";
try
{
// Load settings for the module
std::wcout << L"Loading settings...\n";
SettingsLoader settingsLoader;
std::wstring settingsJson = settingsLoader.LoadSettings(moduleName, dllPath);
if (settingsJson.empty())
{
std::wcerr << L"Error: Could not load settings for module '" << moduleName << L"'\n";
std::wcerr << L"Expected location: %LOCALAPPDATA%\\Microsoft\\PowerToys\\" << moduleName << L"\\settings.json\n";
return 1;
}
std::wcout << L"Settings loaded successfully.\n\n";
// Load the module DLL
std::wcout << L"Loading module DLL...\n";
ModuleLoader moduleLoader;
if (!moduleLoader.Load(dllPath))
{
std::wcerr << L"Error: Failed to load module DLL\n";
return 1;
}
std::wcout << L"Module DLL loaded successfully.\n";
std::wcout << L"Module key: " << moduleLoader.GetModuleKey() << L"\n";
std::wcout << L"Module name: " << moduleLoader.GetModuleName() << L"\n\n";
// Apply settings to the module
std::wcout << L"Applying settings to module...\n";
moduleLoader.SetConfig(settingsJson);
std::wcout << L"Settings applied.\n\n";
// Register hotkeys
std::wcout << L"Registering module hotkeys...\n";
HotkeyManager hotkeyManager;
if (!hotkeyManager.RegisterModuleHotkeys(moduleLoader))
{
std::wcerr << L"Warning: Failed to register some hotkeys\n";
}
std::wcout << L"Hotkeys registered: " << hotkeyManager.GetRegisteredCount() << L"\n\n";
// Enable the module
std::wcout << L"Enabling module...\n";
moduleLoader.Enable();
std::wcout << L"Module enabled.\n\n";
// Display status
std::wcout << L"=============================\n";
std::wcout << L"Module is now running!\n";
std::wcout << L"=============================\n\n";
std::wcout << L"Module Status:\n";
std::wcout << L" - Name: " << moduleLoader.GetModuleName() << L"\n";
std::wcout << L" - Key: " << moduleLoader.GetModuleKey() << L"\n";
std::wcout << L" - Enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n";
std::wcout << L" - Hotkeys: " << hotkeyManager.GetRegisteredCount() << L" registered\n\n";
if (hotkeyManager.GetRegisteredCount() > 0)
{
std::wcout << L"Registered Hotkeys:\n";
hotkeyManager.PrintHotkeys();
std::wcout << L"\n";
}
std::wcout << L"Press Ctrl+C to exit.\n";
std::wcout << L"You can press the module's hotkey to toggle its functionality.\n\n";
// Run the message loop
ConsoleHost consoleHost(moduleLoader, hotkeyManager);
consoleHost.Run();
// Cleanup
std::wcout << L"\nShutting down...\n";
moduleLoader.Disable();
hotkeyManager.UnregisterAll();
std::wcout << L"Module unloaded successfully.\n";
return 0;
}
catch (const std::exception& ex)
{
std::wcerr << L"Fatal error: " << ex.what() << L"\n";
return 1;
}
}