Compare commits

...

15 Commits

Author SHA1 Message Date
Leilei Zhang
1a0cc02909 add log 2025-12-08 18:33:57 +08:00
Leilei Zhang
1e21254c6a use cswin32 native 2025-12-08 18:26:46 +08:00
Leilei Zhang
35242575f5 add reference 2025-12-08 16:35:12 +08:00
Leilei Zhang
29206c7057 remvoe aot 2025-12-08 15:48:03 +08:00
Leilei Zhang
f2c41ca13f remove publishaot 2025-12-08 15:17:34 +08:00
Leilei Zhang
3a5c58c963 update cache 2025-12-08 14:27:07 +08:00
Leilei Zhang
8da2b02413 update file structure 2025-12-05 13:35:50 +08:00
Leilei Zhang
b4cdec5e5d use common 2025-12-05 13:12:43 +08:00
Leilei Zhang
d987d37fff fix conflict and spelling check 2025-12-05 10:41:42 +08:00
Leilei Zhang
0e93343cba Merge branch 'main' of https://github.com/microsoft/PowerToys into leilzh/fancyzonescli 2025-12-05 10:23:47 +08:00
Leilei Zhang
153c6c851c add fancyzones cli 2025-12-04 20:06:20 +08:00
Jiří Polášek
52f2561937 CmdPal: Find app for WinGet package (#43943)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

This PR introduces a bit of dark magic to resolve the correct installed
app for a given WinGet package:

- Packaged apps: matched using their package family name.
- Everything else: matched using the product code (GUID) and heuristic
registry lookup.
- The registry rarely stores the executable path directly, so the logic
compares install locations with known apps.
  - It attempts to pick the best candidate while avoiding uninstallers.
  - It’s not science — let’s call it `#666666` magic.
- MSI API support was removed because it's too slow for this scenario.
- If no reliable match is found, the command is skipped for now. The
future plan is to redirect the user to the list of installed apps and
search by display name, but that needs some supporting infrastructure
first.
- The command order for WinGet list entries was updated: **Install /
Uninstall** is now the primary action, ensuring a stable UI since this
command is always available.


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

- [x] Closes: #43671
<!-- - [ ] 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-12-03 10:16:25 -06:00
Dustin L. Howett
dc30f3fd8e build: move main and setup to SLNX (#43478)
Closes #37100

This does not migrate the rest of the solutions (why do we have so
many?)

Not migrated:

- TemplateCmdPalExtension.sln
- FancyZonesEditor.sln
- BugReportTool.sln
- CleanUp_tool.sln
- FancyZones_DrawLayoutTest.sln
- FancyZones_zonable_tester.sln
- FancyZone_HitTest.sln
- MonitorReportTool.sln
- PowerToyTemplate.sln
- StylesReportTool.sln

---------

Co-authored-by: vanzue <vanzue@outlook.com>
2025-12-03 17:59:46 +08:00
Jessica Dene Earley-Cha
8f9a2c32cc add missing powertoys events (#44016)
## Summary of the Pull Request
This added missing telemetry events from modules that were not listed in
DATA_AND_PRIVACY

## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
2025-12-02 09:59:57 -08:00
moooyo
bcd1583bb7 [AOT] Refactor SettingsLib/SettingsUI for Native AOT compatibility (#42644)
<!-- 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

Key Changes:

1. Settings.UI.Library:
- Added SettingsSerializationContext.cs with comprehensive
JsonSerializable attributes for all settings types
- Updated BasePTModuleSettings.ToJsonString() to use AOT-compatible
serialization
- Updated SettingsUtils.GetFile<T>() to use AOT-compatible
deserialization
- Modified all ToString() methods in Properties classes to use
SettingsSerializationContext
- Converted struct fields to properties in SunTimes and
MouseWithoutBordersProperties for serialization compatibility

2. Settings.UI:
- Fixed namespace alias in SourceGenerationContextContext.cs to avoid
conflicts

For any future developers who discover incorrect settings resolution,
please follow up my changes to add your setting type into
JsonSerilizerContext.



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

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

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

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

Co-authored-by: Yu Leng <yuleng@microsoft.com>
2025-12-02 16:31:02 +08:00
92 changed files with 3781 additions and 3644 deletions

View File

@@ -773,6 +773,7 @@ INITGUID
INITTOLOGFONTSTRUCT
INLINEPREFIX
inlines
Inno
INPC
inproc
INPUTHARDWARE
@@ -1480,6 +1481,7 @@ rgh
rgn
rgs
rguid
rhk
RIDEV
RIGHTSCROLLBAR
riid
@@ -1585,6 +1587,7 @@ SHGDNF
SHGFI
SHIL
shinfo
shk
shlwapi
shobjidl
SHORTCUTATLEAST
@@ -1629,6 +1632,7 @@ SKIPOWNPROCESS
sku
SLGP
sln
slnx
SMALLICON
smartphone
smileys
@@ -1847,6 +1851,7 @@ UNCPRIORITY
UNDNAME
UNICODETEXT
unins
Uninstaller
uninstalls
Uniquifies
unitconverter

View File

@@ -60,6 +60,8 @@
"PowerToys.FancyZonesEditorCommon.dll",
"PowerToys.FancyZonesModuleInterface.dll",
"PowerToys.FancyZones.exe",
"FancyZonesCLI.exe",
"FancyZonesCLI.dll",
"PowerToys.GcodePreviewHandler.dll",
"PowerToys.GcodePreviewHandler.exe",

View File

@@ -192,14 +192,14 @@ jobs:
displayName: Verify XAML formatting
- pwsh: |-
& '.pipelines/verifyNugetPackages.ps1' -solution '$(build.sourcesdirectory)\PowerToys.sln'
displayName: Verify Nuget package versions for PowerToys.sln
& '.pipelines/verifyNugetPackages.ps1' -solution '$(build.sourcesdirectory)\PowerToys.slnx'
displayName: Verify Nuget package versions for PowerToys.slnx
- pwsh: |-
& '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\PowerToys.sln'
& '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\PowerToys.slnx'
& '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\tools\BugReportTool\BugReportTool.sln'
& '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\tools\StylesReportTool\StylesReportTool.sln'
& '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\installer\PowerToysSetup.sln'
& '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\installer\PowerToysSetup.slnx'
displayName: Verify ARM64 configurations
- ${{ if eq(parameters.enablePackageCaching, true) }}:
@@ -252,7 +252,7 @@ jobs:
${{ else }}:
displayName: Build PowerToys main project
inputs:
solution: 'PowerToys.sln'
solution: 'PowerToys.slnx'
vsVersion: 17.0
msbuildArgs: >-
-restore -graph
@@ -275,7 +275,7 @@ jobs:
displayName: Generate DSC artifacts for ARM64
condition: and(succeeded(), eq(variables['BuildPlatform'], 'arm64'))
inputs:
solution: PowerToys.sln
solution: PowerToys.slnx
vsVersion: 17.0
msbuildArgs: >-
-restore

View File

@@ -74,7 +74,7 @@ jobs:
command: restore
feedsToUse: config
configPath: nuget.config
restoreSolution: PowerToys.sln
restoreSolution: PowerToys.slnx
restoreDirectory: '$(Build.SourcesDirectory)\packages'
# Build all UI test projects if no specific modules are specified
@@ -129,4 +129,4 @@ jobs:
- publish: $(JobOutputDirectory)
artifact: $(JobOutputArtifactName)
displayName: Publish UI Test artifacts
condition: always()
condition: always()

View File

@@ -35,7 +35,7 @@ steps:
- task: VSBuild@1
displayName: Build Shared Support DLLs
inputs:
solution: "**/installer/PowerToysSetup.sln"
solution: "**/installer/PowerToysSetup.slnx"
vsVersion: 17.0
msbuildArgs: >-
/t:PowerToysSetupCustomActionsVNext;SilentFilesInUseBAFunction
@@ -74,7 +74,7 @@ steps:
- task: VSBuild@1
displayName: 💻 Build VNext MSI
inputs:
solution: "**/installer/PowerToysSetup.sln"
solution: "**/installer/PowerToysSetup.slnx"
vsVersion: 17.0
msbuildArgs: >-
-restore
@@ -91,7 +91,7 @@ steps:
- task: VSBuild@1
displayName: 👤 Build VNext MSI
inputs:
solution: "**/installer/PowerToysSetup.sln"
solution: "**/installer/PowerToysSetup.slnx"
vsVersion: 17.0
msbuildArgs: >-
/t:PowerToysInstallerVNext
@@ -142,7 +142,7 @@ steps:
- task: VSBuild@1
displayName: 💻 Build VNext Bootstrapper
inputs:
solution: "**/installer/PowerToysSetup.sln"
solution: "**/installer/PowerToysSetup.slnx"
vsVersion: 17.0
msbuildArgs: >-
-restore
@@ -159,7 +159,7 @@ steps:
- task: VSBuild@1
displayName: 👤 Build VNext Bootstrapper
inputs:
solution: "**/installer/PowerToysSetup.sln"
solution: "**/installer/PowerToysSetup.slnx"
vsVersion: 17.0
msbuildArgs: >-
/t:PowerToysBootstrapperVNext

View File

@@ -54,4 +54,13 @@ steps:
feedsToUse: 'config'
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
restoreSolution: '$(build.sourcesdirectory)\**\*.sln'
includeNuGetOrg: false
includeNuGetOrg: false
- task: NuGetCommand@2
displayName: 'Restore NuGet packages (slnx)'
inputs:
command: 'restore'
feedsToUse: 'config'
nugetConfigPath: '$(build.sourcesdirectory)\nuget.config'
restoreSolution: '$(build.sourcesdirectory)\**\*.slnx'
includeNuGetOrg: false

View File

@@ -243,6 +243,10 @@ _If you want to find diagnostic data events in the source code, these two links
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdNotFound_EnableCmdNotFound</td>
<td>Triggered when Command Not Found is enabled or disabled.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdNotFoundInstallEvent</td>
<td>Triggered when a Command Not Found is installed.</td>
@@ -257,6 +261,62 @@ _If you want to find diagnostic data events in the source code, these two links
</tr>
</table>
### Command Palette
<table style="width:100%">
<tr>
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_BeginInvoke</td>
<td>Triggered when the Command Palette is launched by the user.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_ColdLaunch</td>
<td>Occurs when Command Palette starts for the first time (cold start).</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_OpenPage</td>
<td>Triggered when a page is opened within the Command Palette, tracking navigation depth.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_OpenUri</td>
<td>Occurs when a URI is opened through the Command Palette, including whether it's a web URL.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_ReactivateInstance</td>
<td>Triggered when an existing Command Palette instance is reactivated.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_RunCommand</td>
<td>Logs when a command is executed through the Command Palette, including admin elevation status.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPal_RunQuery</td>
<td>Triggered when a search query is performed, including result count and duration.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPalDismissedOnEsc</td>
<td>Occurs when the Command Palette is dismissed by pressing the Escape key.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPalDismissedOnLostFocus</td>
<td>Triggered when the Command Palette is dismissed due to losing focus.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPalHotkeySummoned</td>
<td>Logs when the Command Palette is summoned via hotkey, distinguishing between global and context-specific hotkeys.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPalInvokeResult</td>
<td>Records the result type of a Command Palette invocation.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.CmdPalProcessStarted</td>
<td>Triggered when the Command Palette process is started.</td>
</tr>
</table>
### Crop And Lock
<table style="width:100%">
<tr>
@@ -735,6 +795,10 @@ _If you want to find diagnostic data events in the source code, these two links
<th>Event Name</th>
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.NewPlus_ChangedTemplateLocation</td>
<td>Triggered when the template folder location is changed.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.NewPlus_EventCopyTemplate</td>
<td>Triggered when an item from New+ is created (copied to the current directory).</td>
@@ -743,6 +807,10 @@ _If you want to find diagnostic data events in the source code, these two links
<td>Microsoft.PowerToys.NewPlus_EventCopyTemplateResult</td>
<td>Logs the success of item creation (copying).</td>
</tr>
<tr>
<td>Microsoft.PowerToys.NewPlus_EventOpenTemplates</td>
<td>Triggered when the templates folder is opened.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.NewPlus_EventShowTemplateItems</td>
<td>Triggered when the New+ context menu flyout is displayed.</td>
@@ -928,12 +996,8 @@ _If you want to find diagnostic data events in the source code, these two links
<th>Description</th>
</tr>
<tr>
<td>Microsoft.PowerToys.ShortcutGuide_EnableGuide</td>
<td>Triggered when Shortcut Guide is enabled.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.ShortcutGuide_HideGuide</td>
<td>Occurs when Shortcut Guide is hidden from view.</td>
<td>Microsoft.PowerToys.ShortcutGuide_GuideSession</td>
<td>Logs a Shortcut Guide session including duration and how it was closed.</td>
</tr>
<tr>
<td>Microsoft.PowerToys.ShortcutGuide_Settings</td>

File diff suppressed because it is too large Load Diff

1045
PowerToys.slnx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -134,7 +134,7 @@ If you prefer, you can alternatively build prerequisite projects for the install
#### Locally compiling the installer
1. Open `installer\PowerToysSetup.sln`
1. Open `installer\PowerToysSetup.slnx`
1. In Visual Studio, in the `Solutions Configuration` drop-down menu select `Release`
1. From the `Build` menu choose `Build Solution`.
@@ -144,9 +144,9 @@ To build the installer from the command line, run `Developer Command Prompt for
```
git clean -xfd -e *exe -- .\installer\
MSBuild -t:restore .\installer\PowerToysSetup.sln -p:RestorePackagesConfig=true /p:Platform="x64" /p:Configuration=Release
MSBuild -t:Restore -m .\installer\PowerToysSetup.sln /t:PowerToysInstallerVNext /p:Configuration=Release /p:Platform="x64"
MSBuild -t:Restore -m .\installer\PowerToysSetup.sln /t:PowerToysBootstrapperVNext /p:Configuration=Release /p:Platform="x64"
MSBuild -t:restore .\installer\PowerToysSetup.slnx -p:RestorePackagesConfig=true /p:Platform="x64" /p:Configuration=Release
MSBuild -t:Restore -m .\installer\PowerToysSetup.slnx /t:PowerToysInstallerVNext /p:Configuration=Release /p:Platform="x64"
MSBuild -t:Restore -m .\installer\PowerToysSetup.slnx /t:PowerToysBootstrapperVNext /p:Configuration=Release /p:Platform="x64"
```
### Supported arguments for the .EXE Bootstrapper installer

View File

@@ -19,7 +19,7 @@ You can build the entire solution from the command line, which is sometimes fast
2. Navigate to the repository root directory
3. Run the following command(don't forget to set the correct platform):
```pwsh
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln /tl /p:NuGetInteractive="true"
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.slnx /tl /p:NuGetInteractive="true"
```
4. This process should complete in approximately 13-14 minutes for a full build

View File

@@ -42,10 +42,10 @@ Or reach out to "tools\build\BUILD-GUIDELINES.md"
### Sample plain msbuild command
```powershell
# Restore:
msbuild powertoys.sln -t:restore -p:configuration=debug -p:platform=x64 -m
msbuild powertoys.slnx -t:restore -p:configuration=debug -p:platform=x64 -m
# Build powertoys sln
msbuild powertoys.sln -p:configuration=debug -p:platform=x64 -m
# Build powertoys slnx
msbuild powertoys.slnx -p:configuration=debug -p:platform=x64 -m
# dotnet project
msbuild src\settings-ui\Settings.UI\PowerToys.Settings.csproj -p:Platform=x64 -p:Configuration=Debug -m
@@ -122,7 +122,7 @@ Similar for attach to managed code.
| Task | Command / Action | Notes |
|------|------------------|-------|
| Clean | `git clean -xdf` (careful) or `msbuild /t:Clean PowerToys.sln` | Deep clean removes packages & build outputs |
| Clean | `git clean -xdf` (careful) or `msbuild /t:Clean PowerToys.slnx` | Deep clean removes packages & build outputs |
| Rebuild single project | `msbuild path\to\proj.vcxproj /t:Rebuild -p:Platform=x64 -p:Configuration=Debug` | Faster than whole solution |
| Generate installer (rare in inner loop) | See `tools\build\build-installer.ps1` | Usually not needed for local debug |
| Resource conversion errors | Re-run restore + build | Triggers custom PowerShell targets |
| Resource conversion errors | Re-run restore + build | Triggers custom PowerShell targets |

View File

@@ -12,7 +12,7 @@
- Exit PowerToys if it's running.
- Open `PowerToys.sln` in Visual Studio and build the solution.
- Open `PowerToys.slnx` in Visual Studio and build the solution.
- Run tests in the Test Explorer (`Test > Test Explorer` or `Ctrl+E, T`).

View File

@@ -86,7 +86,7 @@ The module provides a user interface for configuring settings in the PowerToys S
### Building and Testing
1. Clone the repository: `git clone https://github.com/microsoft/PowerToys.git`
2. Open PowerToys.sln in Visual Studio
2. Open PowerToys.slnx in Visual Studio
3. Select the Release configuration and build the solution
4. Run PowerToys.exe from the output directory to test the module

View File

@@ -161,7 +161,7 @@ FancyZones is divided into several projects:
```
git clone https://github.com/microsoft/PowerToys.git
```
2. Open `PowerToys.sln` in Visual Studio
2. Open `PowerToys.slnx` in Visual Studio
3. Select the Release configuration and build the solution
4. If you encounter build errors, try deleting the x64 output folder and rebuild
@@ -244,7 +244,7 @@ UI tests are implemented using [Windows Application Driver](https://github.com/m
- Exit PowerToys if it's running
- Run WinAppDriver.exe from the installation directory. Skip this step if installed in the default directory (`C:\Program Files (x86)\Windows Application Driver`); in this case, it'll be launched automatically during tests.
- Open `PowerToys.sln` in Visual Studio and build the solution.
- Open `PowerToys.slnx` in Visual Studio and build the solution.
- Run tests in the Test Explorer (`Test > Test Explorer` or `Ctrl+E, T`).
>Note: notifications or other application windows, that are shown above the window under test, can disrupt the testing process.

View File

@@ -11,7 +11,7 @@ Keyboard Manager consists of two main components:
## Development Environment Setup
1. Clone the PowerToys repository
2. Open `PowerToys.sln` in Visual Studio
2. Open `PowerToys.slnx` in Visual Studio
3. Ensure all NuGet packages are restored
4. Build the entire solution in Debug configuration
@@ -91,4 +91,4 @@ If you encounter issues with multiple instances, check the mutex logic in `Keybo
To debug both the Editor and Engine:
1. Launch the Engine first in debug mode
2. Attach the debugger to the Editor process when it starts
2. Attach the debugger to the Editor process when it starts

View File

@@ -92,7 +92,7 @@ The modules settings are exposed in the PowerToys Settings UI. Options includ
3. Build the solution:
```sh
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln
msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.slnx
```
> Note: This may take some time.

View File

@@ -53,7 +53,7 @@ The Screen Ruler module consists of several components:
### Building
1. Open PowerToys.sln in Visual Studio
1. Open PowerToys.slnx in Visual Studio
2. In the Solutions Configuration drop-down menu, select Release or Debug
3. From the Build menu, choose Build Solution
4. The executable app for Screen Ruler is named PowerToys.MeasureToolUI.exe

View File

@@ -19,7 +19,7 @@ Shortcut Guide is a PowerToy that displays an overlay of available keyboard shor
## Build and Debug Instructions
### Build
1. Open PowerToys.sln in Visual Studio
1. Open PowerToys.slnx in Visual Studio
2. Select Release or Debug in the Solutions Configuration drop-down menu
3. From the Build menu, choose Build Solution
4. The executable is named PowerToys.ShortcutGuide.exe

View File

@@ -80,7 +80,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an
### Install Visual Studio dependencies
1. Open the `PowerToys.sln` file.
1. Open the `PowerToys.slnx` file.
1. If you see a dialog that says `install extra components` in the solution explorer pane, click `install`
### Get Submodules to compile
@@ -93,7 +93,7 @@ We have submodules that need to be initialized before you can compile most parts
### Compiling Source Code
- Open `PowerToys.sln` in Visual Studio.
- Open `PowerToys.slnx` in Visual Studio.
- In the `Solutions Configuration` drop-down menu select `Release` or `Debug`.
- From the `Build` menu choose `Build Solution`, or press <kbd>Control</kbd>+<kbd>Shift</kbd>+<kbd>b</kbd> on your keyboard.
- The build process may take several minutes depending on your computer's performance. Once it completes, the PowerToys binaries will be in your repo under `x64\Release\`.
@@ -107,10 +107,10 @@ Our installer is two parts, an EXE and an MSI. The EXE (Bootstrapper) contains
The installer can only be compiled in `Release` mode; steps 1 and 2 must be performed before the MSI can be compiled.
1. Compile `PowerToys.sln`. Instructions are listed above.
1. Compile `PowerToys.slnx`. Instructions are listed above.
1. Compile `BugReportTool.sln` tool. Path from root: `tools\BugReportTool\BugReportTool.sln` (details listed below)
1. Compile `StylesReportTool.sln` tool. Path from root: `tools\StylesReportTool\StylesReportTool.sln` (details listed below)
1. Compile `PowerToysSetup.sln` Path from root: `installer\PowerToysSetup.sln` (details listed below)
1. Compile `PowerToysSetup.slnx` Path from root: `installer\PowerToysSetup.slnx` (details listed below)
See [Installer](core/installer.md) for more details on building and debugging the installer.

View File

@@ -1,96 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32414.318
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "spdlog", "..\src\logging\logging.vcxproj", "{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "logger", "..\src\common\logger\logger.vcxproj", "{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Version", "..\src\common\version\version.vcxproj", "{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EtwTrace", "..\src\common\Telemetry\EtwTrace\EtwTrace.vcxproj", "{8F021B46-362B-485C-BFBA-CCF83E820CBD}"
EndProject
Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "PowerToysInstallerVNext", "PowerToysSetupVNext\PowerToysInstallerVNext.wixproj", "{B6E94700-DF38-41F6-A3FD-18B69674AB1E}"
EndProject
Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "PowerToysBootstrapperVNext", "PowerToysSetupVNext\PowerToysBootstrapperVNext.wixproj", "{DA4E9744-80BE-424C-B0F5-AFD8757DB575}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToysSetupCustomActionsVNext", "PowerToysSetupCustomActionsVNext\PowerToysSetupCustomActionsVNext.vcxproj", "{B3A354B0-1E54-4B55-A962-FB5AF9330C19}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SilentFilesInUseBAFunction", "PowerToysSetupVNext\SilentFilesInUseBA\SilentFilesInUseBAFunction.vcxproj", "{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|ARM64.ActiveCfg = Debug|ARM64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.ActiveCfg = Debug|x64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.Build.0 = Debug|x64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|ARM64.ActiveCfg = Release|ARM64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|ARM64.Build.0 = Release|ARM64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.ActiveCfg = Release|x64
{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.Build.0 = Release|x64
{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|ARM64.ActiveCfg = Debug|ARM64
{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x64.ActiveCfg = Debug|x64
{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x64.Build.0 = Debug|x64
{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|ARM64.ActiveCfg = Release|ARM64
{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|ARM64.Build.0 = Release|ARM64
{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x64.ActiveCfg = Release|x64
{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x64.Build.0 = Release|x64
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|ARM64.ActiveCfg = Debug|ARM64
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|ARM64.Build.0 = Debug|ARM64
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x64.ActiveCfg = Debug|x64
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x64.Build.0 = Debug|x64
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|ARM64.ActiveCfg = Release|ARM64
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|ARM64.Build.0 = Release|ARM64
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x64.ActiveCfg = Release|x64
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x64.Build.0 = Release|x64
{8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|ARM64.ActiveCfg = Debug|ARM64
{8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|ARM64.Build.0 = Debug|ARM64
{8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|x64.ActiveCfg = Debug|x64
{8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|x64.Build.0 = Debug|x64
{8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|ARM64.ActiveCfg = Release|ARM64
{8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|ARM64.Build.0 = Release|ARM64
{8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|x64.ActiveCfg = Release|x64
{8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|x64.Build.0 = Release|x64
{B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Debug|ARM64.ActiveCfg = Debug|ARM64
{B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Debug|ARM64.Build.0 = Debug|ARM64
{B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Debug|x64.ActiveCfg = Debug|x64
{B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Debug|x64.Build.0 = Debug|x64
{B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Release|ARM64.ActiveCfg = Release|ARM64
{B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Release|ARM64.Build.0 = Release|ARM64
{B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Release|x64.ActiveCfg = Release|x64
{B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Release|x64.Build.0 = Release|x64
{DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Debug|ARM64.ActiveCfg = Debug|ARM64
{DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Debug|ARM64.Build.0 = Debug|ARM64
{DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Debug|x64.ActiveCfg = Debug|x64
{DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Debug|x64.Build.0 = Debug|x64
{DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Release|ARM64.ActiveCfg = Release|ARM64
{DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Release|ARM64.Build.0 = Release|ARM64
{DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Release|x64.ActiveCfg = Release|x64
{DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Release|x64.Build.0 = Release|x64
{B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Debug|ARM64.ActiveCfg = Debug|ARM64
{B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Debug|x64.ActiveCfg = Debug|x64
{B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Debug|x64.Build.0 = Debug|x64
{B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Release|ARM64.ActiveCfg = Release|ARM64
{B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Release|ARM64.Build.0 = Release|ARM64
{B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Release|x64.ActiveCfg = Release|x64
{B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Release|x64.Build.0 = Release|x64
{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Debug|ARM64.ActiveCfg = Debug|ARM64
{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Debug|x64.ActiveCfg = Debug|x64
{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Debug|x64.Build.0 = Debug|x64
{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Release|ARM64.ActiveCfg = Release|ARM64
{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Release|ARM64.Build.0 = Release|ARM64
{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Release|x64.ActiveCfg = Release|x64
{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B7A3DA30-D443-40FF-AC51-988AD41E3962}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,22 @@
<Solution>
<Configurations>
<Platform Name="ARM64" />
<Platform Name="x64" />
</Configurations>
<Project Path="../src/common/logger/logger.vcxproj" Id="d9b8fc84-322a-4f9f-bbb9-20915c47ddfd">
<Build Solution="Debug|ARM64" Project="false" />
</Project>
<Project Path="../src/common/Telemetry/EtwTrace/EtwTrace.vcxproj" Id="8f021b46-362b-485c-bfba-ccf83e820cbd" />
<Project Path="../src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
<Project Path="../src/logging/logging.vcxproj" Id="7e1e3f13-2bd6-3f75-a6a7-873a2b55c60f">
<Build Solution="Debug|ARM64" Project="false" />
</Project>
<Project Path="PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj" Id="b3a354b0-1e54-4b55-a962-fb5af9330c19">
<Build Solution="Debug|ARM64" Project="false" />
</Project>
<Project Path="PowerToysSetupVNext/PowerToysBootstrapperVNext.wixproj" Type="b7dd6f7e-def8-4e67-b5b7-07ef123db6f0" />
<Project Path="PowerToysSetupVNext/PowerToysInstallerVNext.wixproj" Type="b7dd6f7e-def8-4e67-b5b7-07ef123db6f0" />
<Project Path="PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj" Id="f8b9f842-f5c3-4a2d-8c85-7f8b9e2b4f1d">
<Build Solution="Debug|ARM64" Project="false" />
</Project>
</Solution>

View File

@@ -135,8 +135,9 @@ public partial class App : Application
try
{
var winget = new WinGetExtensionCommandsProvider();
var callback = allApps.LookupApp;
winget.SetAllLookup(callback);
winget.SetAllLookup(
query => allApps.LookupAppByPackageFamilyName(query, requireSingleMatch: true),
query => allApps.LookupAppByProductCode(query, requireSingleMatch: true));
services.AddSingleton<ICommandProvider>(winget);
}
catch (Exception ex)

View File

@@ -58,7 +58,7 @@ public class AllAppsCommandProviderTests : AppsTestBase
var provider = new AllAppsCommandProvider(page);
// Act
var result = provider.LookupApp(string.Empty);
var result = provider.LookupAppByDisplayName(string.Empty);
// Assert
Assert.IsNotNull(result);
@@ -77,7 +77,7 @@ public class AllAppsCommandProviderTests : AppsTestBase
await WaitForPageInitializationAsync();
// Act
var result = provider.LookupApp("TestApp");
var result = provider.LookupAppByDisplayName("TestApp");
// Assert
Assert.IsNotNull(result);
@@ -97,7 +97,7 @@ public class AllAppsCommandProviderTests : AppsTestBase
await WaitForPageInitializationAsync();
// Act
var result = provider.LookupApp("NonExistentApp");
var result = provider.LookupAppByDisplayName("NonExistentApp");
// Assert
Assert.IsNull(result);

View File

@@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions;
@@ -66,7 +68,71 @@ public partial class AllAppsCommandProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() => [_listItem, .. _page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName)
public ICommandItem? LookupAppByPackageFamilyName(string packageFamilyName, bool requireSingleMatch)
{
if (string.IsNullOrEmpty(packageFamilyName))
{
return null;
}
var items = _page.GetItems();
List<ICommandItem> matches = [];
foreach (var item in items)
{
if (item is AppListItem appItem && string.Equals(packageFamilyName, appItem.App.PackageFamilyName, StringComparison.OrdinalIgnoreCase))
{
matches.Add(item);
if (!requireSingleMatch)
{
// Return early if we don't require uniqueness.
return item;
}
}
}
return requireSingleMatch && matches.Count == 1 ? matches[0] : null;
}
public ICommandItem? LookupAppByProductCode(string productCode, bool requireSingleMatch)
{
if (string.IsNullOrEmpty(productCode))
{
return null;
}
if (!UninstallRegistryAppLocator.TryGetInstallInfo(productCode, out _, out var candidates) || candidates.Count <= 0)
{
return null;
}
var items = _page.GetItems();
List<ICommandItem> matches = [];
foreach (var item in items)
{
if (item is not AppListItem appListItem || string.IsNullOrEmpty(appListItem.App.FullExecutablePath))
{
continue;
}
foreach (var candidate in candidates)
{
if (string.Equals(appListItem.App.FullExecutablePath, candidate, StringComparison.OrdinalIgnoreCase))
{
matches.Add(item);
if (!requireSingleMatch)
{
return item;
}
}
}
}
return requireSingleMatch && matches.Count == 1 ? matches[0] : null;
}
public ICommandItem? LookupAppByDisplayName(string displayName)
{
var items = _page.GetItems();

View File

@@ -29,6 +29,10 @@ public sealed class AppItem
public string AppIdentifier { get; set; } = string.Empty;
public string? PackageFamilyName { get; set; }
public string? FullExecutablePath { get; set; }
public AppItem()
{
}

View File

@@ -40,6 +40,8 @@ public sealed partial class AppListItem : ListItem
public string AppIdentifier => _app.AppIdentifier;
public AppItem App => _app;
public AppListItem(AppItem app, bool useThumbnails, bool isPinned)
{
Command = _appCommand = new AppCommand(app);
@@ -82,6 +84,12 @@ public sealed partial class AppListItem : ListItem
metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } });
}
#if DEBUG
metadata.Add(new DetailsElement() { Key = "[DEBUG] AppIdentifier", Data = new DetailsLink() { Text = _app.AppIdentifier } });
metadata.Add(new DetailsElement() { Key = "[DEBUG] ExePath", Data = new DetailsLink() { Text = _app.ExePath } });
metadata.Add(new DetailsElement() { Key = "[DEBUG] IcoPath", Data = new DetailsLink() { Text = _app.IcoPath } });
#endif
// Icon
IconInfo? heroImage = null;
if (_app.IsPackaged)

View File

@@ -0,0 +1,205 @@
// 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.IO;
using System.Linq;
using Microsoft.Win32;
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
internal static class UninstallRegistryAppLocator
{
private static readonly string[] UninstallBaseKeys =
[
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
];
/// <summary>
/// Tries to find install directory and a list of plausible main EXEs from an uninstall key
/// (e.g. Inno Setup keys like "{guid}_is1").
/// <paramref name="exeCandidates"/> may be empty if we couldn't pick any safe EXEs.
/// </summary>
/// <returns>
/// Returns true if the uninstall key is found and an install directory is resolved.
/// </returns>
public static bool TryGetInstallInfo(
string uninstallKeyName,
out string? installDir,
out IReadOnlyList<string> exeCandidates,
string? expectedExeName = null)
{
installDir = null;
exeCandidates = [];
if (string.IsNullOrWhiteSpace(uninstallKeyName))
{
throw new ArgumentException("Key name must not be null or empty.", nameof(uninstallKeyName));
}
uninstallKeyName = uninstallKeyName.Trim();
foreach (var baseKeyPath in UninstallBaseKeys)
{
// HKLM
using (var key = Registry.LocalMachine.OpenSubKey($"{baseKeyPath}\\{uninstallKeyName}"))
{
if (TryFromUninstallKey(key, expectedExeName, out installDir, out exeCandidates))
{
return true;
}
}
// HKCU
using (var key = Registry.CurrentUser.OpenSubKey($"{baseKeyPath}\\{uninstallKeyName}"))
{
if (TryFromUninstallKey(key, expectedExeName, out installDir, out exeCandidates))
{
return true;
}
}
}
return false;
}
private static bool TryFromUninstallKey(
RegistryKey? key,
string? expectedExeName,
out string? installDir,
out IReadOnlyList<string> exeCandidates)
{
installDir = null;
exeCandidates = [];
if (key is null)
{
return false;
}
var location = (key.GetValue("InstallLocation") as string)?.Trim('"', ' ', '\t');
if (string.IsNullOrEmpty(location))
{
location = (key.GetValue("Inno Setup: App Path") as string)?.Trim('"', ' ', '\t');
}
if (string.IsNullOrEmpty(location))
{
var uninstall = key.GetValue("UninstallString") as string;
var uninsExe = ExtractFirstPath(uninstall);
if (!string.IsNullOrEmpty(uninsExe))
{
var dir = Path.GetDirectoryName(uninsExe);
if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
{
location = dir;
}
}
}
if (string.IsNullOrEmpty(location) || !Directory.Exists(location))
{
return false;
}
installDir = location;
// Collect safe EXE candidates; may be empty if ambiguous or only uninstall exes exist.
exeCandidates = GetExeCandidates(location, expectedExeName);
return true;
}
private static IReadOnlyList<string> GetExeCandidates(string root, string? expectedExeName)
{
// Look at root and a "bin" subfolder (very common pattern)
var allExes = Directory.EnumerateFiles(root, "*.exe", SearchOption.TopDirectoryOnly)
.Concat(GetBinExes(root))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (allExes.Length == 0)
{
return [];
}
var result = new List<string>();
// 1) Exact match on expected exe name (if provided), ignoring case, and not uninstall/setup-like.
if (!string.IsNullOrWhiteSpace(expectedExeName))
{
foreach (var exe in allExes)
{
if (string.Equals(Path.GetFileName(exe), expectedExeName, StringComparison.OrdinalIgnoreCase) &&
!LooksLikeUninstallerOrSetup(exe))
{
result.Add(exe);
}
}
}
// 2) All other non-uninstall/setup exes
foreach (var exe in allExes)
{
if (LooksLikeUninstallerOrSetup(exe))
{
continue;
}
// Skip ones already added as expectedExeName matches
if (result.Contains(exe, StringComparer.OrdinalIgnoreCase))
{
continue;
}
result.Add(exe);
}
// 3) We intentionally do NOT add uninstall/setup/update exes here.
// If you ever want them, you can add a separate API to expose them.
return result;
}
private static IEnumerable<string> GetBinExes(string root)
{
var bin = Path.Combine(root, "bin");
return !Directory.Exists(bin)
? []
: Directory.EnumerateFiles(bin, "*.exe", SearchOption.TopDirectoryOnly);
}
private static bool LooksLikeUninstallerOrSetup(string path)
{
var name = Path.GetFileName(path);
return name.StartsWith("unins", StringComparison.OrdinalIgnoreCase) // e.g. Inno: unins000.exe
|| name.Contains("setup", StringComparison.OrdinalIgnoreCase) // setup.exe
|| name.Contains("installer", StringComparison.OrdinalIgnoreCase) // installer.exe / MyAppInstaller.exe
|| name.Contains("update", StringComparison.OrdinalIgnoreCase); // updater/updater.exe
}
private static string? ExtractFirstPath(string? commandLine)
{
if (string.IsNullOrWhiteSpace(commandLine))
{
return null;
}
commandLine = commandLine.Trim();
if (commandLine.StartsWith('"'))
{
var endQuote = commandLine.IndexOf('"', 1);
if (endQuote > 1)
{
return commandLine[1..endQuote];
}
}
var firstSpace = commandLine.IndexOf(' ');
var candidate = firstSpace > 0 ? commandLine[..firstSpace] : commandLine;
candidate = candidate.Trim('"');
return candidate.Length > 0 ? candidate : null;
}
}

View File

@@ -558,6 +558,7 @@ public class UWPApplication : IUWPApplication
IsPackaged = true,
Commands = app.GetCommands(),
AppIdentifier = app.GetAppIdentifier(),
PackageFamilyName = app.Package.FamilyName,
};
return item;
}

View File

@@ -1065,6 +1065,7 @@ public class Win32Program : IProgram
DirPath = app.Location,
Commands = app.GetCommands(),
AppIdentifier = app.GetAppIdentifier(),
FullExecutablePath = app.FullPath,
};
}
}

View File

@@ -62,7 +62,7 @@ public partial class InstallPackageCommand : InvokableCommand
{
PackageInstallCommandState.Install => Icons.DownloadIcon,
PackageInstallCommandState.Update => Icons.UpdateIcon,
PackageInstallCommandState.Uninstall => Icons.CompletedIcon,
PackageInstallCommandState.Uninstall => Icons.DeleteIcon,
_ => throw new NotImplementedException(),
};
Name = InstallCommandState switch

View File

@@ -194,46 +194,95 @@ public partial class InstallPackageListItem : ListItem
var isInstalled = _package.InstalledVersion is not null;
var installedState = isInstalled ?
(_package.IsUpdateAvailable ?
PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) :
(_package.IsUpdateAvailable ? PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) :
PackageInstallCommandState.Install;
// might be an uninstall command
InstallPackageCommand installCommand = new(_package, installedState);
if (isInstalled)
if (_package.InstalledVersion is not null)
{
this.Icon = installCommand.Icon;
this.Command = new NoOpCommand();
#if DEBUG
var installerType = _package.InstalledVersion.GetMetadata(PackageVersionMetadataField.InstallerType);
Subtitle = installerType + " | " + Subtitle;
#endif
List<IContextItem> contextMenu = [];
CommandContextItem uninstallContextItem = new(installCommand)
Command = installCommand;
Icon = installedState switch
{
IsCritical = true,
Icon = Icons.DeleteIcon,
PackageInstallCommandState.Install => Icons.DownloadIcon,
PackageInstallCommandState.Update => Icons.UpdateIcon,
PackageInstallCommandState.Uninstall => Icons.CompletedIcon,
_ => Icons.DownloadIcon,
};
if (WinGetStatics.AppSearchCallback is not null)
TryLocateAndAppendActionForApp(contextMenu);
MoreCommands = contextMenu.ToArray();
}
else
{
_installCommand = new InstallPackageCommand(_package, installedState);
_installCommand.InstallStateChanged += InstallStateChangedHandler;
Command = _installCommand;
Icon = _installCommand.Icon;
}
}
private void TryLocateAndAppendActionForApp(List<IContextItem> contextMenu)
{
try
{
// Let's try to connect it to an installed app if possible
// This is a bit of dark magic, since there's no direct link between
// WinGet packages and installed apps.
var lookupByPackageName = WinGetStatics.AppSearchByPackageFamilyNameCallback;
if (lookupByPackageName is not null)
{
var callback = WinGetStatics.AppSearchCallback;
var installedApp = callback(_package.DefaultInstallVersion is null ? _package.Name : _package.DefaultInstallVersion.DisplayName);
if (installedApp is not null)
var names = _package.InstalledVersion.PackageFamilyNames;
for (var i = 0; i < names.Count; i++)
{
this.Command = installedApp.Command;
contextMenu = [.. installedApp.MoreCommands];
var installedAppByPfn = lookupByPackageName(names[i]);
if (installedAppByPfn is not null)
{
contextMenu.Add(new Separator());
contextMenu.Add(new CommandContextItem(installedAppByPfn.Command));
foreach (var item in installedAppByPfn.MoreCommands)
{
contextMenu.Add(item);
}
return;
}
}
}
contextMenu.Add(uninstallContextItem);
this.MoreCommands = contextMenu.ToArray();
return;
var lookupByProductCode = WinGetStatics.AppSearchByProductCodeCallback;
if (lookupByProductCode is not null)
{
var productCodes = _package.InstalledVersion.ProductCodes;
for (var i = 0; i < productCodes.Count; i++)
{
var installedAppByProductCode = lookupByProductCode(productCodes[i]);
if (installedAppByProductCode is not null)
{
contextMenu.Add(new Separator());
contextMenu.Add(new CommandContextItem(installedAppByProductCode.Command));
foreach (var item in installedAppByProductCode.MoreCommands)
{
contextMenu.Add(item);
}
return;
}
}
}
}
catch (Exception ex)
{
Logger.LogError($"Failed to retrieve app context menu items for package '{_package?.Name ?? "Unknown"}'", ex);
}
// didn't find the app
_installCommand = new InstallPackageCommand(_package, installedState);
this.Command = _installCommand;
Icon = _installCommand.Icon;
_installCommand.InstallStateChanged += InstallStateChangedHandler;
}
private void InstallStateChangedHandler(object? sender, InstallPackageCommand e)

View File

@@ -41,5 +41,9 @@ public partial class WinGetExtensionCommandsProvider : CommandProvider
public override void InitializeWithHost(IExtensionHost host) => WinGetExtensionHost.Instance.Initialize(host);
public void SetAllLookup(Func<string, ICommandItem?> callback) => WinGetStatics.AppSearchCallback = callback;
public void SetAllLookup(Func<string, ICommandItem?> lookupByPackageName, Func<string, ICommandItem?> lookupByProductCode)
{
WinGetStatics.AppSearchByPackageFamilyNameCallback = lookupByPackageName;
WinGetStatics.AppSearchByProductCodeCallback = lookupByProductCode;
}
}

View File

@@ -34,7 +34,9 @@ internal static class WinGetStatics
private static readonly StatusMessage _errorMessage = new() { State = MessageState.Error };
public static Func<string, ICommandItem?>? AppSearchCallback { get; set; }
public static Func<string, ICommandItem?>? AppSearchByPackageFamilyNameCallback { get; set; }
public static Func<string, ICommandItem?>? AppSearchByProductCodeCallback { get; set; }
private static readonly CompositeFormat CreateCatalogErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.winget_create_catalog_error);

View File

@@ -56,7 +56,7 @@ if ($IsAzurePipelineBuild) {
}
if (($BuildStep -ieq "all") -Or ($BuildStep -ieq "build")) {
& $nugetPath restore (Join-Path $PSScriptRoot "..\..\..\..\..\PowerToys.sln")
& $nugetPath restore (Join-Path $PSScriptRoot "..\..\..\..\..\PowerToys.slnx")
Try {
foreach ($config in $Configuration.Split(",")) {

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
namespace FancyZonesCLI.Commands;
/// <summary>
/// Editor and Settings commands.
/// </summary>
internal static class EditorCommands
{
public static (int ExitCode, string Output) OpenEditor()
{
var editorExe = "PowerToys.FancyZonesEditor.exe";
// Check if editor-parameters.json exists
if (!FancyZonesData.EditorParametersExist())
{
return (1, "Error: editor-parameters.json not found.\nPlease launch FancyZones Editor using Win+` (Win+Backtick) hotkey first.");
}
// Check if editor is already running
var existingProcess = Process.GetProcessesByName("PowerToys.FancyZonesEditor").FirstOrDefault();
if (existingProcess != null)
{
NativeMethods.SetForegroundWindow(existingProcess.MainWindowHandle);
return (0, "FancyZones Editor is already running. Brought window to foreground.");
}
// Only check same directory as CLI
var editorPath = Path.Combine(AppContext.BaseDirectory, editorExe);
if (File.Exists(editorPath))
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = editorPath,
UseShellExecute = true,
});
return (0, "FancyZones Editor launched successfully.");
}
catch (Exception ex)
{
return (1, $"Failed to launch: {ex.Message}");
}
}
return (1, $"Error: Could not find {editorExe} in {AppContext.BaseDirectory}");
}
public static (int ExitCode, string Output) OpenSettings()
{
try
{
// Find PowerToys.exe in common locations
string powertoysExe = null;
// Check in the same directory as the CLI (typical for dev builds)
var sameDirPath = Path.Combine(AppContext.BaseDirectory, "PowerToys.exe");
if (File.Exists(sameDirPath))
{
powertoysExe = sameDirPath;
}
if (powertoysExe == null)
{
return (1, "Error: PowerToys.exe not found. Please ensure PowerToys is installed.");
}
Process.Start(new ProcessStartInfo
{
FileName = powertoysExe,
Arguments = "--open-settings=FancyZones",
UseShellExecute = false,
});
return (0, "FancyZones Settings opened successfully.");
}
catch (Exception ex)
{
return (1, $"Error: Failed to open FancyZones Settings. {ex.Message}");
}
}
}

View File

@@ -0,0 +1,98 @@
// 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.Globalization;
using System.Linq;
namespace FancyZonesCLI.Commands;
/// <summary>
/// Hotkey-related commands.
/// </summary>
internal static class HotkeyCommands
{
public static (int ExitCode, string Output) GetHotkeys()
{
var hotkeys = FancyZonesData.ReadLayoutHotkeys();
if (hotkeys?.Hotkeys == null || hotkeys.Hotkeys.Count == 0)
{
return (0, "No hotkeys configured.");
}
var sb = new System.Text.StringBuilder();
sb.AppendLine("=== Layout Hotkeys ===\n");
sb.AppendLine("Press Win + Ctrl + Alt + <number> to switch layouts:\n");
foreach (var hotkey in hotkeys.Hotkeys.OrderBy(h => h.Key))
{
sb.AppendLine(CultureInfo.InvariantCulture, $" [{hotkey.Key}] => {hotkey.LayoutId}");
}
return (0, sb.ToString().TrimEnd());
}
public static (int ExitCode, string Output) SetHotkey(int key, string layoutUuid, Action<uint> notifyFancyZones, uint wmPrivLayoutHotkeysFileUpdate)
{
if (key < 0 || key > 9)
{
return (1, "Error: Key must be between 0 and 9");
}
// Check if this is a custom layout UUID
var customLayouts = FancyZonesData.ReadCustomLayouts();
var matchedLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(layoutUuid, StringComparison.OrdinalIgnoreCase));
bool isCustomLayout = matchedLayout != null;
string layoutName = matchedLayout?.Name ?? layoutUuid;
var hotkeys = FancyZonesData.ReadLayoutHotkeys() ?? new LayoutHotkeys();
hotkeys.Hotkeys ??= new List<LayoutHotkey>();
// Remove existing hotkey for this key
hotkeys.Hotkeys.RemoveAll(h => h.Key == key);
// Add new hotkey
hotkeys.Hotkeys.Add(new LayoutHotkey { Key = key, LayoutId = layoutUuid });
// Save
FancyZonesData.WriteLayoutHotkeys(hotkeys);
// Notify FancyZones
notifyFancyZones(wmPrivLayoutHotkeysFileUpdate);
if (isCustomLayout)
{
return (0, $"✓ Hotkey {key} assigned to custom layout '{layoutName}'\n Press Win + Ctrl + Alt + {key} to switch to this layout");
}
else
{
return (0, $"⚠ Warning: Hotkey {key} assigned to '{layoutUuid}'\n Note: FancyZones hotkeys only work with CUSTOM layouts.\n Template layouts (focus, columns, rows, etc.) cannot be used with hotkeys.\n Create a custom layout in the FancyZones Editor to use this hotkey.");
}
}
public static (int ExitCode, string Output) RemoveHotkey(int key, Action<uint> notifyFancyZones, uint wmPrivLayoutHotkeysFileUpdate)
{
var hotkeys = FancyZonesData.ReadLayoutHotkeys();
if (hotkeys?.Hotkeys == null)
{
return (0, $"No hotkey assigned to key {key}");
}
var removed = hotkeys.Hotkeys.RemoveAll(h => h.Key == key);
if (removed == 0)
{
return (0, $"No hotkey assigned to key {key}");
}
// Save
FancyZonesData.WriteLayoutHotkeys(hotkeys);
// Notify FancyZones
notifyFancyZones(wmPrivLayoutHotkeysFileUpdate);
return (0, $"Hotkey {key} removed");
}
}

View File

@@ -0,0 +1,276 @@
// 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.Globalization;
using System.Linq;
using System.Text.Json;
namespace FancyZonesCLI.Commands;
/// <summary>
/// Layout-related commands.
/// </summary>
internal static class LayoutCommands
{
public static (int ExitCode, string Output) GetLayouts()
{
var sb = new System.Text.StringBuilder();
// Print template layouts
var templatesJson = FancyZonesData.ReadLayoutTemplates();
if (templatesJson?.Templates != null)
{
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Built-in Template Layouts ({templatesJson.Templates.Count} total) ===\n");
for (int i = 0; i < templatesJson.Templates.Count; i++)
{
var template = templatesJson.Templates[i];
sb.AppendLine(CultureInfo.InvariantCulture, $"[T{i + 1}] {template.Type}");
sb.Append(CultureInfo.InvariantCulture, $" Zones: {template.ZoneCount}");
if (template.ShowSpacing && template.Spacing > 0)
{
sb.Append(CultureInfo.InvariantCulture, $", Spacing: {template.Spacing}px");
}
sb.AppendLine();
sb.AppendLine();
// Draw visual preview
sb.Append(LayoutVisualizer.DrawTemplateLayout(template));
if (i < templatesJson.Templates.Count - 1)
{
sb.AppendLine();
}
}
sb.AppendLine("\n");
}
// Print custom layouts
var customLayouts = FancyZonesData.ReadCustomLayouts();
if (customLayouts?.Layouts != null)
{
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.Layouts.Count} total) ===");
for (int i = 0; i < customLayouts.Layouts.Count; i++)
{
var layout = customLayouts.Layouts[i];
sb.AppendLine(CultureInfo.InvariantCulture, $"[{i + 1}] {layout.Name}");
sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.Uuid}");
sb.Append(CultureInfo.InvariantCulture, $" Type: {layout.Type}");
bool isCanvasLayout = false;
if (layout.Info.ValueKind != JsonValueKind.Undefined && layout.Info.ValueKind != JsonValueKind.Null)
{
if (layout.Type == "grid" && layout.Info.TryGetProperty("rows", out var rows) && layout.Info.TryGetProperty("columns", out var cols))
{
sb.Append(CultureInfo.InvariantCulture, $" ({rows.GetInt32()}x{cols.GetInt32()} grid)");
}
else if (layout.Type == "canvas" && layout.Info.TryGetProperty("zones", out var zones))
{
sb.Append(CultureInfo.InvariantCulture, $" ({zones.GetArrayLength()} zones)");
isCanvasLayout = true;
}
}
sb.AppendLine("\n");
// Draw visual preview
sb.Append(LayoutVisualizer.DrawCustomLayout(layout));
// Add note for canvas layouts
if (isCanvasLayout)
{
sb.AppendLine("\n Note: Canvas layout preview is approximate.");
sb.AppendLine(" Open FancyZones Editor for precise zone boundaries.");
}
if (i < customLayouts.Layouts.Count - 1)
{
sb.AppendLine();
}
}
sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout <UUID>' to apply a layout.");
}
return (0, sb.ToString().TrimEnd());
}
public static (int ExitCode, string Output) GetActiveLayout()
{
if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error))
{
return (1, $"Error: {error}");
}
if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0)
{
return (0, "No active layouts found.");
}
var sb = new System.Text.StringBuilder();
sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n");
for (int i = 0; i < appliedLayouts.Layouts.Count; i++)
{
var layout = appliedLayouts.Layouts[i];
sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {i + 1}:");
sb.AppendLine(CultureInfo.InvariantCulture, $" Name: {layout.AppliedLayout.Type}");
sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.AppliedLayout.Uuid}");
sb.AppendLine(CultureInfo.InvariantCulture, $" Type: {layout.AppliedLayout.Type} ({layout.AppliedLayout.ZoneCount} zones)");
if (layout.AppliedLayout.ShowSpacing)
{
sb.AppendLine(CultureInfo.InvariantCulture, $" Spacing: {layout.AppliedLayout.Spacing}px");
}
sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px");
if (i < appliedLayouts.Layouts.Count - 1)
{
sb.AppendLine();
}
}
return (0, sb.ToString().TrimEnd());
}
public static (int ExitCode, string Output) SetLayout(string[] args, Action<uint> notifyFancyZones, uint wmPrivAppliedLayoutsFileUpdate)
{
Logger.LogInfo($"SetLayout called with args: [{string.Join(", ", args)}]");
if (args.Length == 0)
{
return (1, "Error: set-layout requires a UUID parameter");
}
string uuid = args[0];
int? targetMonitor = null;
bool applyToAll = false;
// Parse options
for (int i = 1; i < args.Length; i++)
{
if (args[i] == "--monitor" && i + 1 < args.Length)
{
if (int.TryParse(args[i + 1], out int monitorNum))
{
targetMonitor = monitorNum;
i++; // Skip next arg
}
else
{
return (1, $"Error: Invalid monitor number: {args[i + 1]}");
}
}
else if (args[i] == "--all")
{
applyToAll = true;
}
}
if (targetMonitor.HasValue && applyToAll)
{
return (1, "Error: Cannot specify both --monitor and --all");
}
// Try to find layout in custom layouts first (by UUID)
var customLayouts = FancyZonesData.ReadCustomLayouts();
var targetCustomLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(uuid, StringComparison.OrdinalIgnoreCase));
// If not found in custom layouts, try template layouts (by type name)
TemplateLayout targetTemplate = null;
if (targetCustomLayout == null)
{
var templates = FancyZonesData.ReadLayoutTemplates();
targetTemplate = templates?.Templates?.FirstOrDefault(t => t.Type.Equals(uuid, StringComparison.OrdinalIgnoreCase));
}
if (targetCustomLayout == null && targetTemplate == null)
{
return (1, $"Error: Layout '{uuid}' not found\nTip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')\n For custom layouts, use the UUID from 'get-layouts'");
}
// Read current applied layouts
if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error))
{
return (1, $"Error: {error}");
}
if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0)
{
return (1, "Error: No monitors configured");
}
// Determine which monitors to update
List<int> monitorsToUpdate = new List<int>();
if (applyToAll)
{
for (int i = 0; i < appliedLayouts.Layouts.Count; i++)
{
monitorsToUpdate.Add(i);
}
}
else if (targetMonitor.HasValue)
{
int monitorIndex = targetMonitor.Value - 1; // Convert to 0-based
if (monitorIndex < 0 || monitorIndex >= appliedLayouts.Layouts.Count)
{
return (1, $"Error: Monitor {targetMonitor.Value} not found. Available monitors: 1-{appliedLayouts.Layouts.Count}");
}
monitorsToUpdate.Add(monitorIndex);
}
else
{
// Default: first monitor
monitorsToUpdate.Add(0);
}
// Update selected monitors
foreach (int monitorIndex in monitorsToUpdate)
{
if (targetCustomLayout != null)
{
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Uuid = targetCustomLayout.Uuid;
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Type = targetCustomLayout.Type;
}
else if (targetTemplate != null)
{
// For templates, use all-zeros UUID and the template type
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Uuid = "{00000000-0000-0000-0000-000000000000}";
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Type = targetTemplate.Type;
appliedLayouts.Layouts[monitorIndex].AppliedLayout.ZoneCount = targetTemplate.ZoneCount;
appliedLayouts.Layouts[monitorIndex].AppliedLayout.ShowSpacing = targetTemplate.ShowSpacing;
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Spacing = targetTemplate.Spacing;
}
}
// Write back to file
FancyZonesData.WriteAppliedLayouts(appliedLayouts);
Logger.LogInfo($"Applied layouts file updated for {monitorsToUpdate.Count} monitor(s)");
// Notify FancyZones to reload
notifyFancyZones(wmPrivAppliedLayoutsFileUpdate);
Logger.LogInfo("FancyZones notified of layout change");
string layoutName = targetCustomLayout?.Name ?? targetTemplate?.Type ?? uuid;
if (applyToAll)
{
return (0, $"Layout '{layoutName}' applied to all {monitorsToUpdate.Count} monitors");
}
else if (targetMonitor.HasValue)
{
return (0, $"Layout '{layoutName}' applied to monitor {targetMonitor.Value}");
}
else
{
return (0, $"Layout '{layoutName}' applied to monitor 1");
}
}
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Globalization;
namespace FancyZonesCLI.Commands;
/// <summary>
/// Monitor-related commands.
/// </summary>
internal static class MonitorCommands
{
public static (int ExitCode, string Output) GetMonitors()
{
if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error))
{
return (1, $"Error: {error}");
}
if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0)
{
return (0, "No monitors found.");
}
var sb = new System.Text.StringBuilder();
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Monitors ({appliedLayouts.Layouts.Count} total) ===");
sb.AppendLine();
for (int i = 0; i < appliedLayouts.Layouts.Count; i++)
{
var layout = appliedLayouts.Layouts[i];
var monitorNum = i + 1;
sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {monitorNum}:");
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor: {layout.Device.Monitor}");
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Instance: {layout.Device.MonitorInstance}");
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Number: {layout.Device.MonitorNumber}");
sb.AppendLine(CultureInfo.InvariantCulture, $" Serial Number: {layout.Device.SerialNumber}");
sb.AppendLine(CultureInfo.InvariantCulture, $" Virtual Desktop: {layout.Device.VirtualDesktop}");
sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px");
sb.AppendLine(CultureInfo.InvariantCulture, $" Active Layout: {layout.AppliedLayout.Type}");
sb.AppendLine(CultureInfo.InvariantCulture, $" Zone Count: {layout.AppliedLayout.ZoneCount}");
sb.AppendLine();
}
return (0, sb.ToString().TrimEnd());
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.SelfContained.props" />
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.FancyZonesCLI</AssemblyTitle>
<AssemblyDescription>PowerToys FancyZones Command Line Interface</AssemblyDescription>
<Description>PowerToys FancyZones CLI</Description>
<OutputType>Exe</OutputType>
<Platforms>x64;ARM64</Platforms>
<PublishAot>true</PublishAot>
<DisableRuntimeMarshalling>true</DisableRuntimeMarshalling>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
<AssemblyName>FancyZonesCLI</AssemblyName>
<NoWarn>$(NoWarn);SA1500;SA1402;CA1852</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" />
<PackageReference Include="Microsoft.Windows.CsWin32" PrivateAssets="all" />
</ItemGroup>
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
<ItemGroup>
<FrameworkReference Include="Microsoft.WindowsDesktop.App" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace FancyZonesCLI;
/// <summary>
/// Provides methods to read and write FancyZones configuration data.
/// </summary>
internal static class FancyZonesData
{
/// <summary>
/// Try to read applied layouts configuration.
/// </summary>
public static bool TryReadAppliedLayouts(out AppliedLayouts result, out string error)
{
return TryReadJsonFile(FancyZonesPaths.AppliedLayouts, FancyZonesJsonContext.Default.AppliedLayouts, out result, out error);
}
/// <summary>
/// Read applied layouts or return null if not found.
/// </summary>
public static AppliedLayouts ReadAppliedLayouts()
{
return ReadJsonFileOrDefault(FancyZonesPaths.AppliedLayouts, FancyZonesJsonContext.Default.AppliedLayouts);
}
/// <summary>
/// Write applied layouts configuration.
/// </summary>
public static void WriteAppliedLayouts(AppliedLayouts layouts)
{
WriteJsonFile(FancyZonesPaths.AppliedLayouts, layouts, FancyZonesJsonContext.Default.AppliedLayouts);
}
/// <summary>
/// Read custom layouts or return null if not found.
/// </summary>
public static CustomLayouts ReadCustomLayouts()
{
return ReadJsonFileOrDefault(FancyZonesPaths.CustomLayouts, FancyZonesJsonContext.Default.CustomLayouts);
}
/// <summary>
/// Read layout templates or return null if not found.
/// </summary>
public static LayoutTemplates ReadLayoutTemplates()
{
return ReadJsonFileOrDefault(FancyZonesPaths.LayoutTemplates, FancyZonesJsonContext.Default.LayoutTemplates);
}
/// <summary>
/// Read layout hotkeys or return null if not found.
/// </summary>
public static LayoutHotkeys ReadLayoutHotkeys()
{
return ReadJsonFileOrDefault(FancyZonesPaths.LayoutHotkeys, FancyZonesJsonContext.Default.LayoutHotkeys);
}
/// <summary>
/// Write layout hotkeys configuration.
/// </summary>
public static void WriteLayoutHotkeys(LayoutHotkeys hotkeys)
{
WriteJsonFile(FancyZonesPaths.LayoutHotkeys, hotkeys, FancyZonesJsonContext.Default.LayoutHotkeys);
}
/// <summary>
/// Check if editor parameters file exists.
/// </summary>
public static bool EditorParametersExist()
{
return File.Exists(FancyZonesPaths.EditorParameters);
}
private static bool TryReadJsonFile<T>(string filePath, JsonTypeInfo<T> jsonTypeInfo, out T result, out string error)
where T : class
{
result = null;
error = null;
Logger.LogDebug($"Reading file: {filePath}");
if (!File.Exists(filePath))
{
error = $"File not found: {Path.GetFileName(filePath)}";
Logger.LogWarning(error);
return false;
}
try
{
var json = File.ReadAllText(filePath);
result = JsonSerializer.Deserialize(json, jsonTypeInfo);
if (result == null)
{
error = $"Failed to parse {Path.GetFileName(filePath)}";
Logger.LogError(error);
return false;
}
Logger.LogDebug($"Successfully read {Path.GetFileName(filePath)}");
return true;
}
catch (JsonException ex)
{
error = $"JSON parse error in {Path.GetFileName(filePath)}: {ex.Message}";
Logger.LogError(error, ex);
return false;
}
catch (IOException ex)
{
error = $"Failed to read {Path.GetFileName(filePath)}: {ex.Message}";
Logger.LogError(error, ex);
return false;
}
}
private static T ReadJsonFileOrDefault<T>(string filePath, JsonTypeInfo<T> jsonTypeInfo, T defaultValue = null)
where T : class
{
if (TryReadJsonFile(filePath, jsonTypeInfo, out var result, out _))
{
return result;
}
return defaultValue;
}
private static void WriteJsonFile<T>(string filePath, T data, JsonTypeInfo<T> jsonTypeInfo)
{
Logger.LogDebug($"Writing file: {filePath}");
var json = JsonSerializer.Serialize(data, jsonTypeInfo);
File.WriteAllText(filePath, json);
Logger.LogInfo($"Successfully wrote {Path.GetFileName(filePath)}");
}
}

View File

@@ -0,0 +1,30 @@
// 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 FancyZonesCLI;
/// <summary>
/// Provides paths to FancyZones configuration files.
/// </summary>
internal static class FancyZonesPaths
{
private static readonly string DataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft",
"PowerToys",
"FancyZones");
public static string AppliedLayouts => Path.Combine(DataPath, "applied-layouts.json");
public static string CustomLayouts => Path.Combine(DataPath, "custom-layouts.json");
public static string LayoutTemplates => Path.Combine(DataPath, "layout-templates.json");
public static string LayoutHotkeys => Path.Combine(DataPath, "layout-hotkeys.json");
public static string EditorParameters => Path.Combine(DataPath, "editor-parameters.json");
}

View File

@@ -0,0 +1,550 @@
// 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.Globalization;
using System.Text;
using System.Text.Json;
namespace FancyZonesCLI;
public static class LayoutVisualizer
{
public static string DrawTemplateLayout(TemplateLayout template)
{
var sb = new StringBuilder();
sb.AppendLine(" Visual Preview:");
switch (template.Type.ToLowerInvariant())
{
case "focus":
sb.Append(RenderFocusLayout(template.ZoneCount > 0 ? template.ZoneCount : 3));
break;
case "columns":
sb.Append(RenderGridLayout(1, template.ZoneCount > 0 ? template.ZoneCount : 3));
break;
case "rows":
sb.Append(RenderGridLayout(template.ZoneCount > 0 ? template.ZoneCount : 3, 1));
break;
case "grid":
// Grid layout: calculate rows and columns from zone count
// Algorithm from GridLayoutModel.InitGrid() - tries to make it close to square
// with cols >= rows preference
int zoneCount = template.ZoneCount > 0 ? template.ZoneCount : 3;
int rows = 1;
while (zoneCount / rows >= rows)
{
rows++;
}
rows--;
int cols = zoneCount / rows;
if (zoneCount % rows != 0)
{
cols++;
}
sb.Append(RenderGridLayoutWithZoneCount(rows, cols, zoneCount));
break;
case "priority-grid":
sb.Append(RenderPriorityGridLayout(template.ZoneCount > 0 ? template.ZoneCount : 3));
break;
case "blank":
sb.AppendLine(" (No zones)");
break;
default:
sb.AppendLine(CultureInfo.InvariantCulture, $" ({template.Type} layout)");
break;
}
return sb.ToString();
}
public static string DrawCustomLayout(CustomLayout layout)
{
if (layout.Info.ValueKind == JsonValueKind.Undefined || layout.Info.ValueKind == JsonValueKind.Null)
{
return string.Empty;
}
var sb = new StringBuilder();
sb.AppendLine(" Visual Preview:");
if (layout.Type == "grid" &&
layout.Info.TryGetProperty("rows", out var rows) &&
layout.Info.TryGetProperty("columns", out var cols))
{
int r = rows.GetInt32();
int c = cols.GetInt32();
// Check if there's a cell-child-map (merged cells)
if (layout.Info.TryGetProperty("cell-child-map", out var cellMap))
{
sb.Append(RenderGridLayoutWithMergedCells(r, c, cellMap));
}
else
{
int height = r >= 4 ? 12 : 8;
sb.Append(RenderGridLayout(r, c, 30, height));
}
}
else if (layout.Type == "canvas" &&
layout.Info.TryGetProperty("zones", out var zones) &&
layout.Info.TryGetProperty("ref-width", out var refWidth) &&
layout.Info.TryGetProperty("ref-height", out var refHeight))
{
sb.Append(RenderCanvasLayout(zones, refWidth.GetInt32(), refHeight.GetInt32()));
}
return sb.ToString();
}
private static string RenderFocusLayout(int zoneCount = 3)
{
var sb = new StringBuilder();
// Focus layout: overlapping zones with cascading offset
if (zoneCount == 1)
{
sb.AppendLine(" +-------+");
sb.AppendLine(" | |");
sb.AppendLine(" | |");
sb.AppendLine(" +-------+");
}
else if (zoneCount == 2)
{
sb.AppendLine(" +-------+");
sb.AppendLine(" | |");
sb.AppendLine(" | +-------+");
sb.AppendLine(" +-| |");
sb.AppendLine(" | |");
sb.AppendLine(" +-------+");
}
else
{
sb.AppendLine(" +-------+");
sb.AppendLine(" | |");
sb.AppendLine(" | +-------+");
sb.AppendLine(" +-| |");
sb.AppendLine(" | +-------+");
sb.AppendLine(" +-| |");
sb.AppendLine(" ...");
sb.AppendLine(CultureInfo.InvariantCulture, $" (total: {zoneCount} zones)");
sb.AppendLine(" ...");
sb.AppendLine(" | +-------+");
sb.AppendLine(" +-| |");
sb.AppendLine(" | |");
sb.AppendLine(" +-------+");
}
return sb.ToString();
}
private static string RenderPriorityGridLayout(int zoneCount = 3)
{
// Priority Grid has predefined layouts for zone counts 1-11
// Data format from GridLayoutModel._priorityData
if (zoneCount >= 1 && zoneCount <= 11)
{
int[,] cellMap = GetPriorityGridCellMap(zoneCount);
return RenderGridLayoutWithCellMap(cellMap);
}
else
{
// > 11 zones: use grid layout
int rows = 1;
while (zoneCount / rows >= rows)
{
rows++;
}
rows--;
int cols = zoneCount / rows;
if (zoneCount % rows != 0)
{
cols++;
}
return RenderGridLayoutWithZoneCount(rows, cols, zoneCount);
}
}
private static int[,] GetPriorityGridCellMap(int zoneCount)
{
// Parsed from Editor's _priorityData byte arrays
return zoneCount switch
{
1 => new int[,] { { 0 } },
2 => new int[,] { { 0, 1 } },
3 => new int[,] { { 0, 1, 2 } },
4 => new int[,] { { 0, 1, 2 }, { 0, 1, 3 } },
5 => new int[,] { { 0, 1, 2 }, { 3, 1, 4 } },
6 => new int[,] { { 0, 1, 2 }, { 0, 1, 3 }, { 4, 1, 5 } },
7 => new int[,] { { 0, 1, 2 }, { 3, 1, 4 }, { 5, 1, 6 } },
8 => new int[,] { { 0, 1, 2, 3 }, { 4, 1, 2, 5 }, { 6, 1, 2, 7 } },
9 => new int[,] { { 0, 1, 2, 3 }, { 4, 1, 2, 5 }, { 6, 1, 7, 8 } },
10 => new int[,] { { 0, 1, 2, 3 }, { 4, 1, 5, 6 }, { 7, 1, 8, 9 } },
11 => new int[,] { { 0, 1, 2, 3 }, { 4, 1, 5, 6 }, { 7, 8, 9, 10 } },
_ => new int[,] { { 0 } },
};
}
private static string RenderGridLayoutWithCellMap(int[,] cellMap, int width = 30, int height = 8)
{
var sb = new StringBuilder();
int rows = cellMap.GetLength(0);
int cols = cellMap.GetLength(1);
int cellWidth = width / cols;
int cellHeight = height / rows;
for (int r = 0; r < rows; r++)
{
// Top border
sb.Append(" +");
for (int c = 0; c < cols; c++)
{
bool mergeTop = r > 0 && cellMap[r, c] == cellMap[r - 1, c];
bool mergeLeft = c > 0 && cellMap[r, c] == cellMap[r, c - 1];
if (mergeTop)
{
sb.Append(mergeLeft ? new string(' ', cellWidth) : new string(' ', cellWidth - 1) + "+");
}
else
{
sb.Append(mergeLeft ? new string('-', cellWidth) : new string('-', cellWidth - 1) + "+");
}
}
sb.AppendLine();
// Cell content
for (int h = 0; h < cellHeight - 1; h++)
{
sb.Append(" ");
for (int c = 0; c < cols; c++)
{
bool mergeLeft = c > 0 && cellMap[r, c] == cellMap[r, c - 1];
sb.Append(mergeLeft ? ' ' : '|');
sb.Append(' ', cellWidth - 1);
}
sb.AppendLine("|");
}
}
// Bottom border
sb.Append(" +");
for (int c = 0; c < cols; c++)
{
sb.Append('-', cellWidth - 1);
sb.Append('+');
}
sb.AppendLine();
return sb.ToString();
}
private static string RenderGridLayoutWithMergedCells(int rows, int cols, JsonElement cellMap)
{
var sb = new StringBuilder();
const int displayWidth = 39;
const int displayHeight = 12;
// Build zone map from cell-child-map
int[,] zoneMap = new int[rows, cols];
for (int r = 0; r < rows; r++)
{
var rowArray = cellMap[r];
for (int c = 0; c < cols; c++)
{
zoneMap[r, c] = rowArray[c].GetInt32();
}
}
int cellHeight = displayHeight / rows;
int cellWidth = displayWidth / cols;
// Draw top border
sb.Append(" +");
sb.Append('-', displayWidth);
sb.AppendLine("+");
// Draw rows
for (int r = 0; r < rows; r++)
{
for (int h = 0; h < cellHeight; h++)
{
sb.Append(" |");
for (int c = 0; c < cols; c++)
{
int currentZone = zoneMap[r, c];
int leftZone = c > 0 ? zoneMap[r, c - 1] : -1;
bool needLeftBorder = c > 0 && currentZone != leftZone;
bool zoneHasTopBorder = r > 0 && h == 0 && currentZone != zoneMap[r - 1, c];
if (needLeftBorder)
{
sb.Append('|');
sb.Append(zoneHasTopBorder ? '-' : ' ', cellWidth - 1);
}
else
{
sb.Append(zoneHasTopBorder ? '-' : ' ', cellWidth);
}
}
sb.AppendLine("|");
}
}
// Draw bottom border
sb.Append(" +");
sb.Append('-', displayWidth);
sb.AppendLine("+");
return sb.ToString();
}
public static string RenderGridLayoutWithZoneCount(int rows, int cols, int zoneCount, int width = 30, int height = 8)
{
var sb = new StringBuilder();
// Build zone map like Editor's InitGrid
int[,] zoneMap = new int[rows, cols];
int index = 0;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
zoneMap[r, c] = index++;
if (index == zoneCount)
{
index--; // Remaining cells use the last zone index
}
}
}
int cellWidth = width / cols;
int cellHeight = height / rows;
for (int r = 0; r < rows; r++)
{
// Top border
sb.Append(" +");
for (int c = 0; c < cols; c++)
{
bool mergeLeft = c > 0 && zoneMap[r, c] == zoneMap[r, c - 1];
sb.Append('-', mergeLeft ? cellWidth : cellWidth - 1);
if (!mergeLeft)
{
sb.Append('+');
}
}
sb.AppendLine();
// Cell content
for (int h = 0; h < cellHeight - 1; h++)
{
sb.Append(" ");
for (int c = 0; c < cols; c++)
{
bool mergeLeft = c > 0 && zoneMap[r, c] == zoneMap[r, c - 1];
sb.Append(mergeLeft ? ' ' : '|');
sb.Append(' ', cellWidth - 1);
}
sb.AppendLine("|");
}
}
// Bottom border
sb.Append(" +");
for (int c = 0; c < cols; c++)
{
sb.Append('-', cellWidth - 1);
sb.Append('+');
}
sb.AppendLine();
return sb.ToString();
}
public static string RenderGridLayout(int rows, int cols, int width = 30, int height = 8)
{
var sb = new StringBuilder();
int cellWidth = width / cols;
int cellHeight = height / rows;
for (int r = 0; r < rows; r++)
{
// Top border
sb.Append(" +");
for (int c = 0; c < cols; c++)
{
sb.Append('-', cellWidth - 1);
sb.Append('+');
}
sb.AppendLine();
// Cell content
for (int h = 0; h < cellHeight - 1; h++)
{
sb.Append(" ");
for (int c = 0; c < cols; c++)
{
sb.Append('|');
sb.Append(' ', cellWidth - 1);
}
sb.AppendLine("|");
}
}
// Bottom border
sb.Append(" +");
for (int c = 0; c < cols; c++)
{
sb.Append('-', cellWidth - 1);
sb.Append('+');
}
sb.AppendLine();
return sb.ToString();
}
private static string RenderCanvasLayout(JsonElement zones, int refWidth, int refHeight)
{
var sb = new StringBuilder();
const int displayWidth = 49;
const int displayHeight = 15;
// Create a 2D array to track which zones occupy each position
var zoneGrid = new List<int>[displayHeight, displayWidth];
for (int i = 0; i < displayHeight; i++)
{
for (int j = 0; j < displayWidth; j++)
{
zoneGrid[i, j] = new List<int>();
}
}
// Map each zone to the grid
int zoneId = 0;
var zoneList = new List<(int X, int Y, int Width, int Height, int Id)>();
foreach (var zone in zones.EnumerateArray())
{
int x = zone.GetProperty("X").GetInt32();
int y = zone.GetProperty("Y").GetInt32();
int w = zone.GetProperty("width").GetInt32();
int h = zone.GetProperty("height").GetInt32();
int dx = Math.Max(0, Math.Min(displayWidth - 1, x * displayWidth / refWidth));
int dy = Math.Max(0, Math.Min(displayHeight - 1, y * displayHeight / refHeight));
int dw = Math.Max(3, w * displayWidth / refWidth);
int dh = Math.Max(2, h * displayHeight / refHeight);
if (dx + dw > displayWidth)
{
dw = displayWidth - dx;
}
if (dy + dh > displayHeight)
{
dh = displayHeight - dy;
}
zoneList.Add((dx, dy, dw, dh, zoneId));
for (int r = dy; r < dy + dh && r < displayHeight; r++)
{
for (int c = dx; c < dx + dw && c < displayWidth; c++)
{
zoneGrid[r, c].Add(zoneId);
}
}
zoneId++;
}
// Draw top border
sb.Append(" +");
sb.Append('-', displayWidth);
sb.AppendLine("+");
// Draw each row
char[] shades = { '.', ':', '░', '▒', '▓', '█', '◆', '●', '■', '▪' };
for (int r = 0; r < displayHeight; r++)
{
sb.Append(" |");
for (int c = 0; c < displayWidth; c++)
{
var zonesHere = zoneGrid[r, c];
if (zonesHere.Count == 0)
{
sb.Append(' ');
}
else
{
int topZone = zonesHere[zonesHere.Count - 1];
var rect = zoneList[topZone];
bool isTopEdge = r == rect.Y;
bool isBottomEdge = r == rect.Y + rect.Height - 1;
bool isLeftEdge = c == rect.X;
bool isRightEdge = c == rect.X + rect.Width - 1;
if ((isTopEdge || isBottomEdge) && (isLeftEdge || isRightEdge))
{
sb.Append('+');
}
else if (isTopEdge || isBottomEdge)
{
sb.Append('-');
}
else if (isLeftEdge || isRightEdge)
{
sb.Append('|');
}
else
{
sb.Append(shades[topZone % shades.Length]);
}
}
}
sb.AppendLine("|");
}
// Draw bottom border
sb.Append(" +");
sb.Append('-', displayWidth);
sb.AppendLine("+");
// Draw legend
sb.AppendLine();
sb.Append(" Legend: ");
for (int i = 0; i < Math.Min(zoneId, shades.Length); i++)
{
if (i > 0)
{
sb.Append(", ");
}
sb.Append(CultureInfo.InvariantCulture, $"Zone {i} = {shades[i]}");
}
sb.AppendLine();
return sb.ToString();
}
}

View File

@@ -0,0 +1,126 @@
// 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.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
namespace FancyZonesCLI;
/// <summary>
/// Simple logger for FancyZones CLI.
/// Logs to %LOCALAPPDATA%\Microsoft\PowerToys\FancyZones\CLI\Logs
/// </summary>
internal static class Logger
{
private static readonly object LockObj = new();
private static string _logFilePath = string.Empty;
private static bool _isInitialized;
/// <summary>
/// Gets the path to the current log file.
/// </summary>
public static string LogFilePath => _logFilePath;
/// <summary>
/// Initializes the logger.
/// </summary>
public static void InitializeLogger()
{
if (_isInitialized)
{
return;
}
try
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var logDirectory = Path.Combine(localAppData, "Microsoft", "PowerToys", "FancyZones", "CLI", "Logs");
if (!Directory.Exists(logDirectory))
{
Directory.CreateDirectory(logDirectory);
}
var logFileName = $"FancyZonesCLI_{DateTime.Now:yyyy-MM-dd}.log";
_logFilePath = Path.Combine(logDirectory, logFileName);
_isInitialized = true;
LogInfo("FancyZones CLI started");
}
catch
{
// Silently fail if logging cannot be initialized
}
}
/// <summary>
/// Logs an error message.
/// </summary>
public static void LogError(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
Log("ERROR", message, memberName, sourceFilePath, sourceLineNumber);
}
/// <summary>
/// Logs an error message with exception details.
/// </summary>
public static void LogError(string message, Exception ex, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
var fullMessage = ex == null
? message
: $"{message} | Exception: {ex.GetType().Name}: {ex.Message}";
Log("ERROR", fullMessage, memberName, sourceFilePath, sourceLineNumber);
}
/// <summary>
/// Logs a warning message.
/// </summary>
public static void LogWarning(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
Log("WARN", message, memberName, sourceFilePath, sourceLineNumber);
}
/// <summary>
/// Logs an informational message.
/// </summary>
public static void LogInfo(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
Log("INFO", message, memberName, sourceFilePath, sourceLineNumber);
}
/// <summary>
/// Logs a debug message (only in DEBUG builds).
/// </summary>
[System.Diagnostics.Conditional("DEBUG")]
public static void LogDebug(string message, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
{
Log("DEBUG", message, memberName, sourceFilePath, sourceLineNumber);
}
private static void Log(string level, string message, string memberName, string sourceFilePath, int sourceLineNumber)
{
if (!_isInitialized || string.IsNullOrEmpty(_logFilePath))
{
return;
}
try
{
var fileName = Path.GetFileName(sourceFilePath);
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);
var logEntry = $"[{timestamp}] [{level}] [{fileName}:{sourceLineNumber}] [{memberName}] {message}{Environment.NewLine}";
lock (LockObj)
{
File.AppendAllText(_logFilePath, logEntry);
}
}
catch
{
// Silently fail if logging fails
}
}
}

View File

@@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace FancyZonesCLI;
// JSON Source Generator for AOT compatibility
[JsonSerializable(typeof(LayoutTemplates))]
[JsonSerializable(typeof(CustomLayouts))]
[JsonSerializable(typeof(AppliedLayouts))]
[JsonSerializable(typeof(LayoutHotkeys))]
[JsonSourceGenerationOptions(WriteIndented = true)]
internal partial class FancyZonesJsonContext : JsonSerializerContext
{
}
// Layout Templates
public sealed class LayoutTemplates
{
[JsonPropertyName("layout-templates")]
public List<TemplateLayout> Templates { get; set; }
}
public sealed class TemplateLayout
{
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("zone-count")]
public int ZoneCount { get; set; }
[JsonPropertyName("show-spacing")]
public bool ShowSpacing { get; set; }
[JsonPropertyName("spacing")]
public int Spacing { get; set; }
[JsonPropertyName("sensitivity-radius")]
public int SensitivityRadius { get; set; }
}
// Custom Layouts
public sealed class CustomLayouts
{
[JsonPropertyName("custom-layouts")]
public List<CustomLayout> Layouts { get; set; }
}
public sealed class CustomLayout
{
[JsonPropertyName("uuid")]
public string Uuid { get; set; } = string.Empty;
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("info")]
public JsonElement Info { get; set; }
}
// Applied Layouts
public sealed class AppliedLayouts
{
[JsonPropertyName("applied-layouts")]
public List<AppliedLayoutWrapper> Layouts { get; set; }
}
public sealed class AppliedLayoutWrapper
{
[JsonPropertyName("device")]
public DeviceInfo Device { get; set; } = new();
[JsonPropertyName("applied-layout")]
public AppliedLayoutInfo AppliedLayout { get; set; } = new();
}
public sealed class DeviceInfo
{
[JsonPropertyName("monitor")]
public string Monitor { get; set; } = string.Empty;
[JsonPropertyName("monitor-instance")]
public string MonitorInstance { get; set; } = string.Empty;
[JsonPropertyName("monitor-number")]
public int MonitorNumber { get; set; }
[JsonPropertyName("serial-number")]
public string SerialNumber { get; set; } = string.Empty;
[JsonPropertyName("virtual-desktop")]
public string VirtualDesktop { get; set; } = string.Empty;
}
public sealed class AppliedLayoutInfo
{
[JsonPropertyName("uuid")]
public string Uuid { get; set; } = string.Empty;
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("show-spacing")]
public bool ShowSpacing { get; set; }
[JsonPropertyName("spacing")]
public int Spacing { get; set; }
[JsonPropertyName("zone-count")]
public int ZoneCount { get; set; }
[JsonPropertyName("sensitivity-radius")]
public int SensitivityRadius { get; set; }
}
// Layout Hotkeys
public sealed class LayoutHotkeys
{
[JsonPropertyName("layout-hotkeys")]
public List<LayoutHotkey> Hotkeys { get; set; }
}
public sealed class LayoutHotkey
{
[JsonPropertyName("key")]
public int Key { get; set; }
[JsonPropertyName("layout-id")]
public string LayoutId { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,56 @@
// 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 Windows.Win32;
using Windows.Win32.Foundation;
namespace FancyZonesCLI;
/// <summary>
/// Native Windows API methods for FancyZones CLI.
/// </summary>
internal static class NativeMethods
{
// Registered Windows messages for notifying FancyZones
private static uint wmPrivAppliedLayoutsFileUpdate;
private static uint wmPrivLayoutHotkeysFileUpdate;
/// <summary>
/// Gets the Windows message ID for applied layouts file update notification.
/// </summary>
public static uint WM_PRIV_APPLIED_LAYOUTS_FILE_UPDATE => wmPrivAppliedLayoutsFileUpdate;
/// <summary>
/// Gets the Windows message ID for layout hotkeys file update notification.
/// </summary>
public static uint WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE => wmPrivLayoutHotkeysFileUpdate;
/// <summary>
/// Initializes the Windows messages used for FancyZones notifications.
/// </summary>
public static void InitializeWindowMessages()
{
wmPrivAppliedLayoutsFileUpdate = PInvoke.RegisterWindowMessage("{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}");
wmPrivLayoutHotkeysFileUpdate = PInvoke.RegisterWindowMessage("{07229b7e-4f22-4357-b136-33c289be2295}");
}
/// <summary>
/// Broadcasts a notification message to FancyZones.
/// </summary>
/// <param name="message">The Windows message ID to broadcast.</param>
public static void NotifyFancyZones(uint message)
{
PInvoke.PostMessage(HWND.HWND_BROADCAST, message, 0, 0);
}
/// <summary>
/// Brings the specified window to the foreground.
/// </summary>
/// <param name="hWnd">A handle to the window.</param>
/// <returns>True if the window was brought to the foreground.</returns>
public static bool SetForegroundWindow(nint hWnd)
{
return PInvoke.SetForegroundWindow(new HWND(hWnd));
}
}

View File

@@ -0,0 +1,5 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"emitSingleFile": true,
"allowMarshaling": false
}

View File

@@ -0,0 +1,4 @@
PostMessage
SetForegroundWindow
RegisterWindowMessage
HWND_BROADCAST

View File

@@ -0,0 +1,115 @@
// 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.Globalization;
using System.Linq;
using FancyZonesCLI.Commands;
namespace FancyZonesCLI;
internal sealed class Program
{
private static int Main(string[] args)
{
// Initialize logger
Logger.InitializeLogger();
Logger.LogInfo($"CLI invoked with args: [{string.Join(", ", args)}]");
// Initialize Windows messages
NativeMethods.InitializeWindowMessages();
(int ExitCode, string Output) result;
if (args.Length == 0)
{
result = (1, GetUsageText());
}
else
{
var command = args[0].ToLowerInvariant();
result = command switch
{
"open-editor" or "editor" or "e" => EditorCommands.OpenEditor(),
"get-monitors" or "monitors" or "m" => MonitorCommands.GetMonitors(),
"get-layouts" or "layouts" or "ls" => LayoutCommands.GetLayouts(),
"get-active-layout" or "active" or "get-active" or "a" => LayoutCommands.GetActiveLayout(),
"set-layout" or "set" or "s" => args.Length >= 2
? LayoutCommands.SetLayout(args.Skip(1).ToArray(), NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_APPLIED_LAYOUTS_FILE_UPDATE)
: (1, "Error: set-layout requires a UUID parameter"),
"open-settings" or "settings" => EditorCommands.OpenSettings(),
"get-hotkeys" or "hotkeys" or "hk" => HotkeyCommands.GetHotkeys(),
"set-hotkey" or "shk" => args.Length >= 3
? HotkeyCommands.SetHotkey(int.Parse(args[1], CultureInfo.InvariantCulture), args[2], NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE)
: (1, "Error: set-hotkey requires <key> <uuid>"),
"remove-hotkey" or "rhk" => args.Length >= 2
? HotkeyCommands.RemoveHotkey(int.Parse(args[1], CultureInfo.InvariantCulture), NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE)
: (1, "Error: remove-hotkey requires <key>"),
"help" or "--help" or "-h" => (0, GetUsageText()),
_ => (1, $"Error: Unknown command: {command}\n\n{GetUsageText()}"),
};
}
// Log result
if (result.ExitCode == 0)
{
Logger.LogInfo($"Command completed successfully");
}
else
{
Logger.LogWarning($"Command failed with exit code {result.ExitCode}: {result.Output}");
}
// Output result
if (!string.IsNullOrEmpty(result.Output))
{
Console.WriteLine(result.Output);
}
return result.ExitCode;
}
private static string GetUsageText()
{
return """
FancyZones CLI - Command line interface for FancyZones
======================================================
Usage: FancyZonesCLI.exe <command> [options]
Commands:
open-editor (editor, e) Launch FancyZones layout editor
get-monitors (monitors, m) List all monitors and their properties
get-layouts (layouts, ls) List all available layouts
get-active-layout (get-active, active, a)
Show currently active layout
set-layout (set, s) <uuid> [options]
Set layout by UUID
--monitor <n> Apply to monitor N (1-based)
--all Apply to all monitors
open-settings (settings) Open FancyZones settings page
get-hotkeys (hotkeys, hk) List all layout hotkeys
set-hotkey (shk) <key> <uuid> Assign hotkey (0-9) to CUSTOM layout
Note: Only custom layouts work with hotkeys
remove-hotkey (rhk) <key> Remove hotkey assignment
help Show this help message
Examples:
FancyZonesCLI.exe e # Open editor (short)
FancyZonesCLI.exe m # List monitors (short)
FancyZonesCLI.exe ls # List layouts (short)
FancyZonesCLI.exe a # Get active layout (short)
FancyZonesCLI.exe s focus --all # Set layout (short)
FancyZonesCLI.exe open-editor # Open editor (long)
FancyZonesCLI.exe get-monitors
FancyZonesCLI.exe get-layouts
FancyZonesCLI.exe set-layout {12345678-1234-1234-1234-123456789012}
FancyZonesCLI.exe set-layout focus --monitor 2
FancyZonesCLI.exe set-layout columns --all
FancyZonesCLI.exe set-hotkey 3 {12345678-1234-1234-1234-123456789012}
""";
}
}

View File

@@ -10,7 +10,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPasteCustomActions
{
private static readonly JsonSerializerOptions _serializerOptions = new()
private static readonly JsonSerializerOptions _serializerOptions = new(SettingsSerializationContext.Default.Options)
{
WriteIndented = true,
};

View File

@@ -104,6 +104,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public PasteAIConfiguration PasteAIConfiguration { get; set; }
public override string ToString()
=> JsonSerializer.Serialize(this);
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.AdvancedPasteProperties);
}
}

View File

@@ -2,13 +2,32 @@
// 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.Json;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// Base class for all PowerToys module settings.
/// </summary>
/// <remarks>
/// <para><strong>IMPORTANT for Native AOT compatibility:</strong></para>
/// <para>When creating a new class that inherits from <see cref="BasePTModuleSettings"/>,
/// you MUST register it in <see cref="SettingsSerializationContext"/> by adding a
/// <c>[JsonSerializable(typeof(YourNewSettingsClass))]</c> attribute.</para>
/// <para>Failure to register the type will cause <see cref="ToJsonString"/> to throw
/// <see cref="InvalidOperationException"/> at runtime.</para>
/// <para>See <see cref="SettingsSerializationContext"/> for registration instructions.</para>
/// </remarks>
public abstract class BasePTModuleSettings
{
// Cached JsonSerializerOptions for Native AOT compatibility
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
{
TypeInfoResolver = SettingsSerializationContext.Default,
};
// Gets or sets name of the powertoy module.
[JsonPropertyName("name")]
public string Name { get; set; }
@@ -17,11 +36,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("version")]
public string Version { get; set; }
// converts the current to a json string.
/// <summary>
/// Converts the current settings object to a JSON string.
/// </summary>
/// <returns>JSON string representation of this settings object.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown when the runtime type is not registered in <see cref="SettingsSerializationContext"/>.
/// All derived types must be registered with <c>[JsonSerializable(typeof(YourType))]</c> attribute.
/// </exception>
/// <remarks>
/// This method uses Native AOT-compatible JSON serialization. The runtime type must be
/// registered in <see cref="SettingsSerializationContext"/> for serialization to work.
/// </remarks>
public virtual string ToJsonString()
{
// By default JsonSerializer will only serialize the properties in the base class. This can be avoided by passing the object type (more details at https://stackoverflow.com/a/62498888)
return JsonSerializer.Serialize(this, GetType());
var runtimeType = GetType();
// For Native AOT compatibility, get JsonTypeInfo from the TypeInfoResolver
var typeInfo = _jsonSerializerOptions.TypeInfoResolver?.GetTypeInfo(runtimeType, _jsonSerializerOptions);
if (typeInfo == null)
{
throw new InvalidOperationException($"Type {runtimeType.FullName} is not registered in SettingsSerializationContext. Please add it to the [JsonSerializable] attributes.");
}
// Use AOT-friendly serialization
return JsonSerializer.Serialize(this, typeInfo);
}
public override int GetHashCode()

View File

@@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public override string ToString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.BoolProperty);
}
public bool TryToCmdRepresentable(out string result)

View File

@@ -12,14 +12,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library
{
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var boolProperty = JsonSerializer.Deserialize<BoolProperty>(ref reader, options);
var boolProperty = JsonSerializer.Deserialize(ref reader, SettingsSerializationContext.Default.BoolProperty);
return boolProperty.Value;
}
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
var boolProperty = new BoolProperty(value);
JsonSerializer.Serialize(writer, boolProperty, options);
JsonSerializer.Serialize(writer, boolProperty, SettingsSerializationContext.Default.BoolProperty);
}
}
}

View File

@@ -43,7 +43,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement))
{
Hotkey = JsonSerializer.Deserialize<HotkeySettings>(hotkeyElement.GetRawText());
Hotkey = JsonSerializer.Deserialize(hotkeyElement.GetRawText(), SettingsSerializationContext.Default.HotkeySettings);
}
}
catch (Exception)

View File

@@ -87,6 +87,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public bool ShowColorName { get; set; }
public override string ToString()
=> JsonSerializer.Serialize(this);
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerProperties);
}
}

View File

@@ -54,6 +54,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public bool ShowColorName { get; set; }
public override string ToString()
=> JsonSerializer.Serialize(this);
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerPropertiesVersion1);
}
}

View File

@@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// Returns a JSON version of the class settings configuration class.
public override string ToString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.DoubleProperty);
}
}
}

View File

@@ -21,7 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.FileLocksmithLocalProperties);
}
// This function is required to implement the ISettingsConfig interface and obtain the settings configurations.

View File

@@ -17,6 +17,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("bool_show_extended_menu")]
public BoolProperty ExtendedContextMenuOnly { get; set; }
public override string ToString() => JsonSerializer.Serialize(this);
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.FileLocksmithProperties);
}
}

View File

@@ -119,7 +119,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// converts the current to a json string.
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettings);
}
private static string DefaultPowertoysVersion()

View File

@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public override string ToString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettingsCustomAction);
}
}
}

View File

@@ -12,13 +12,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
{
public struct SunTimes
{
public int SunriseHour;
public int SunriseMinute;
public int SunsetHour;
public int SunsetMinute;
public string Text;
public int SunriseHour { get; set; }
public bool HasSunrise;
public bool HasSunset;
public int SunriseMinute { get; set; }
public int SunsetHour { get; set; }
public int SunsetMinute { get; set; }
public string Text { get; set; }
public bool HasSunrise { get; set; }
public bool HasSunset { get; set; }
}
}

View File

@@ -86,7 +86,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ImageResizerProperties);
}
}

View File

@@ -37,8 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public override string ToJsonString()
{
var options = _serializerOptions;
return JsonSerializer.Serialize(this, options);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ImageResizerSettings);
}
public string GetModuleName()

View File

@@ -42,7 +42,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// Returns a JSON version of the class settings configuration class.
public override string ToString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.IntProperty);
}
public static implicit operator IntProperty(int v)

View File

@@ -34,7 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.KeyboardManagerProfile);
}
public string GetModuleName()

View File

@@ -46,6 +46,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public IntProperty DefaultMeasureStyle { get; set; }
public override string ToString() => JsonSerializer.Serialize(this);
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.MeasureToolProperties);
}
}

View File

@@ -15,8 +15,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public struct ConnectionRequest
#pragma warning restore SA1649 // File name should match first type name
{
public string PCName;
public string SecurityKey;
public string PCName { get; set; }
public string SecurityKey { get; set; }
}
public struct NewKeyGenerationRequest

View File

@@ -33,6 +33,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("ReplaceVariables")]
public BoolProperty ReplaceVariables { get; set; }
public override string ToString() => JsonSerializer.Serialize(this);
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.NewPlusProperties);
}
}

View File

@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public override string ToString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingGeneralSettings);
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public override string ToString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingLanguageSettings);
}
}
}

View File

@@ -64,7 +64,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public AIServiceType ActiveServiceTypeKind => ActiveProvider?.ServiceTypeKind ?? AIServiceType.OpenAI;
public override string ToString()
=> JsonSerializer.Serialize(this);
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PasteAIConfiguration);
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{

View File

@@ -34,7 +34,7 @@ namespace Settings.UI.Library
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, Microsoft.PowerToys.Settings.UI.Library.SettingsSerializationContext.Default.PeekPreviewSettings);
}
public string GetModuleName()

View File

@@ -32,6 +32,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public BoolProperty EnableSpaceToActivate { get; set; }
public override string ToString() => JsonSerializer.Serialize(this);
public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PeekProperties);
}
}

View File

@@ -24,6 +24,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string PreferredLanguage { get; set; }
public override string ToString()
=> JsonSerializer.Serialize(this);
=> JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PowerOcrProperties);
}
}

View File

@@ -340,7 +340,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public override string ToString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PowerPreviewProperties);
}
private static void LogTelemetryEvent(bool value, [CallerMemberName] string propertyName = null)

View File

@@ -54,7 +54,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public string ToJsonString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PowerRenameLocalProperties);
}
// This function is required to implement the ISettingsConfig interface and obtain the settings configurations.

View File

@@ -0,0 +1,172 @@
// 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.Text.Json.Serialization;
using SettingsUILibrary = Settings.UI.Library;
using SettingsUILibraryHelpers = Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library
{
/// <summary>
/// JSON serialization context for Native AOT compatibility.
/// This context provides source-generated serialization for all PowerToys settings types.
/// </summary>
/// <remarks>
/// <para><strong>⚠️ CRITICAL REQUIREMENT FOR ALL NEW SETTINGS CLASSES ⚠️</strong></para>
/// <para>
/// When adding a new PowerToys module or any class that inherits from <see cref="BasePTModuleSettings"/>,
/// you <strong>MUST</strong> add a <c>[JsonSerializable(typeof(YourNewSettingsClass))]</c> attribute
/// to this class. This is a MANDATORY step for Native AOT compatibility.
/// </para>
/// <para><strong>Steps to add a new settings class:</strong></para>
/// <list type="number">
/// <item><description>Create your new settings class (e.g., <c>MyNewModuleSettings</c>) that inherits from <see cref="BasePTModuleSettings"/></description></item>
/// <item><description>Add <c>[JsonSerializable(typeof(MyNewModuleSettings))]</c> attribute to this <see cref="SettingsSerializationContext"/> class</description></item>
/// <item><description>If you have a corresponding Properties class, also add <c>[JsonSerializable(typeof(MyNewModuleProperties))]</c></description></item>
/// <item><description>Rebuild the project - source generator will create serialization code at compile time</description></item>
/// </list>
/// <para><strong>⚠️ Failure to register types will cause runtime errors:</strong></para>
/// <para>
/// If you forget to add the <c>[JsonSerializable]</c> attribute, calling <c>ToJsonString()</c> or
/// deserialization methods will throw <see cref="InvalidOperationException"/> at runtime with a clear
/// error message indicating which type is missing registration.
/// </para>
/// </remarks>
[JsonSourceGenerationOptions(
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
IncludeFields = true)]
// Main Settings Classes
[JsonSerializable(typeof(GeneralSettings))]
[JsonSerializable(typeof(AdvancedPasteSettings))]
[JsonSerializable(typeof(AlwaysOnTopSettings))]
[JsonSerializable(typeof(AwakeSettings))]
[JsonSerializable(typeof(CmdNotFoundSettings))]
[JsonSerializable(typeof(ColorPickerSettings))]
[JsonSerializable(typeof(ColorPickerSettingsVersion1))]
[JsonSerializable(typeof(CropAndLockSettings))]
[JsonSerializable(typeof(CursorWrapSettings))]
[JsonSerializable(typeof(EnvironmentVariablesSettings))]
[JsonSerializable(typeof(FancyZonesSettings))]
[JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
[JsonSerializable(typeof(HostsSettings))]
[JsonSerializable(typeof(ImageResizerSettings))]
[JsonSerializable(typeof(KeyboardManagerSettings))]
[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
[JsonSerializable(typeof(MouseJumpSettings))]
[JsonSerializable(typeof(MousePointerCrosshairsSettings))]
[JsonSerializable(typeof(MouseWithoutBordersSettings))]
[JsonSerializable(typeof(NewPlusSettings))]
[JsonSerializable(typeof(PeekSettings))]
[JsonSerializable(typeof(PowerAccentSettings))]
[JsonSerializable(typeof(PowerLauncherSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerPreviewSettings))]
[JsonSerializable(typeof(PowerRenameSettings))]
[JsonSerializable(typeof(RegistryPreviewSettings))]
[JsonSerializable(typeof(ShortcutGuideSettings))]
[JsonSerializable(typeof(WorkspacesSettings))]
[JsonSerializable(typeof(ZoomItSettings))]
// Properties Classes
[JsonSerializable(typeof(AdvancedPasteProperties))]
[JsonSerializable(typeof(AlwaysOnTopProperties))]
[JsonSerializable(typeof(AwakeProperties))]
[JsonSerializable(typeof(CmdPalProperties))]
[JsonSerializable(typeof(ColorPickerProperties))]
[JsonSerializable(typeof(ColorPickerPropertiesVersion1))]
[JsonSerializable(typeof(CropAndLockProperties))]
[JsonSerializable(typeof(CursorWrapProperties))]
[JsonSerializable(typeof(EnvironmentVariablesProperties))]
[JsonSerializable(typeof(FileLocksmithProperties))]
[JsonSerializable(typeof(FileLocksmithLocalProperties))]
[JsonSerializable(typeof(FindMyMouseProperties))]
[JsonSerializable(typeof(FZConfigProperties))]
[JsonSerializable(typeof(HostsProperties))]
[JsonSerializable(typeof(ImageResizerProperties))]
[JsonSerializable(typeof(KeyboardManagerProperties))]
[JsonSerializable(typeof(KeyboardManagerProfile))]
[JsonSerializable(typeof(LightSwitchProperties))]
[JsonSerializable(typeof(MeasureToolProperties))]
[JsonSerializable(typeof(MouseHighlighterProperties))]
[JsonSerializable(typeof(MouseJumpProperties))]
[JsonSerializable(typeof(MousePointerCrosshairsProperties))]
[JsonSerializable(typeof(MouseWithoutBordersProperties))]
[JsonSerializable(typeof(NewPlusProperties))]
[JsonSerializable(typeof(PeekProperties))]
[JsonSerializable(typeof(SettingsUILibrary.PeekPreviewSettings))]
[JsonSerializable(typeof(PowerAccentProperties))]
[JsonSerializable(typeof(PowerLauncherProperties))]
[JsonSerializable(typeof(PowerOcrProperties))]
[JsonSerializable(typeof(PowerPreviewProperties))]
[JsonSerializable(typeof(PowerRenameProperties))]
[JsonSerializable(typeof(PowerRenameLocalProperties))]
[JsonSerializable(typeof(RegistryPreviewProperties))]
[JsonSerializable(typeof(ShortcutConflictProperties))]
[JsonSerializable(typeof(ShortcutGuideProperties))]
[JsonSerializable(typeof(WorkspacesProperties))]
[JsonSerializable(typeof(ZoomItProperties))]
// Base Property Types (used throughout settings)
[JsonSerializable(typeof(BoolProperty))]
[JsonSerializable(typeof(StringProperty))]
[JsonSerializable(typeof(IntProperty))]
[JsonSerializable(typeof(DoubleProperty))]
// Helper and Utility Types
[JsonSerializable(typeof(HotkeySettings))]
[JsonSerializable(typeof(ColorFormatModel))]
[JsonSerializable(typeof(ImageSize))]
[JsonSerializable(typeof(KeysDataModel))]
[JsonSerializable(typeof(EnabledModules))]
[JsonSerializable(typeof(GeneralSettingsCustomAction))]
[JsonSerializable(typeof(OutGoingGeneralSettings))]
[JsonSerializable(typeof(OutGoingLanguageSettings))]
[JsonSerializable(typeof(AdvancedPasteCustomActions))]
[JsonSerializable(typeof(AdvancedPasteAdditionalActions))]
[JsonSerializable(typeof(AdvancedPasteCustomAction))]
[JsonSerializable(typeof(AdvancedPasteAdditionalAction))]
[JsonSerializable(typeof(AdvancedPastePasteAsFileAction))]
[JsonSerializable(typeof(AdvancedPasteTranscodeAction))]
[JsonSerializable(typeof(PasteAIConfiguration))]
[JsonSerializable(typeof(PasteAIProviderDefinition))]
[JsonSerializable(typeof(ImageResizerSizes))]
[JsonSerializable(typeof(ImageResizerCustomSizeProperty))]
[JsonSerializable(typeof(KeyboardKeysProperty))]
[JsonSerializable(typeof(SettingsUILibraryHelpers.SearchLocation))]
// IPC Send Message Wrapper Classes (Snd*)
[JsonSerializable(typeof(SndAwakeSettings))]
[JsonSerializable(typeof(SndCursorWrapSettings))]
[JsonSerializable(typeof(SndFindMyMouseSettings))]
[JsonSerializable(typeof(SndLightSwitchSettings))]
[JsonSerializable(typeof(SndMouseHighlighterSettings))]
[JsonSerializable(typeof(SndMouseJumpSettings))]
[JsonSerializable(typeof(SndMousePointerCrosshairsSettings))]
[JsonSerializable(typeof(SndPowerAccentSettings))]
[JsonSerializable(typeof(SndPowerPreviewSettings))]
[JsonSerializable(typeof(SndPowerRenameSettings))]
[JsonSerializable(typeof(SndShortcutGuideSettings))]
// IPC Message Generic Wrapper Types (SndModuleSettings<T>)
[JsonSerializable(typeof(SndModuleSettings<SndAwakeSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndCursorWrapSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndFindMyMouseSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndLightSwitchSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndMouseHighlighterSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndMouseJumpSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndMousePointerCrosshairsSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndPowerAccentSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndPowerPreviewSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndPowerRenameSettings>))]
[JsonSerializable(typeof(SndModuleSettings<SndShortcutGuideSettings>))]
public partial class SettingsSerializationContext : JsonSerializerContext
{
}
}

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable enable
using System;
using System.IO;
using System.IO.Abstractions;
@@ -18,27 +20,28 @@ namespace Microsoft.PowerToys.Settings.UI.Library
private const string DefaultModuleName = "";
private readonly IFile _file;
private readonly ISettingsPath _settingsPath;
private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
{
MaxDepth = 0,
IncludeFields = true,
};
private readonly JsonSerializerOptions _serializerOptions;
public SettingsUtils()
: this(new FileSystem())
{
}
public SettingsUtils(IFileSystem fileSystem)
: this(fileSystem?.File, new SettingPath(fileSystem?.Directory, fileSystem?.Path))
public SettingsUtils(IFileSystem? fileSystem, JsonSerializerOptions? serializerOptions = null)
: this(fileSystem?.File!, new SettingPath(fileSystem?.Directory, fileSystem?.Path), serializerOptions)
{
}
public SettingsUtils(IFile file, ISettingsPath settingPath)
public SettingsUtils(IFile file, ISettingsPath settingPath, JsonSerializerOptions? serializerOptions = null)
{
_file = file ?? throw new ArgumentNullException(nameof(file));
_settingsPath = settingPath;
_serializerOptions = serializerOptions ?? new JsonSerializerOptions
{
MaxDepth = 0,
IncludeFields = true,
TypeInfoResolver = SettingsSerializationContext.Default,
};
}
public bool SettingsExists(string powertoy = DefaultModuleName, string fileName = DefaultFileName)
@@ -108,7 +111,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
/// This function creates a file in the powertoy folder if it does not exist and returns an object with default properties.
/// </summary>
/// <returns>Deserialized json settings object.</returns>
public T GetSettingsOrDefault<T, T2>(string powertoy = DefaultModuleName, string fileName = DefaultFileName, Func<object, object> settingsUpgrader = null)
public T GetSettingsOrDefault<T, T2>(string powertoy = DefaultModuleName, string fileName = DefaultFileName, Func<object, object>? settingsUpgrader = null)
where T : ISettingsConfig, new()
where T2 : ISettingsConfig, new()
{
@@ -128,7 +131,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
try
{
T2 oldSettings = GetSettings<T2>(powertoy, fileName);
T newSettings = (T)settingsUpgrader(oldSettings);
T newSettings = (T)settingsUpgrader!(oldSettings);
Logger.LogInfo($"Settings file {fileName} for {powertoy} was read successfully in the old format.");
// If the file needs to be modified, to save the new configurations accordingly.
@@ -156,7 +159,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return newSettingsItem;
}
// Given the powerToy folder name and filename to be accessed, this function deserializes and returns the file.
/// <summary>
/// Deserializes settings from a JSON file.
/// </summary>
/// <typeparam name="T">The settings type to deserialize. Must be registered in <see cref="SettingsSerializationContext"/>.</typeparam>
/// <param name="powertoyFolderName">The PowerToy module folder name.</param>
/// <param name="fileName">The settings file name.</param>
/// <returns>Deserialized settings object of type T.</returns>
/// <exception cref="InvalidOperationException">
/// Thrown when type T is not registered in <see cref="SettingsSerializationContext"/>.
/// All settings types must be registered with <c>[JsonSerializable(typeof(T))]</c> attribute
/// for Native AOT compatibility.
/// </exception>
/// <remarks>
/// This method uses Native AOT-compatible JSON deserialization. Type T must be registered
/// in <see cref="SettingsSerializationContext"/> before calling this method.
/// </remarks>
private T GetFile<T>(string powertoyFolderName = DefaultModuleName, string fileName = DefaultFileName)
{
// Adding Trim('\0') to overcome possible NTFS file corruption.
@@ -165,8 +183,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// The file itself did write the content correctly but something is off with the actual end of the file, hence the 0x00 bug
var jsonSettingsString = _file.ReadAllText(_settingsPath.GetSettingsPath(powertoyFolderName, fileName)).Trim('\0');
var options = _serializerOptions;
return JsonSerializer.Deserialize<T>(jsonSettingsString, options);
// For Native AOT compatibility, get JsonTypeInfo from the TypeInfoResolver
var typeInfo = _serializerOptions.TypeInfoResolver?.GetTypeInfo(typeof(T), _serializerOptions);
if (typeInfo == null)
{
throw new InvalidOperationException($"Type {typeof(T).FullName} is not registered in SettingsSerializationContext. Please add it to the [JsonSerializable] attributes.");
}
// Use AOT-friendly deserialization
return (T)JsonSerializer.Deserialize(jsonSettingsString, typeInfo)!;
}
// Save settings to a json file.

View File

@@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// Returns a JSON version of the class settings configuration class.
public override string ToString()
{
return JsonSerializer.Serialize(this);
return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.StringProperty);
}
public static StringProperty ToStringProperty(string v)

View File

@@ -0,0 +1,127 @@
// 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.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace CommonLibTest
{
[TestClass]
public class BasePTModuleSettingsSerializationTests
{
/// <summary>
/// Test to verify that all classes derived from BasePTModuleSettings are registered
/// in the SettingsSerializationContext for Native AOT compatibility.
/// </summary>
[TestMethod]
public void AllBasePTModuleSettingsClasses_ShouldBeRegisteredInSerializationContext()
{
// Arrange
var assembly = typeof(BasePTModuleSettings).Assembly;
var settingsClasses = assembly.GetTypes()
.Where(t => typeof(BasePTModuleSettings).IsAssignableFrom(t) && !t.IsAbstract && t != typeof(BasePTModuleSettings))
.OrderBy(t => t.Name)
.ToList();
Assert.IsTrue(settingsClasses.Count > 0, "No BasePTModuleSettings derived classes found. This test may be broken.");
var jsonSerializerOptions = new JsonSerializerOptions
{
TypeInfoResolver = SettingsSerializationContext.Default,
};
var unregisteredTypes = new System.Collections.Generic.List<string>();
// Act & Assert
foreach (var settingsType in settingsClasses)
{
var typeInfo = jsonSerializerOptions.TypeInfoResolver?.GetTypeInfo(settingsType, jsonSerializerOptions);
if (typeInfo == null)
{
unregisteredTypes.Add(settingsType.FullName ?? settingsType.Name);
}
}
// Assert
if (unregisteredTypes.Count > 0)
{
var errorMessage = $"The following {unregisteredTypes.Count} settings class(es) are NOT registered in SettingsSerializationContext:\n" +
$"{string.Join("\n", unregisteredTypes.Select(t => $" - {t}"))}\n\n" +
$"Please add [JsonSerializable(typeof(ClassName))] attribute to SettingsSerializationContext.cs for each missing type.";
Assert.Fail(errorMessage);
}
// Print success message with count
Console.WriteLine($"✓ All {settingsClasses.Count} BasePTModuleSettings derived classes are properly registered in SettingsSerializationContext.");
}
/// <summary>
/// Test to verify that calling ToJsonString() on an unregistered type throws InvalidOperationException
/// with a helpful error message.
/// </summary>
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void ToJsonString_UnregisteredType_ShouldThrowInvalidOperationException()
{
// Arrange
var unregisteredSettings = new UnregisteredTestSettings
{
Name = "UnregisteredModule",
Version = "1.0.0",
};
// Act - This should throw InvalidOperationException
var jsonString = unregisteredSettings.ToJsonString();
// Assert - Exception should be thrown, so this line should never be reached
Assert.Fail("Expected InvalidOperationException was not thrown.");
}
/// <summary>
/// Test to verify that the error message for unregistered types is helpful and contains
/// necessary information for developers.
/// </summary>
[TestMethod]
public void ToJsonString_UnregisteredType_ShouldHaveHelpfulErrorMessage()
{
// Arrange
var unregisteredSettings = new UnregisteredTestSettings
{
Name = "UnregisteredModule",
Version = "1.0.0",
};
// Act & Assert
try
{
var jsonString = unregisteredSettings.ToJsonString();
Assert.Fail("Expected InvalidOperationException was not thrown.");
}
catch (InvalidOperationException ex)
{
// Verify the error message contains helpful information
Assert.IsTrue(ex.Message.Contains("UnregisteredTestSettings"), "Error message should contain the type name.");
Assert.IsTrue(ex.Message.Contains("SettingsSerializationContext"), "Error message should mention SettingsSerializationContext.");
Assert.IsTrue(ex.Message.Contains("JsonSerializable"), "Error message should mention JsonSerializable attribute.");
Console.WriteLine($"✓ Error message is helpful: {ex.Message}");
}
}
/// <summary>
/// Test class that is intentionally NOT registered in SettingsSerializationContext
/// to verify error handling for unregistered types.
/// </summary>
private sealed class UnregisteredTestSettings : BasePTModuleSettings
{
// Intentionally empty - this class should NOT be registered in SettingsSerializationContext
}
}
}

View File

@@ -2,8 +2,10 @@
// 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.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.UnitTests;
namespace Microsoft.PowerToys.Settings.UnitTest
{
@@ -24,5 +26,11 @@ namespace Microsoft.PowerToys.Settings.UnitTest
{
return false;
}
// Override ToJsonString to use test-specific serialization context
public override string ToJsonString()
{
return JsonSerializer.Serialize(this, TestSettingsSerializationContext.Default.BasePTSettingsTest);
}
}
}

View File

@@ -9,6 +9,7 @@ using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.UnitTests;
using Microsoft.PowerToys.Settings.UnitTest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -22,7 +23,13 @@ namespace CommonLibTest
{
// Arrange
var mockFileSystem = new MockFileSystem();
var settingsUtils = new SettingsUtils(mockFileSystem);
var testSerializerOptions = new JsonSerializerOptions
{
MaxDepth = 0,
IncludeFields = true,
TypeInfoResolver = TestSettingsSerializationContext.Default,
};
var settingsUtils = new SettingsUtils(mockFileSystem, testSerializerOptions);
string file_name = "\\test";
string file_contents_correct_json_content = "{\"name\":\"powertoy module name\",\"version\":\"powertoy version\"}";
@@ -42,7 +49,13 @@ namespace CommonLibTest
{
// Arrange
var mockFileSystem = new MockFileSystem();
var settingsUtils = new SettingsUtils(mockFileSystem);
var testSerializerOptions = new JsonSerializerOptions
{
MaxDepth = 0,
IncludeFields = true,
TypeInfoResolver = TestSettingsSerializationContext.Default,
};
var settingsUtils = new SettingsUtils(mockFileSystem, testSerializerOptions);
string file_name = "test\\Test Folder";
string file_contents_correct_json_content = "{\"name\":\"powertoy module name\",\"version\":\"powertoy version\"}";

View File

@@ -0,0 +1,22 @@
// 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.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UnitTest;
namespace Microsoft.PowerToys.Settings.UI.UnitTests
{
/// <summary>
/// JSON serialization context for unit tests.
/// This context provides source-generated serialization for test-specific types.
/// </summary>
[JsonSourceGenerationOptions(
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
IncludeFields = true)]
[JsonSerializable(typeof(BasePTSettingsTest))]
public partial class TestSettingsSerializationContext : JsonSerializerContext
{
}
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility;
using Microsoft.PowerToys.Settings.UI.UnitTests.Mocks;
using Microsoft.PowerToys.Settings.UI.ViewModels;
@@ -100,6 +101,22 @@ namespace ViewModelTests
mockFancyZonesSettingsUtils = ISettingsUtilsMocks.GetStubSettingsUtils<FancyZonesSettings>();
}
[TestCleanup]
public void CleanUp()
{
// Reset singleton instances to prevent state pollution between tests
ResetSettingsRepository<GeneralSettings>();
ResetSettingsRepository<FancyZonesSettings>();
}
private void ResetSettingsRepository<T>()
where T : class, ISettingsConfig, new()
{
var repositoryType = typeof(SettingsRepository<T>);
var field = repositoryType.GetField("settingsRepository", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
field?.SetValue(null, null);
}
[TestMethod]
public void IsEnabledShouldDisableModuleWhenSuccessful()
{

View File

@@ -10,7 +10,7 @@ using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Settings.UI.Library;
using SettingsUILibrary = Settings.UI.Library;
namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(FileLocksmithSettings))]
[JsonSerializable(typeof(FindMyMouseSettings))]
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
[JsonSerializable(typeof(LightSwitchSettings))]
[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))]
[JsonSerializable(typeof(MeasureToolSettings))]
[JsonSerializable(typeof(MouseHighlighterSettings))]
[JsonSerializable(typeof(MouseJumpSettings))]

View File

@@ -12,7 +12,7 @@ Tip: Add `D:\PowerToys\tools\build` to your PATH to use the wrappers anywhere.
## When to use which
1) `build-essentials.ps1`
- Restores NuGet for `PowerToys.sln` and builds essentials (runner, settings).
- Restores NuGet for `PowerToys.slnx` and builds essentials (runner, settings).
- Auto-detects Platform; initializes VS Dev environment automatically.
- Example (PowerShell):
- `./tools/build/build-essentials.ps1`

View File

@@ -113,7 +113,7 @@ function BuildProjectsInDirectory {
$files = @()
try {
$files = Get-ChildItem -Path (Join-Path $DirectoryPath '*') -Include *.sln,*.csproj,*.vcxproj -File -ErrorAction SilentlyContinue
$files = Get-ChildItem -Path (Join-Path $DirectoryPath '*') -Include *.sln,*.slnx,*.csproj,*.vcxproj -File -ErrorAction SilentlyContinue
} catch {
$files = @()
}

View File

@@ -3,7 +3,7 @@
Build essential native PowerToys projects (runner and settings), restoring NuGet packages first.
.DESCRIPTION
Lightweight script to build a small set of essential C++ projects used by PowerToys' runner and native modules. This script first restores NuGet packages for the full solution (`PowerToys.sln`) and then builds the runner and settings projects. Intended for fast local builds during development.
Lightweight script to build a small set of essential C++ projects used by PowerToys' runner and native modules. This script first restores NuGet packages for the full solution (`PowerToys.slnx`) and then builds the runner and settings projects. Intended for fast local builds during development.
.PARAMETER Platform
Target platform for the build (for example: 'x64', 'arm64'). If omitted the script will attempt to auto-detect the host platform.
@@ -21,7 +21,7 @@ Restores packages and builds the essentials in Release mode for ARM64, even if y
.NOTES
- This script dot-sources `build-common.ps1` and uses the shared helper `RunMSBuild`.
- It will call `RestoreThenBuild 'PowerToys.sln'` before building the essential projects to ensure NuGet packages are restored.
- It will call `RestoreThenBuild 'PowerToys.slnx'` before building the essential projects to ensure NuGet packages are restored.
- The script attempts to locate the repository root automatically and can be run from any folder inside the repo.
#>
@@ -33,7 +33,7 @@ param (
# Find repository root starting from the script location
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = $ScriptDir
while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "PowerToys.sln"))) {
while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx"))) {
$parent = Split-Path -Parent $repoRoot
if ($parent -eq $repoRoot) {
Write-Error "Could not find PowerToys repository root."
@@ -63,7 +63,7 @@ if (-not $Platform -or $Platform -eq '') {
}
# Ensure solution packages are restored
RestoreThenBuild 'PowerToys.sln' '' $Platform $Configuration $true
RestoreThenBuild 'PowerToys.slnx' '' $Platform $Configuration $true
# Build both runner and settings
$ProjectsToBuild = @(".\src\runner\runner.vcxproj", ".\src\settings-ui\Settings.UI\PowerToys.Settings.csproj")
@@ -71,4 +71,4 @@ $ExtraArgs = "/p:SolutionDir=$repoRoot\"
foreach ($proj in $ProjectsToBuild) {
Write-Host ("[BUILD-ESSENTIALS] Building {0}" -f $proj)
RunMSBuild $proj $ExtraArgs $Platform $Configuration
}
}

View File

@@ -73,18 +73,18 @@ $repoRoot = $scriptDir
# Navigate up from the script location to find the repo root
# Script is typically in tools\build, so go up two levels
while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "PowerToys.sln"))) {
while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx"))) {
$parentDir = Split-Path -Parent $repoRoot
if ($parentDir -eq $repoRoot) {
# Reached the root of the drive, PowerToys.sln not found
# Reached the root of the drive, PowerToys.slnx not found
Write-Error "Could not find PowerToys repository root. Make sure this script is in the PowerToys repository."
exit 1
}
$repoRoot = $parentDir
}
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "PowerToys.sln"))) {
Write-Error "Could not locate PowerToys.sln. Please ensure this script is run from within the PowerToys repository."
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "PowerToys.slnx"))) {
Write-Error "Could not locate PowerToys.slnx. Please ensure this script is run from within the PowerToys repository."
exit 1
}
@@ -102,7 +102,7 @@ if (Test-Path $cmdpalOutputPath) {
$commonArgs = '/p:CIBuild=true'
# No local projects found (or continuing) - build full solution and tools
RestoreThenBuild 'PowerToys.sln' $commonArgs $Platform $Configuration
RestoreThenBuild 'PowerToys.slnx' $commonArgs $Platform $Configuration
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
@@ -141,10 +141,10 @@ try {
Pop-Location
}
RunMSBuild 'installer\PowerToysSetup.sln' "$commonArgs /t:restore /p:RestorePackagesConfig=true" $Platform $Configuration
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /t:restore /p:RestorePackagesConfig=true" $Platform $Configuration
RunMSBuild 'installer\PowerToysSetup.sln' "$commonArgs /m /t:PowerToysInstallerVNext /p:PerUser=$PerUser" $Platform $Configuration
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysInstallerVNext /p:PerUser=$PerUser" $Platform $Configuration
RunMSBuild 'installer\PowerToysSetup.sln' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration
RunMSBuild 'installer\PowerToysSetup.slnx' "$commonArgs /m /t:PowerToysBootstrapperVNext /p:PerUser=$PerUser" $Platform $Configuration
Write-Host '[PIPELINE] Completed'

View File

@@ -6,7 +6,7 @@
- The template will be available in Visual Studio, when adding a new project, under the `Visual C++` tab.
## Contributing
If you'd like to work on a PowerToy template, make required modifications to `\tools\project_template\ModuleTemplate.vcxproj` and then use the dedicated solution `PowerToyTemplate.sln` to export it as a template. Note that `ModuleTemplate.vcxproj` is actually a project template, therefore uncompilable, so we also have a dedicated `ModuleTemplateCompileTest.vcxproj` project referenced from the `PowerToys.sln` to help keeping the template sources up to date and verify it compiles correctly.
If you'd like to work on a PowerToy template, make required modifications to `\tools\project_template\ModuleTemplate.vcxproj` and then use the dedicated solution `PowerToyTemplate.sln` to export it as a template. Note that `ModuleTemplate.vcxproj` is actually a project template, therefore uncompilable, so we also have a dedicated `ModuleTemplateCompileTest.vcxproj` project referenced from the `PowerToys.slnx` to help keeping the template sources up to date and verify it compiles correctly.
## Create a new PowerToy Module
@@ -442,7 +442,7 @@ void ExamplePowertoy::save_settings() {
## Add a new PowerToy to the Installer
In the `installer` folder, open the `PowerToysSetup.sln` solution.
In the `installer` folder, open the `PowerToysSetup.slnx` solution.
Under the `PowerToysSetup` project, edit `Product.wxs`.
You will need to add a component for your module DLL. Search for `Module_ShortcutGuide` to see where to add the component declaration and where to reference that declaration so the DLL is added to the installer.
Each component requires a newly generated GUID (you can use the Visual Studio integrated tool to generate one).