diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 5a1b6e4ca9..99e14c684a 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -162,6 +162,7 @@ BUILDARCH
BUILDNUMBER
buildtransitive
builttoroam
+BUNDLEINFO
BVal
BValue
byapp
@@ -869,6 +870,7 @@ LOCKTYPE
LOGFONT
LOGFONTW
logon
+LOGMSG
LOGPIXELSX
LOGPIXELSY
lng
@@ -1049,6 +1051,7 @@ MWBEx
MYICON
NAMECHANGE
namespaceanddescendants
+Namotion
nao
NCACTIVATE
ncc
@@ -1086,6 +1089,7 @@ NEWPLUSSHELLEXTENSIONWIN
newrow
nicksnettravels
NIF
+NJson
NLog
NLSTEXT
NMAKE
diff --git a/.gitignore b/.gitignore
index 8859e53742..ed3f80a4ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -350,7 +350,9 @@ src/common/Telemetry/*.etl
# Generated installer file for Monaco source files.
/installer/PowerToysSetup/MonacoSRC.wxs
+/installer/PowerToysSetup/DscResources.wxs
/installer/PowerToysSetupVNext/MonacoSRC.wxs
+/installer/PowerToysSetupVNext/DscResources.wxs
# MSBuildCache
/MSBuildCacheLogs/
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 9cb1fcb7d5..736f1eefd8 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -55,7 +55,6 @@
"PowerToys.Awake.exe",
"PowerToys.Awake.dll",
-
"PowerToys.FancyZonesEditor.exe",
"PowerToys.FancyZonesEditor.dll",
"PowerToys.FancyZonesEditorCommon.dll",
@@ -230,7 +229,10 @@
"PowerToys.CmdPalModuleInterface.dll",
"CmdPalKeyboardService.dll",
- "*Microsoft.CmdPal.UI_*.msix"
+ "*Microsoft.CmdPal.UI_*.msix",
+
+ "PowerToys.DSC.dll",
+ "PowerToys.DSC.exe"
],
"SigningInfo": {
"Operations": [
@@ -297,6 +299,9 @@
"msvcp140_1_app.dll",
"msvcp140_2_app.dll",
"msvcp140_app.dll",
+ "Namotion.Reflection.dll",
+ "NJsonSchema.Annotations.dll",
+ "NJsonSchema.dll",
"vcamp140_app.dll",
"vccorlib140_app.dll",
"vcomp140_app.dll",
@@ -322,6 +327,12 @@
"WinUI3Apps\\ReverseMarkdown.dll",
"WinUI3Apps\\SharpCompress.dll",
"WinUI3Apps\\ZstdSharp.dll",
+ "CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll",
+ "WinUI3Apps\\CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll",
+ "Markdig.dll",
+ "WinUI3Apps\\Markdig.dll",
+ "RomanNumerals.dll",
+ "WinUI3Apps\\RomanNumerals.dll",
"TestableIO.System.IO.Abstractions.dll",
"WinUI3Apps\\TestableIO.System.IO.Abstractions.dll",
"TestableIO.System.IO.Abstractions.Wrappers.dll",
diff --git a/.pipelines/generateDscManifests.ps1 b/.pipelines/generateDscManifests.ps1
new file mode 100644
index 0000000000..109610e62e
--- /dev/null
+++ b/.pipelines/generateDscManifests.ps1
@@ -0,0 +1,88 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildPlatform,
+
+ [Parameter(Mandatory = $true)]
+ [string]$BuildConfiguration,
+
+ [Parameter()]
+ [string]$RepoRoot = (Get-Location).Path
+)
+
+$ErrorActionPreference = 'Stop'
+
+function Resolve-PlatformDirectory {
+ param(
+ [string]$Root,
+ [string]$Platform
+ )
+
+ $normalized = $Platform.Trim()
+ $candidates = @()
+ $candidates += Join-Path $Root $normalized
+ $candidates += Join-Path $Root ($normalized.ToUpperInvariant())
+ $candidates += Join-Path $Root ($normalized.ToLowerInvariant())
+ $candidates = $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
+
+ foreach ($candidate in $candidates) {
+ if (Test-Path $candidate) {
+ return $candidate
+ }
+ }
+
+ return $candidates[0]
+}
+
+Write-Host "Repo root: $RepoRoot"
+Write-Host "Requested build platform: $BuildPlatform"
+Write-Host "Requested configuration: $BuildConfiguration"
+
+# Always use x64 PowerToys.DSC.exe since CI/CD machines are x64
+$exePlatform = 'x64'
+$exeRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $exePlatform
+$exeOutputDir = Join-Path $exeRoot $BuildConfiguration
+$exePath = Join-Path $exeOutputDir 'PowerToys.DSC.exe'
+
+Write-Host "Using x64 PowerToys.DSC.exe to generate DSC manifests for $BuildPlatform build"
+
+if (-not (Test-Path $exePath)) {
+ throw "PowerToys.DSC.exe not found at '$exePath'. Make sure it has been built first."
+}
+
+Write-Host "Using PowerToys.DSC.exe at '$exePath'."
+
+# Output DSC manifests to the target build platform directory (x64, ARM64, etc.)
+$outputRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $BuildPlatform
+if (-not (Test-Path $outputRoot)) {
+ Write-Host "Creating missing platform output root at '$outputRoot'."
+ New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
+}
+
+$outputDir = Join-Path $outputRoot $BuildConfiguration
+if (-not (Test-Path $outputDir)) {
+ Write-Host "Creating missing configuration output directory at '$outputDir'."
+ New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
+}
+
+Write-Host "DSC manifests will be generated to: '$outputDir'"
+
+Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'."
+Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
+
+$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
+Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
+& $exePath @arguments
+if ($LASTEXITCODE -ne 0) {
+ throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
+}
+
+$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
+if ($generatedFiles.Count -eq 0) {
+ throw "No DSC manifest files were generated in '$outputDir'."
+}
+
+Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"
+foreach ($file in $generatedFiles) {
+ Write-Host " - $($file.FullName)"
+}
diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml
index e13792d8d1..45514d4b0f 100644
--- a/.pipelines/v2/release.yml
+++ b/.pipelines/v2/release.yml
@@ -43,11 +43,6 @@ parameters:
displayName: "Build Using Visual Studio Preview"
default: false
- - name: enableAOT
- type: boolean
- displayName: "Enable AOT (Ahead-of-Time) Compilation for CmdPal"
- default: true
-
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
variables:
@@ -109,8 +104,8 @@ extends:
useManagedIdentity: $(SigningUseManagedIdentity)
clientId: $(SigningOriginalClientId)
# Have msbuild use the release nuget config profile
- additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:EnableCmdPalAOT=${{ parameters.enableAOT }} /p:InstallerSuffix=${{ parameters.installerSuffix }}
installerSuffix: ${{ parameters.installerSuffix }}
+ additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:InstallerSuffix=${{ parameters.installerSuffix }} /p:EnableCmdPalAOT=true
beforeBuildSteps:
# Sets versions for all PowerToy created DLLs
- pwsh: |-
diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml
index 89ee99b3e0..6994c7a199 100644
--- a/.pipelines/v2/templates/job-build-project.yml
+++ b/.pipelines/v2/templates/job-build-project.yml
@@ -271,6 +271,23 @@ jobs:
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
+ # Build PowerToys.DSC.exe for ARM64 (x64 uses existing binary from previous build)
+ - task: VSBuild@1
+ displayName: Build PowerToys.DSC.exe (x64 for generating manifests)
+ condition: ne(variables['BuildPlatform'], 'x64')
+ inputs:
+ solution: src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
+ msbuildArgs: /t:Build /m /restore
+ platform: x64
+ configuration: $(BuildConfiguration)
+ msbuildArchitecture: x64
+ maximumCpuCount: true
+
+ # Generate DSC manifests using PowerToys.DSC.exe
+ - pwsh: |-
+ & '.pipelines/generateDscManifests.ps1' -BuildPlatform '$(BuildPlatform)' -BuildConfiguration '$(BuildConfiguration)' -RepoRoot '$(Build.SourcesDirectory)'
+ displayName: Generate DSC manifests
+
- task: CopyFiles@2
displayName: Stage SDK/build
inputs:
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 08bf6febee..eabda4151d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -67,6 +67,7 @@
+
diff --git a/NOTICE.md b/NOTICE.md
index fc9f9b9696..1998ea805a 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1521,6 +1521,7 @@ SOFTWARE.
- ModernWpfUI
- Moq
- MSTest
+- NJsonSchema
- NLog
- NLog.Extensions.Logging
- NLog.Schema
diff --git a/PowerToys.sln b/PowerToys.sln
index 10e6229f32..cd2f4de28d 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -805,6 +805,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v3", "v3", "{9605B84E-FAC4-477B-B9EC-0753177EE6A8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC", "src\dsc\v3\PowerToys.DSC\PowerToys.DSC.csproj", "{94CDC147-6137-45E9-AEDE-17FF809607C0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC.UnitTests", "src\dsc\v3\PowerToys.DSC.UnitTests\PowerToys.DSC.UnitTests.csproj", "{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
@@ -2919,6 +2925,22 @@ Global
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Build.0 = Release|ARM64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.ActiveCfg = Release|x64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Build.0 = Release|x64
+ {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.Build.0 = Debug|ARM64
+ {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.ActiveCfg = Debug|x64
+ {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.Build.0 = Debug|x64
+ {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.ActiveCfg = Release|ARM64
+ {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.Build.0 = Release|ARM64
+ {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.ActiveCfg = Release|x64
+ {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.Build.0 = Release|x64
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.Build.0 = Debug|ARM64
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.ActiveCfg = Debug|x64
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.Build.0 = Debug|x64
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.ActiveCfg = Release|ARM64
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.Build.0 = Release|ARM64
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.ActiveCfg = Release|x64
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.Build.0 = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
@@ -3281,6 +3303,9 @@ Global
{08E71C67-6A7E-4CA1-B04E-2FB336410BAC} = {5B201255-53C8-490B-A34F-01F05D48A477}
{E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1}
+ {9605B84E-FAC4-477B-B9EC-0753177EE6A8} = {557C4636-D7E1-4838-A504-7D19B725EE95}
+ {94CDC147-6137-45E9-AEDE-17FF809607C0} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8}
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8}
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
diff --git a/README.md b/README.md
index cf2dd7beba..631b43d6aa 100644
--- a/README.md
+++ b/README.md
@@ -1,38 +1,56 @@
-# Microsoft PowerToys
+
+
+
+
+
+
+
+ Microsoft PowerToys
+
-
+
+
+Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.
+
-[How to use PowerToys][usingPowerToys-docs-link] | [Downloads & Release notes][github-release-link] | [Contributing to PowerToys](#contributing) | [What's Happening](#whats-happening) | [Roadmap](#powertoys-roadmap)
+| | | |
+|---|---|---|
+| [ Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [ Always on Top](https://aka.ms/PowerToysOverview_AoT) | [ Awake](https://aka.ms/PowerToysOverview_Awake) |
+| [ Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [ Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [ Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
+| [ Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [ Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [ FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
+| [ File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [ File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [ Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
+| [ Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [ Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [ Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
+| [ Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [ New+](https://aka.ms/PowerToysOverview_NewPlus) | [ Peek](https://aka.ms/PowerToysOverview_Peek) |
+| [ PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [ PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [ Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
+| [ Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [ Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [ Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
+| [ Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [ Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [ ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
-## About
-Microsoft PowerToys is a set of utilities for power users to tune and streamline their Windows experience for greater productivity. For more info on [PowerToys overviews and how to use the utilities][usingPowerToys-docs-link], or any other tools and resources for [Windows development environments](https://learn.microsoft.com/windows/dev-environment/overview), head over to [learn.microsoft.com][usingPowerToys-docs-link]!
+## 📋 Installation
-| | Current utilities: | |
-|--------------|--------------------|--------------|
-| [Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) |
-| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
-| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
-| [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
-| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
-| [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) |
-| [Peek](https://aka.ms/PowerToysOverview_Peek) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
-| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
-| [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
-| [ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
+For detailed installation instructions, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
-## Installing and running Microsoft PowerToys
+Before you begin, make sure your device meets the system requirements:
-### Requirements
+> [!NOTE]
+> - Windows 11 or Windows 10 version 2004 (20H1 / build 19041) or newer
+> - 64-bit processor: x64 or ARM64
+> - Latest stable version of [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) is installed via the bootstrapper during setup
-- Windows 11 or Windows 10 version 2004 (code name 20H1 / build number 19041) or newer.
-- x64 or ARM64 processor
-- Our installer will install the following items:
- - [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) bootstrapper. This will install the latest version.
+Choose one of the installation methods below:
-### Via GitHub with EXE [Recommended]
+
+Download .exe from GitHub
-Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
+Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
@@ -49,57 +67,49 @@ Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and cl
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
-This is our preferred method.
+
-### Via Microsoft Store
+
+Microsoft Store
+You can easily install PowerToys from the Microsoft Store:
+
+
+
+
+
+
+
+
-Install from the [Microsoft Store's PowerToys page][microsoft-store-link]. You must be using the [new Microsoft Store](https://blogs.windows.com/windowsExperience/2021/06/24/building-a-new-open-microsoft-store-on-windows-11/), which is available for both Windows 11 and Windows 10.
-### Via WinGet
+
+WinGet
+
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
-#### User scope installer [default]
+*User scope installer [default]*
```powershell
winget install Microsoft.PowerToys -s winget
```
-#### Machine-wide scope installer
-
+*Machine-wide scope installer*
```powershell
winget install --scope machine Microsoft.PowerToys -s winget
```
+
-### Other install methods
+
+Other methods
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
+
-## Third-Party Run Plugins
+## ✨ What's new
+**Version 0.94 (September 2025)**
-There is a collection of [third-party plugins](./doc/thirdPartyRunPlugins.md) created by the community that aren't distributed with PowerToys.
+For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
-## Contributing
-
-This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
-
-We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
-
-Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
-
-For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
-
-## What's Happening
-
-### PowerToys Roadmap
-
-Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
-
-### 0.94 - Sep 2025 Update
-
-In this release, we focused on new features, stability, optimization improvements, and automation.
-
-For an in-depth look at the latest changes, visit the [release blog](https://aka.ms/powertoys-releaseblog).
-
-**✨Highlights**
+**✨ Highlights**
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
@@ -138,13 +148,13 @@ For an in-depth look at the latest changes, visit the [release blog](https://aka
- Allowed providers to override Dispose with a virtual method.
- Fixed memory leaks by cleaning up removed or cancelled list items.
- Sorted DateTime extension results by relevance for better usability.
- - Reduced search text “jiggling” by avoiding redundant change notifications.
+ - Reduced search text "jiggling" by avoiding redundant change notifications.
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Preserved Adaptive Card action types during trimming via DynamicDependency.
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made the extension API easier to evolve without breaking clients.
- - Added “evil” sample pages to help reproduce tricky bugs.
+ - Added "evil" sample pages to help reproduce tricky bugs.
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
@@ -220,10 +230,10 @@ For an in-depth look at the latest changes, visit the [release blog](https://aka
- Rewrote system command tests with a new test base and cleaner patterns.
- Added unit tests for WebSearch and Shell extensions with mockable settings.
- Added unit tests and abstractions for Apps and Bookmarks extensions.
- - Cleans up AI‑generated tests; adds meaningful query tests across extensions.
+ - Cleans up AI-generated tests; adds meaningful query tests across extensions.
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
-### What is being planned over the next few releases
+## 🛣️ Roadmap
For [v0.95][github-next-release-work], we'll work on the items below:
@@ -235,9 +245,19 @@ For [v0.95][github-next-release-work], we'll work on the items below:
- New UI automation tests
- Stability, bug fixes
-## PowerToys Community
+## ❤️ PowerToys Community
-The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn’t be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Month by month, you directly help make PowerToys a better piece of software.
+The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
+
+## Contributing
+
+This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
+
+We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
+
+Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
+
+For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
## Code of Conduct
diff --git a/doc/dsc/Settings.md b/doc/dsc/Settings.md
new file mode 100644
index 0000000000..24fab1e2dd
--- /dev/null
+++ b/doc/dsc/Settings.md
@@ -0,0 +1,83 @@
+# Settings resource
+Manage the settings for PowerToys modules
+
+## Commands
+
+### ✨ Modules
+List all the modules supported by the settings resource.
+```shell
+PS C:\> PowerToys.DSC.exe modules --resource 'settings'
+AdvancedPaste
+AlwaysOnTop
+App
+Awake
+ColorPicker
+CropAndLock
+EnvironmentVariables
+FancyZones
+FileLocksmith
+FindMyMouse
+Hosts
+ImageResizer
+KeyboardManager
+MeasureTool
+MouseHighlighter
+MouseJump
+MousePointerCrosshairs
+Peek
+PowerAccent
+PowerOCR
+PowerRename
+RegistryPreview
+ShortcutGuide
+Workspaces
+ZoomIt
+```
+
+### 📄 Get
+Get the settings for a specific module.
+```shell
+PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables
+{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}}
+```
+
+### 🖨️ Export
+Export the settings for a specific module.
+
+ℹ️ Settings resource Get and Export operation output states are identical.
+```shell
+PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables
+{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}}
+```
+
+### 📝 Set
+Set the settings for a specific module. This command will update the settings to the specified values.
+```shell
+PS C:\> PowerToys.DSC.exe set --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}'
+{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}
+["settings"]
+```
+
+### 🧪 Test
+Test the settings for a specific module. This command will check if the current settings match the desired state.
+```shell
+PS C:\> PowerToys.DSC.exe test --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000002-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}'
+{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"},"_inDesiredState":false}
+["settings"]
+```
+
+### 🛠️ Schema
+Generates the JSON schema for the settings resource of a specific module.
+```shell
+PS C:\> PowerToys.DSC.exe schema --resource 'settings' --module Awake
+{"$schema":"http://json-schema.org/draft-04/schema#","title":"SettingsResourceObjectOfAwakeSettings","type":"object","additionalProperties":false,"required":["settings"],"properties":{"_inDesiredState":{"type":["boolean","null"],"description":"Indicates whether an instance is in the desired state"},"settings":{"description":"The settings content for the module."}}}
+PS E:\src\powertoys> PowerToys.DSC.exe schema --resource 'settings' --module Awake | Format-Json
+```
+
+### 📦 Manifest
+Generates a manifest dsc resource JSON file for the specified module.
+- If the module is not specified, it will generate a manifest for all modules.
+- If the output directory is not specified, it will print the manifest to the console.
+```shell
+PS C:\> PowerToys.DSC.exe manifest --resource settings --module 'Awake' --outputDir "C:\manifests"
+```
\ No newline at end of file
diff --git a/doc/images/icons/Command Palette.png b/doc/images/icons/Command Palette.png
new file mode 100644
index 0000000000..7360fdd113
Binary files /dev/null and b/doc/images/icons/Command Palette.png differ
diff --git a/doc/images/icons/ZoomIt.png b/doc/images/icons/ZoomIt.png
new file mode 100644
index 0000000000..777a30bd1f
Binary files /dev/null and b/doc/images/icons/ZoomIt.png differ
diff --git a/doc/images/overview/PT_hero_image.png b/doc/images/overview/PT_hero_image.png
deleted file mode 100644
index 026a456297..0000000000
Binary files a/doc/images/overview/PT_hero_image.png and /dev/null differ
diff --git a/doc/images/overview/PT_large.png b/doc/images/overview/PT_large.png
deleted file mode 100644
index 340cde5283..0000000000
Binary files a/doc/images/overview/PT_large.png and /dev/null differ
diff --git a/doc/images/overview/PT_small.png b/doc/images/overview/PT_small.png
deleted file mode 100644
index 4c66f43b62..0000000000
Binary files a/doc/images/overview/PT_small.png and /dev/null differ
diff --git a/doc/images/readme/StoreBadge-dark.png b/doc/images/readme/StoreBadge-dark.png
new file mode 100644
index 0000000000..8095159a82
Binary files /dev/null and b/doc/images/readme/StoreBadge-dark.png differ
diff --git a/doc/images/readme/StoreBadge-light.png b/doc/images/readme/StoreBadge-light.png
new file mode 100644
index 0000000000..fc4c9aa8eb
Binary files /dev/null and b/doc/images/readme/StoreBadge-light.png differ
diff --git a/doc/images/readme/pt-hero.dark.png b/doc/images/readme/pt-hero.dark.png
new file mode 100644
index 0000000000..e0ac68155a
Binary files /dev/null and b/doc/images/readme/pt-hero.dark.png differ
diff --git a/doc/images/readme/pt-hero.light.png b/doc/images/readme/pt-hero.light.png
new file mode 100644
index 0000000000..8cdda7b92f
Binary files /dev/null and b/doc/images/readme/pt-hero.light.png differ
diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj
index 6a28fbc896..4a391eb901 100644
--- a/installer/PowerToysSetup/PowerToysInstaller.wixproj
+++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj
@@ -16,6 +16,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
+call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
@@ -26,6 +27,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs"
+call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
@@ -121,6 +123,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
+
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 77ffad8483..c7f9d3bda4 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -75,6 +75,7 @@
+
@@ -324,7 +325,6 @@
BinaryKey="PTCustomActions"
DllEntry="UninstallDSCModuleCA"
/>
-
+
+
+
+
+
+
+"@
+ Set-Content -Path $dscWxsFile -Value $wxsContent
+ exit 0
+}
+
+Write-Host "Found $($dscFiles.Count) DSC manifest file(s)"
+
+$wxsContent = @"
+
+
+
+
+"@
+
+$componentRefs = @()
+foreach ($file in $dscFiles) {
+ $componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_')
+ $fileId = $componentId + "_File"
+ $guid = [System.Guid]::NewGuid().ToString().ToUpper()
+ $componentRefs += $componentId
+
+ $wxsContent += @"
+
+
+
+
+
+
+
+"@
+}
+
+$wxsContent += @"
+
+
+
+
+
+"@
+
+foreach ($componentId in $componentRefs) {
+ $wxsContent += @"
+
+
+"@
+}
+
+$wxsContent += @"
+
+
+
+
+"@
+
+Set-Content -Path $dscWxsFile -Value $wxsContent
+Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'"
diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
index b65b0f38c6..308b304591 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
@@ -3,6 +3,7 @@
#include "RcResource.h"
#include
#include
+#include
#include "../../src/common/logger/logger.h"
#include "../../src/common/utils/gpo.h"
@@ -232,7 +233,9 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
auto action = [&commandLine](HANDLE userToken)
{
- STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL};
+ STARTUPINFO startupInfo = { 0 };
+ startupInfo.cb = sizeof(STARTUPINFO);
+ startupInfo.wShowWindow = SW_SHOWNORMAL;
PROCESS_INFORMATION processInformation;
PVOID lpEnvironment = NULL;
@@ -271,7 +274,9 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
}
else
{
- STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL};
+ STARTUPINFO startupInfo = { 0 };
+ startupInfo.cb = sizeof(STARTUPINFO);
+ startupInfo.wShowWindow = SW_SHOWNORMAL;
PROCESS_INFORMATION processInformation;
@@ -424,7 +429,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
const auto modulesPath = baseModulesPath / L"Microsoft.PowerToys.Configure" / (get_product_version(false) + L".0");
std::error_code errorCode;
- fs::create_directories(modulesPath, errorCode);
+ std::filesystem::create_directories(modulesPath, errorCode);
if (errorCode)
{
hr = E_FAIL;
@@ -433,7 +438,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME})
{
- fs::copy_file(fs::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, fs::copy_options::overwrite_existing, errorCode);
+ std::filesystem::copy_file(std::filesystem::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, std::filesystem::copy_options::overwrite_existing, errorCode);
if (errorCode)
{
@@ -481,7 +486,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME})
{
- fs::remove(versionedModulePath / filename, errorCode);
+ std::filesystem::remove(versionedModulePath / filename, errorCode);
if (errorCode)
{
@@ -492,7 +497,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *modulePath : {&versionedModulePath, &powerToysModulePath})
{
- fs::remove(*modulePath, errorCode);
+ std::filesystem::remove(*modulePath, errorCode);
if (errorCode)
{
@@ -1376,6 +1381,120 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
return WcaFinalize(er);
}
+UINT __stdcall SetBundleInstallLocationCA(MSIHANDLE hInstall)
+{
+ HRESULT hr = S_OK;
+ UINT er = ERROR_SUCCESS;
+
+ // Declare all variables at the beginning to avoid goto issues
+ std::wstring customActionData;
+ std::wstring installationFolder;
+ std::wstring bundleUpgradeCode;
+ std::wstring installScope;
+ bool isPerUser = false;
+ size_t pos1 = std::wstring::npos;
+ size_t pos2 = std::wstring::npos;
+ std::vector keysToTry;
+
+ hr = WcaInitialize(hInstall, "SetBundleInstallLocationCA");
+ ExitOnFailure(hr, "Failed to initialize");
+
+ // Parse CustomActionData: "installFolder;upgradeCode;installScope"
+ hr = getInstallFolder(hInstall, customActionData);
+ ExitOnFailure(hr, "Failed to get CustomActionData.");
+
+ pos1 = customActionData.find(L';');
+ if (pos1 == std::wstring::npos)
+ {
+ hr = E_INVALIDARG;
+ ExitOnFailure(hr, "Invalid CustomActionData format - missing first semicolon");
+ }
+
+ pos2 = customActionData.find(L';', pos1 + 1);
+ if (pos2 == std::wstring::npos)
+ {
+ hr = E_INVALIDARG;
+ ExitOnFailure(hr, "Invalid CustomActionData format - missing second semicolon");
+ }
+
+ installationFolder = customActionData.substr(0, pos1);
+ bundleUpgradeCode = customActionData.substr(pos1 + 1, pos2 - pos1 - 1);
+ installScope = customActionData.substr(pos2 + 1);
+
+ isPerUser = (installScope == L"perUser");
+
+ // Use the appropriate registry based on install scope
+ HKEY targetKey = isPerUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
+ const wchar_t* keyName = isPerUser ? L"HKCU" : L"HKLM";
+
+ WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Searching for Bundle in %ls registry", keyName);
+
+ HKEY uninstallKey;
+ LONG openResult = RegOpenKeyExW(targetKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", 0, KEY_READ | KEY_ENUMERATE_SUB_KEYS, &uninstallKey);
+ if (openResult != ERROR_SUCCESS)
+ {
+ WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to open uninstall key, error: %ld", openResult);
+ goto LExit;
+ }
+
+ DWORD index = 0;
+ wchar_t subKeyName[256];
+ DWORD subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t);
+
+ while (RegEnumKeyExW(uninstallKey, index, subKeyName, &subKeyNameSize, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS)
+ {
+ HKEY productKey;
+ if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ | KEY_WRITE, &productKey) == ERROR_SUCCESS)
+ {
+ wchar_t upgradeCode[256];
+ DWORD upgradeCodeSize = sizeof(upgradeCode);
+ DWORD valueType;
+
+ if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, &valueType,
+ reinterpret_cast(upgradeCode), &upgradeCodeSize) == ERROR_SUCCESS)
+ {
+ // Remove brackets from registry upgradeCode for comparison (bundleUpgradeCode doesn't have brackets)
+ std::wstring regUpgradeCode = upgradeCode;
+ if (!regUpgradeCode.empty() && regUpgradeCode.front() == L'{' && regUpgradeCode.back() == L'}')
+ {
+ regUpgradeCode = regUpgradeCode.substr(1, regUpgradeCode.length() - 2);
+ }
+
+ if (_wcsicmp(regUpgradeCode.c_str(), bundleUpgradeCode.c_str()) == 0)
+ {
+ // Found matching Bundle, set InstallLocation
+ LONG setResult = RegSetValueExW(productKey, L"InstallLocation", 0, REG_SZ,
+ reinterpret_cast(installationFolder.c_str()),
+ static_cast((installationFolder.length() + 1) * sizeof(wchar_t)));
+
+ if (setResult == ERROR_SUCCESS)
+ {
+ WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: InstallLocation set successfully");
+ }
+ else
+ {
+ WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to set InstallLocation, error: %ld", setResult);
+ }
+
+ RegCloseKey(productKey);
+ RegCloseKey(uninstallKey);
+ goto LExit;
+ }
+ }
+ RegCloseKey(productKey);
+ }
+
+ index++;
+ subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t);
+ }
+
+ RegCloseKey(uninstallKey);
+
+LExit:
+ er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
+ return WcaFinalize(er);
+}
+
void initSystemLogger()
{
static std::once_flag initLoggerFlag;
diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
index 39efc9ff70..931a555953 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
@@ -32,4 +32,4 @@ EXPORTS
CleanFileLocksmithRuntimeRegistryCA
CleanPowerRenameRuntimeRegistryCA
CleanNewPlusRuntimeRegistryCA
-
\ No newline at end of file
+ SetBundleInstallLocationCA
diff --git a/installer/PowerToysSetupVNext/Core.wxs b/installer/PowerToysSetupVNext/Core.wxs
index d3f992d82e..f7da6162f9 100644
--- a/installer/PowerToysSetupVNext/Core.wxs
+++ b/installer/PowerToysSetupVNext/Core.wxs
@@ -9,6 +9,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -109,6 +128,11 @@
+
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/PowerToys.wxs b/installer/PowerToysSetupVNext/PowerToys.wxs
index 19906089bf..64f6f35c5e 100644
--- a/installer/PowerToysSetupVNext/PowerToys.wxs
+++ b/installer/PowerToysSetupVNext/PowerToys.wxs
@@ -28,6 +28,9 @@
+
+
+
@@ -58,6 +61,7 @@
+
diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
index c70c3d9622..5341f66768 100644
--- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
+++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
@@ -14,6 +14,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
+call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
@@ -24,6 +25,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
+call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
@@ -117,6 +119,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
+
diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs
index 7a0aa41132..2505557d77 100644
--- a/installer/PowerToysSetupVNext/Product.wxs
+++ b/installer/PowerToysSetupVNext/Product.wxs
@@ -63,6 +63,7 @@
+
@@ -70,8 +71,8 @@
-
-
+
+
@@ -118,6 +119,8 @@
+
+
@@ -161,6 +164,9 @@
+
+
+
@@ -245,6 +251,8 @@
+
+
diff --git a/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1 b/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1
new file mode 100644
index 0000000000..14172db0bc
--- /dev/null
+++ b/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1
@@ -0,0 +1,102 @@
+[CmdletBinding()]
+Param(
+ [Parameter(Mandatory = $True)]
+ [string]$dscWxsFile,
+ [Parameter(Mandatory = $True)]
+ [string]$Platform,
+ [Parameter(Mandatory = $True)]
+ [string]$Configuration
+)
+
+$ErrorActionPreference = 'Stop'
+
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+
+# Find build output directory
+$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration"
+
+if (-not (Test-Path $buildOutputDir)) {
+ Write-Error "Build output directory not found: '$buildOutputDir'"
+ exit 1
+}
+
+# Find all DSC manifest JSON files
+$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File
+
+if (-not $dscFiles) {
+ Write-Warning "No DSC manifest files found in '$buildOutputDir'"
+ # Create empty component group
+ $wxsContent = @"
+
+
+
+
+
+
+
+
+
+"@
+ Set-Content -Path $dscWxsFile -Value $wxsContent
+ exit 0
+}
+
+Write-Host "Found $($dscFiles.Count) DSC manifest file(s)"
+
+# Generate WiX fragment
+$wxsContent = @"
+
+
+
+
+
+
+"@
+
+$componentRefs = @()
+
+foreach ($file in $dscFiles) {
+ $componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_')
+ $fileId = $componentId + "_File"
+ $guid = [System.Guid]::NewGuid().ToString().ToUpper()
+
+ $componentRefs += $componentId
+
+ $wxsContent += @"
+
+
+
+
+
+
+
+"@
+}
+
+$wxsContent += @"
+
+
+
+
+
+
+"@
+
+foreach ($componentId in $componentRefs) {
+ $wxsContent += @"
+
+
+"@
+}
+
+$wxsContent += @"
+
+
+
+
+"@
+
+# Write the WiX file
+Set-Content -Path $dscWxsFile -Value $wxsContent
+
+Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'"
\ No newline at end of file
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/BaseDscTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/BaseDscTest.cs
new file mode 100644
index 0000000000..2eda4bdac5
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/BaseDscTest.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.CommandLine;
+using System.CommandLine.Parsing;
+using System.Globalization;
+using System.IO;
+using System.Resources;
+using PowerToys.DSC.UnitTests.Models;
+
+namespace PowerToys.DSC.UnitTests;
+
+public class BaseDscTest
+{
+ private readonly ResourceManager _resourceManager;
+
+ public BaseDscTest()
+ {
+ _resourceManager = new ResourceManager("PowerToys.DSC.Properties.Resources", typeof(PowerToys.DSC.Program).Assembly);
+ }
+
+ ///
+ /// Returns the string resource for the given name, formatted with the provided arguments.
+ ///
+ /// The name of the resource string.
+ /// The arguments to format the resource string with.
+ ///
+ public string GetResourceString(string name, params string[] args)
+ {
+ return string.Format(CultureInfo.InvariantCulture, _resourceManager.GetString(name, CultureInfo.InvariantCulture), args);
+ }
+
+ ///
+ /// Execute a dsc command with the provided arguments.
+ ///
+ ///
+ ///
+ ///
+ protected DscExecuteResult ExecuteDscCommand(params string[] args)
+ where T : Command, new()
+ {
+ var originalOut = Console.Out;
+ var originalErr = Console.Error;
+
+ var outSw = new StringWriter();
+ var errSw = new StringWriter();
+
+ try
+ {
+ Console.SetOut(outSw);
+ Console.SetError(errSw);
+
+ var executeResult = new T().Invoke(args);
+ var output = outSw.ToString();
+ var errorOutput = errSw.ToString();
+ return new(executeResult == 0, output, errorOutput);
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ Console.SetError(originalErr);
+ outSw.Dispose();
+ errSw.Dispose();
+ }
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/CommandTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/CommandTest.cs
new file mode 100644
index 0000000000..0941c03fdf
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/CommandTest.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using PowerToys.DSC.Commands;
+using PowerToys.DSC.DSCResources;
+
+namespace PowerToys.DSC.UnitTests;
+
+[TestClass]
+public sealed class CommandTest : BaseDscTest
+{
+ [TestMethod]
+ public void GetResource_Found_Success()
+ {
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName);
+
+ // Assert
+ Assert.IsTrue(result.Success);
+ }
+
+ [TestMethod]
+ public void GetResource_NotFound_Fail()
+ {
+ // Arrange
+ var availableResources = string.Join(", ", BaseCommand.AvailableResources);
+
+ // Act
+ var result = ExecuteDscCommand("--resource", "ResourceNotFound");
+
+ // Assert
+ Assert.IsFalse(result.Success);
+ Assert.Contains(GetResourceString("InvalidResourceNameError", availableResources), result.Error);
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/Models/DscExecuteResult.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/Models/DscExecuteResult.cs
new file mode 100644
index 0000000000..7bf79f1041
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/Models/DscExecuteResult.cs
@@ -0,0 +1,103 @@
+// 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.Diagnostics;
+using System.Linq;
+using System.Text.Json;
+using PowerToys.DSC.Models;
+
+namespace PowerToys.DSC.UnitTests.Models;
+
+///
+/// Result of executing a DSC command.
+///
+public class DscExecuteResult
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Value indicating whether the command execution was successful.
+ /// Output stream content.
+ /// Error stream content.
+ public DscExecuteResult(bool success, string output, string error)
+ {
+ Success = success;
+ Output = output;
+ Error = error;
+ }
+
+ ///
+ /// Gets a value indicating whether the command execution was successful.
+ ///
+ public bool Success { get; }
+
+ ///
+ /// Gets the output stream content of the operation.
+ ///
+ public string Output { get; }
+
+ ///
+ /// Gets the error stream content of the operation.
+ ///
+ public string Error { get; }
+
+ ///
+ /// Gets the messages from the error stream.
+ ///
+ /// List of messages with their levels.
+ public List<(DscMessageLevel Level, string Message)> Messages()
+ {
+ var lines = Error.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
+ return lines.SelectMany(line =>
+ {
+ var map = JsonSerializer.Deserialize>(line);
+ return map.Select(v => (GetMessageLevel(v.Key), v.Value)).ToList();
+ }).ToList();
+ }
+
+ ///
+ /// Gets the output as state.
+ ///
+ /// State.
+ public T OutputState()
+ {
+ var lines = Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
+ Debug.Assert(lines.Length == 1, "Output should contain exactly one line.");
+ return JsonSerializer.Deserialize(lines[0]);
+ }
+
+ ///
+ /// Gets the output as state and diff.
+ ///
+ /// State and diff.
+ public (T State, List Diff) OutputStateAndDiff()
+ {
+ var lines = Output.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
+ Debug.Assert(lines.Length == 2, "Output should contain exactly two lines.");
+ var obj = JsonSerializer.Deserialize(lines[0]);
+ var diff = JsonSerializer.Deserialize>(lines[1]);
+ return (obj, diff);
+ }
+
+ ///
+ /// Gets the message level from a string representation.
+ ///
+ /// The string representation of the message level.
+ /// The level as .
+ /// Thrown when the level is unknown.
+ private DscMessageLevel GetMessageLevel(string level)
+ {
+ return level switch
+ {
+ "error" => DscMessageLevel.Error,
+ "warn" => DscMessageLevel.Warning,
+ "info" => DscMessageLevel.Info,
+ "debug" => DscMessageLevel.Debug,
+ "trace" => DscMessageLevel.Trace,
+ _ => throw new ArgumentOutOfRangeException(nameof(level), level, "Unknown message level"),
+ };
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/PowerToys.DSC.UnitTests.csproj b/src/dsc/v3/PowerToys.DSC.UnitTests/PowerToys.DSC.UnitTests.csproj
new file mode 100644
index 0000000000..d7a8c8c2f8
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/PowerToys.DSC.UnitTests.csproj
@@ -0,0 +1,17 @@
+
+
+
+
+
+ false
+ ..\..\..\..\$(Configuration)\$(Platform)\tests\PowerToys.DSC.Tests\
+
+
+
+
+
+
+
+
+
+
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs
new file mode 100644
index 0000000000..deae2eb832
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
+
+[TestClass]
+public sealed class SettingsResourceAdvancedPasteModuleTest : SettingsResourceModuleTest
+{
+ public SettingsResourceAdvancedPasteModuleTest()
+ : base(nameof(ModuleType.AdvancedPaste))
+ {
+ }
+
+ protected override Action GetSettingsModifier()
+ {
+ return s =>
+ {
+ s.Properties.ShowCustomPreview = !s.Properties.ShowCustomPreview;
+ s.Properties.CloseAfterLosingFocus = !s.Properties.CloseAfterLosingFocus;
+ s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled;
+ s.Properties.AdvancedPasteUIShortcut = new HotkeySettings
+ {
+ Key = "mock",
+ Alt = true,
+ };
+ };
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAlwaysOnTopModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAlwaysOnTopModuleTest.cs
new file mode 100644
index 0000000000..5aeb10b27e
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAlwaysOnTopModuleTest.cs
@@ -0,0 +1,28 @@
+// 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 ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
+
+[TestClass]
+public sealed class SettingsResourceAlwaysOnTopModuleTest : SettingsResourceModuleTest
+{
+ public SettingsResourceAlwaysOnTopModuleTest()
+ : base(nameof(ModuleType.AlwaysOnTop))
+ {
+ }
+
+ protected override Action GetSettingsModifier()
+ {
+ return s =>
+ {
+ s.Properties.RoundCornersEnabled.Value = !s.Properties.RoundCornersEnabled.Value;
+ s.Properties.FrameEnabled.Value = !s.Properties.FrameEnabled.Value;
+ };
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAppModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAppModuleTest.cs
new file mode 100644
index 0000000000..b49563e100
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAppModuleTest.cs
@@ -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 Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using PowerToys.DSC.DSCResources;
+
+namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
+
+[TestClass]
+public sealed class SettingsResourceAppModuleTest : SettingsResourceModuleTest
+{
+ public SettingsResourceAppModuleTest()
+ : base(SettingsResource.AppModule)
+ {
+ }
+
+ protected override Action GetSettingsModifier()
+ {
+ return s =>
+ {
+ s.Startup = !s.Startup;
+ s.ShowSysTrayIcon = !s.ShowSysTrayIcon;
+ s.Enabled.Awake = !s.Enabled.Awake;
+ s.Enabled.ColorPicker = !s.Enabled.ColorPicker;
+ };
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAwakeModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAwakeModuleTest.cs
new file mode 100644
index 0000000000..bd5e60c371
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAwakeModuleTest.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
+
+[TestClass]
+public sealed class SettingsResourceAwakeModuleTest : SettingsResourceModuleTest
+{
+ public SettingsResourceAwakeModuleTest()
+ : base(nameof(ModuleType.Awake))
+ {
+ }
+
+ protected override Action GetSettingsModifier()
+ {
+ return s =>
+ {
+ s.Properties.ExpirationDateTime = DateTimeOffset.MinValue;
+ s.Properties.IntervalHours = DefaultSettings.Properties.IntervalHours + 1;
+ s.Properties.IntervalMinutes = DefaultSettings.Properties.IntervalMinutes + 1;
+ s.Properties.Mode = s.Properties.Mode == AwakeMode.PASSIVE ? AwakeMode.TIMED : AwakeMode.PASSIVE;
+ s.Properties.KeepDisplayOn = !s.Properties.KeepDisplayOn;
+ s.Properties.CustomTrayTimes = new Dictionary
+ {
+ { "08:00", 1 },
+ { "12:00", 2 },
+ { "16:00", 3 },
+ };
+ };
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceColorPickerModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceColorPickerModuleTest.cs
new file mode 100644
index 0000000000..175b74623c
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceColorPickerModuleTest.cs
@@ -0,0 +1,28 @@
+// 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 ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
+
+[TestClass]
+public sealed class SettingsResourceColorPickerModuleTest : SettingsResourceModuleTest
+{
+ public SettingsResourceColorPickerModuleTest()
+ : base(nameof(ModuleType.ColorPicker))
+ {
+ }
+
+ protected override Action GetSettingsModifier()
+ {
+ return s =>
+ {
+ s.Properties.ShowColorName = !s.Properties.ShowColorName;
+ s.Properties.ColorHistoryLimit = s.Properties.ColorHistoryLimit == 0 ? 10 : 0;
+ };
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCommandTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCommandTest.cs
new file mode 100644
index 0000000000..5333f5a832
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCommandTest.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ManagedCommon;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using PowerToys.DSC.Commands;
+using PowerToys.DSC.DSCResources;
+using PowerToys.DSC.Models;
+
+namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
+
+[TestClass]
+public sealed class SettingsResourceCommandTest : BaseDscTest
+{
+ [TestMethod]
+ public void Modules_ListAllSupportedModules()
+ {
+ // Arrange
+ var expectedModules = new List()
+ {
+ SettingsResource.AppModule,
+ nameof(ModuleType.AdvancedPaste),
+ nameof(ModuleType.AlwaysOnTop),
+ nameof(ModuleType.Awake),
+ nameof(ModuleType.ColorPicker),
+ nameof(ModuleType.CropAndLock),
+ nameof(ModuleType.EnvironmentVariables),
+ nameof(ModuleType.FancyZones),
+ nameof(ModuleType.FileLocksmith),
+ nameof(ModuleType.FindMyMouse),
+ nameof(ModuleType.Hosts),
+ nameof(ModuleType.ImageResizer),
+ nameof(ModuleType.KeyboardManager),
+ nameof(ModuleType.MouseHighlighter),
+ nameof(ModuleType.MouseJump),
+ nameof(ModuleType.MousePointerCrosshairs),
+ nameof(ModuleType.Peek),
+ nameof(ModuleType.PowerRename),
+ nameof(ModuleType.PowerAccent),
+ nameof(ModuleType.RegistryPreview),
+ nameof(ModuleType.MeasureTool),
+ nameof(ModuleType.ShortcutGuide),
+ nameof(ModuleType.PowerOCR),
+ nameof(ModuleType.Workspaces),
+ nameof(ModuleType.ZoomIt),
+ };
+
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName);
+
+ // Assert
+ Assert.IsTrue(result.Success);
+ Assert.AreEqual(string.Join(Environment.NewLine, expectedModules.Order()), result.Output.Trim());
+ }
+
+ [TestMethod]
+ public void Set_EmptyInput_Fail()
+ {
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", "Awake");
+ var messages = result.Messages();
+
+ // Assert
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(1, messages.Count);
+ Assert.AreEqual(DscMessageLevel.Error, messages[0].Level);
+ Assert.AreEqual(GetResourceString("InputEmptyOrNullError"), messages[0].Message);
+ }
+
+ [TestMethod]
+ public void Test_EmptyInput_Fail()
+ {
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", "Awake");
+ var messages = result.Messages();
+
+ // Assert
+ Assert.IsFalse(result.Success);
+ Assert.AreEqual(1, messages.Count);
+ Assert.AreEqual(DscMessageLevel.Error, messages[0].Level);
+ Assert.AreEqual(GetResourceString("InputEmptyOrNullError"), messages[0].Message);
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCropAndLockModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCropAndLockModuleTest.cs
new file mode 100644
index 0000000000..516a5fac86
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceCropAndLockModuleTest.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
+
+[TestClass]
+public sealed class SettingsResourceCropAndLockModuleTest : SettingsResourceModuleTest
+{
+ public SettingsResourceCropAndLockModuleTest()
+ : base(nameof(ModuleType.CropAndLock))
+ {
+ }
+
+ protected override Action GetSettingsModifier()
+ {
+ return s =>
+ {
+ s.Properties.ThumbnailHotkey = new KeyboardKeysProperty()
+ {
+ Value = new HotkeySettings
+ {
+ Key = "mock",
+ Alt = true,
+ },
+ };
+ };
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs
new file mode 100644
index 0000000000..ad7eb1d200
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceModuleTest`1.cs
@@ -0,0 +1,267 @@
+// 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.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using PowerToys.DSC.Commands;
+using PowerToys.DSC.DSCResources;
+using PowerToys.DSC.Models.ResourceObjects;
+
+namespace PowerToys.DSC.UnitTests.SettingsResourceTests;
+
+public abstract class SettingsResourceModuleTest : BaseDscTest
+ where TSettingsConfig : ISettingsConfig, new()
+{
+ private readonly SettingsUtils _settingsUtils = new();
+ private TSettingsConfig _originalSettings;
+
+ protected TSettingsConfig DefaultSettings => new();
+
+ protected string Module { get; }
+
+ protected List DiffSettings { get; } = [SettingsResourceObject.SettingsJsonPropertyName];
+
+ protected List DiffEmpty { get; } = [];
+
+ public SettingsResourceModuleTest(string module)
+ {
+ Module = module;
+ }
+
+ [TestInitialize]
+ public void TestInitialize()
+ {
+ _originalSettings = GetSettings();
+ ResetSettingsToDefaultValues();
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ SaveSettings(_originalSettings);
+ }
+
+ [TestMethod]
+ public void Get_Success()
+ {
+ // Arrange
+ var settingsBeforeExecute = GetSettings();
+
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module);
+ var state = result.OutputState>();
+
+ // Assert
+ Assert.IsTrue(result.Success);
+ AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
+ AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
+ }
+
+ [TestMethod]
+ public void Export_Success()
+ {
+ // Arrange
+ var settingsBeforeExecute = GetSettings();
+
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module);
+ var state = result.OutputState>();
+
+ // Assert
+ Assert.IsTrue(result.Success);
+ AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
+ AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
+ }
+
+ [TestMethod]
+ public void SetWithDiff_Success()
+ {
+ // Arrange
+ var settingsModifier = GetSettingsModifier();
+ var input = CreateInputResourceObject(settingsModifier);
+
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
+ var (state, diff) = result.OutputStateAndDiff>();
+
+ // Assert
+ Assert.IsTrue(result.Success);
+ AssertSettingsHasChanged(settingsModifier);
+ AssertStateAndSettingsAreEqual(GetSettings(), state);
+ CollectionAssert.AreEqual(DiffSettings, diff);
+ }
+
+ [TestMethod]
+ public void SetWithoutDiff_Success()
+ {
+ // Arrange
+ var settingsModifier = GetSettingsModifier();
+ UpdateSettings(settingsModifier);
+ var settingsBeforeExecute = GetSettings();
+ var input = CreateInputResourceObject(settingsModifier);
+
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
+ var (state, diff) = result.OutputStateAndDiff>();
+
+ // Assert
+ Assert.IsTrue(result.Success);
+ AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
+ AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
+ CollectionAssert.AreEqual(DiffEmpty, diff);
+ }
+
+ [TestMethod]
+ public void TestWithDiff_Success()
+ {
+ // Arrange
+ var settingsModifier = GetSettingsModifier();
+ var settingsBeforeExecute = GetSettings();
+ var input = CreateInputResourceObject(settingsModifier);
+
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
+ var (state, diff) = result.OutputStateAndDiff>();
+
+ // Assert
+ Assert.IsTrue(result.Success);
+ AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
+ AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
+ CollectionAssert.AreEqual(DiffSettings, diff);
+ Assert.IsFalse(state.InDesiredState);
+ }
+
+ [TestMethod]
+ public void TestWithoutDiff_Success()
+ {
+ // Arrange
+ var settingsModifier = GetSettingsModifier();
+ UpdateSettings(settingsModifier);
+ var settingsBeforeExecute = GetSettings();
+ var input = CreateInputResourceObject(settingsModifier);
+
+ // Act
+ var result = ExecuteDscCommand("--resource", SettingsResource.ResourceName, "--module", Module, "--input", input);
+ var (state, diff) = result.OutputStateAndDiff>();
+
+ // Assert
+ Assert.IsTrue(result.Success);
+ AssertSettingsAreEqual(settingsBeforeExecute, GetSettings());
+ AssertStateAndSettingsAreEqual(settingsBeforeExecute, state);
+ CollectionAssert.AreEqual(DiffEmpty, diff);
+ Assert.IsTrue(state.InDesiredState);
+ }
+
+ ///
+ /// Gets the settings modifier action for the specific settings configuration.
+ ///
+ /// An action that modifies the settings configuration.
+ protected abstract Action GetSettingsModifier();
+
+ ///
+ /// Resets the settings to default values.
+ ///
+ private void ResetSettingsToDefaultValues()
+ {
+ SaveSettings(DefaultSettings);
+ }
+
+ ///
+ /// Get the settings for the specified module.
+ ///
+ /// An instance of the settings type with the current configuration.
+ private TSettingsConfig GetSettings()
+ {
+ return _settingsUtils.GetSettingsOrDefault(DefaultSettings.GetModuleName());
+ }
+
+ ///
+ /// Saves the settings for the specified module.
+ ///
+ /// Settings to save.
+ private void SaveSettings(TSettingsConfig settings)
+ {
+ _settingsUtils.SaveSettings(JsonSerializer.Serialize(settings), DefaultSettings.GetModuleName());
+ }
+
+ ///
+ /// Create the resource object for the operation.
+ ///
+ /// Settings to include in the resource object.
+ /// A JSON string representing the resource object.
+ private string CreateResourceObject(TSettingsConfig settings)
+ {
+ var resourceObject = new SettingsResourceObject
+ {
+ Settings = settings,
+ };
+ return JsonSerializer.Serialize(resourceObject);
+ }
+
+ private string CreateInputResourceObject(Action settingsModifier)
+ {
+ var settings = DefaultSettings;
+ settingsModifier(settings);
+ return CreateResourceObject(settings);
+ }
+
+ ///
+ /// Create the response for the Get operation.
+ ///
+ /// A JSON string representing the response.
+ private string CreateGetResponse()
+ {
+ return CreateResourceObject(GetSettings());
+ }
+
+ ///
+ /// Asserts that the state and settings are equal.
+ ///
+ /// Settings manifest to compare against.
+ /// Output state to compare.
+ private void AssertStateAndSettingsAreEqual(TSettingsConfig settings, SettingsResourceObject state)
+ {
+ AssertSettingsAreEqual(settings, state.Settings);
+ }
+
+ ///
+ /// Asserts that two settings manifests are equal.
+ ///
+ /// Expected settings.
+ /// Actual settings.
+ private void AssertSettingsAreEqual(TSettingsConfig expected, TSettingsConfig actual)
+ {
+ var expectedJson = JsonSerializer.SerializeToNode(expected) as JsonObject;
+ var actualJson = JsonSerializer.SerializeToNode(actual) as JsonObject;
+ Assert.IsTrue(JsonNode.DeepEquals(expectedJson, actualJson));
+ }
+
+ ///
+ /// Asserts that the current settings have changed.
+ ///
+ /// Action to prepare the default settings.
+ private void AssertSettingsHasChanged(Action action)
+ {
+ var currentSettings = GetSettings();
+ var defaultSettings = DefaultSettings;
+ action(defaultSettings);
+ AssertSettingsAreEqual(defaultSettings, currentSettings);
+ }
+
+ ///
+ /// Updates the settings.
+ ///
+ /// Action to modify the settings.
+ private void UpdateSettings(Action action)
+ {
+ var settings = GetSettings();
+ action(settings);
+ SaveSettings(settings);
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Commands/BaseCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/BaseCommand.cs
new file mode 100644
index 0000000000..d8cfaaefc6
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Commands/BaseCommand.cs
@@ -0,0 +1,120 @@
+// 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.CommandLine;
+using System.CommandLine.Invocation;
+using System.CommandLine.IO;
+using System.Diagnostics;
+using System.Globalization;
+using System.Text;
+using PowerToys.DSC.DSCResources;
+using PowerToys.DSC.Options;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Commands;
+
+///
+/// Base class for all DSC commands.
+///
+public abstract class BaseCommand : Command
+{
+ private static readonly CompositeFormat ModuleNotSupportedByResource = CompositeFormat.Parse(Resources.ModuleNotSupportedByResource);
+
+ // Shared options for all commands
+ private readonly ModuleOption _moduleOption;
+ private readonly ResourceOption _resourceOption;
+ private readonly InputOption _inputOption;
+
+ // The dictionary of available resources and their factories.
+ private static readonly Dictionary> _resourceFactories = new()
+ {
+ { SettingsResource.ResourceName, module => new SettingsResource(module) },
+
+ // Add other resources here
+ };
+
+ ///
+ /// Gets the list of available DSC resources that can be used with the command.
+ ///
+ public static List AvailableResources => [.._resourceFactories.Keys];
+
+ ///
+ /// Gets the DSC resource to be used by the command.
+ ///
+ protected BaseResource? Resource { get; private set; }
+
+ ///
+ /// Gets the input JSON provided by the user.
+ ///
+ protected string? Input { get; private set; }
+
+ ///
+ /// Gets the PowerToys module to be used by the command.
+ ///
+ protected string? Module { get; private set; }
+
+ public BaseCommand(string name, string description)
+ : base(name, description)
+ {
+ // Register the common options for all commands
+ _moduleOption = new ModuleOption();
+ AddOption(_moduleOption);
+
+ _resourceOption = new ResourceOption(AvailableResources);
+ AddOption(_resourceOption);
+
+ _inputOption = new InputOption();
+ AddOption(_inputOption);
+
+ // Register the command handler
+ this.SetHandler(CommandHandler);
+ }
+
+ ///
+ /// Handles the command invocation.
+ ///
+ /// The invocation context containing the parsed command options.
+ public void CommandHandler(InvocationContext context)
+ {
+ Input = context.ParseResult.GetValueForOption(_inputOption);
+ Module = context.ParseResult.GetValueForOption(_moduleOption);
+ Resource = ResolvedResource(context);
+
+ // Validate the module against the resource's supported modules
+ var supportedModules = Resource.GetSupportedModules();
+ if (!string.IsNullOrEmpty(Module) && !supportedModules.Contains(Module))
+ {
+ var errorMessage = string.Format(CultureInfo.InvariantCulture, ModuleNotSupportedByResource, Module, Resource.Name);
+ context.Console.Error.WriteLine(errorMessage);
+ context.ExitCode = 1;
+ return;
+ }
+
+ // Continue with the command handler logic
+ CommandHandlerInternal(context);
+ }
+
+ ///
+ /// Handles the command logic internally.
+ ///
+ /// Invocation context containing the parsed command options.
+ public abstract void CommandHandlerInternal(InvocationContext context);
+
+ ///
+ /// Resolves the resource from the provided resource name in the context.
+ ///
+ /// Invocation context containing the parsed command options.
+ /// The resolved instance.
+ private BaseResource ResolvedResource(InvocationContext context)
+ {
+ // Resource option has already been validated before the command
+ // handler is invoked.
+ var resourceName = context.ParseResult.GetValueForOption(_resourceOption);
+ Debug.Assert(!string.IsNullOrEmpty(resourceName), "Resource name must not be null or empty.");
+ Debug.Assert(_resourceFactories.ContainsKey(resourceName), $"Resource '{resourceName}' is not registered.");
+ return _resourceFactories[resourceName](Module);
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Commands/ExportCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/ExportCommand.cs
new file mode 100644
index 0000000000..e8001fd0bd
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Commands/ExportCommand.cs
@@ -0,0 +1,25 @@
+// 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.CommandLine.Invocation;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Commands;
+
+///
+/// Command to export all state instances.
+///
+public sealed class ExportCommand : BaseCommand
+{
+ public ExportCommand()
+ : base("export", Resources.ExportCommandDescription)
+ {
+ }
+
+ ///
+ public override void CommandHandlerInternal(InvocationContext context)
+ {
+ context.ExitCode = Resource!.ExportState(Input) ? 0 : 1;
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Commands/GetCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/GetCommand.cs
new file mode 100644
index 0000000000..a5fed7bc73
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Commands/GetCommand.cs
@@ -0,0 +1,25 @@
+// 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.CommandLine.Invocation;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Commands;
+
+///
+/// Command to get the resource state.
+///
+public sealed class GetCommand : BaseCommand
+{
+ public GetCommand()
+ : base("get", Resources.GetCommandDescription)
+ {
+ }
+
+ ///
+ public override void CommandHandlerInternal(InvocationContext context)
+ {
+ context.ExitCode = Resource!.GetState(Input) ? 0 : 1;
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Commands/ManifestCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/ManifestCommand.cs
new file mode 100644
index 0000000000..da3c637137
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Commands/ManifestCommand.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.CommandLine.Invocation;
+using PowerToys.DSC.Options;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Commands;
+
+///
+/// Command to get the manifest of the DSC resource.
+///
+public sealed class ManifestCommand : BaseCommand
+{
+ ///
+ /// Option to specify the output directory for the manifest.
+ ///
+ private readonly OutputDirectoryOption _outputDirectoryOption;
+
+ public ManifestCommand()
+ : base("manifest", Resources.ManifestCommandDescription)
+ {
+ _outputDirectoryOption = new OutputDirectoryOption();
+ AddOption(_outputDirectoryOption);
+ }
+
+ ///
+ public override void CommandHandlerInternal(InvocationContext context)
+ {
+ var outputDir = context.ParseResult.GetValueForOption(_outputDirectoryOption);
+ context.ExitCode = Resource!.Manifest(outputDir) ? 0 : 1;
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Commands/ModulesCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/ModulesCommand.cs
new file mode 100644
index 0000000000..9eb60659df
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Commands/ModulesCommand.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.Diagnostics;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Commands;
+
+///
+/// Command to get all supported modules for a specific resource.
+///
+///
+/// This class is primarily used for debugging purposes and for build scripts.
+///
+public sealed class ModulesCommand : BaseCommand
+{
+ public ModulesCommand()
+ : base("modules", Resources.ModulesCommandDescription)
+ {
+ }
+
+ ///
+ public override void CommandHandlerInternal(InvocationContext context)
+ {
+ // Module is optional, if not provided, all supported modules for the
+ // resource will be printed. If provided, it must be one of the
+ // supported modules since it has been validated before this command is
+ // executed.
+ if (!string.IsNullOrEmpty(Module))
+ {
+ Debug.Assert(Resource!.GetSupportedModules().Contains(Module), "Module must be present in the list of supported modules.");
+ context.Console.WriteLine(Module);
+ }
+ else
+ {
+ // Print the supported modules for the specified resource
+ foreach (var module in Resource!.GetSupportedModules())
+ {
+ context.Console.WriteLine(module);
+ }
+ }
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Commands/SchemaCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/SchemaCommand.cs
new file mode 100644
index 0000000000..f7fbfc2448
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Commands/SchemaCommand.cs
@@ -0,0 +1,25 @@
+// 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.CommandLine.Invocation;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Commands;
+
+///
+/// Command to output the schema of the resource.
+///
+public sealed class SchemaCommand : BaseCommand
+{
+ public SchemaCommand()
+ : base("schema", Resources.SchemaCommandDescription)
+ {
+ }
+
+ ///
+ public override void CommandHandlerInternal(InvocationContext context)
+ {
+ context.ExitCode = Resource!.Schema() ? 0 : 1;
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Commands/SetCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/SetCommand.cs
new file mode 100644
index 0000000000..f76c24a0a8
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Commands/SetCommand.cs
@@ -0,0 +1,25 @@
+// 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.CommandLine.Invocation;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Commands;
+
+///
+/// Command to set the resource state.
+///
+public sealed class SetCommand : BaseCommand
+{
+ public SetCommand()
+ : base("set", Resources.SetCommandDescription)
+ {
+ }
+
+ ///
+ public override void CommandHandlerInternal(InvocationContext context)
+ {
+ context.ExitCode = Resource!.SetState(Input) ? 0 : 1;
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Commands/TestCommand.cs b/src/dsc/v3/PowerToys.DSC/Commands/TestCommand.cs
new file mode 100644
index 0000000000..fcdd83342e
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Commands/TestCommand.cs
@@ -0,0 +1,25 @@
+// 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.CommandLine.Invocation;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Commands;
+
+///
+/// Command to test the resource state.
+///
+public sealed class TestCommand : BaseCommand
+{
+ public TestCommand()
+ : base("test", Resources.TestCommandDescription)
+ {
+ }
+
+ ///
+ public override void CommandHandlerInternal(InvocationContext context)
+ {
+ context.ExitCode = Resource!.TestState(Input) ? 0 : 1;
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/DSCResources/BaseResource.cs b/src/dsc/v3/PowerToys.DSC/DSCResources/BaseResource.cs
new file mode 100644
index 0000000000..51d265cff7
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/DSCResources/BaseResource.cs
@@ -0,0 +1,134 @@
+// 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.Text.Json.Nodes;
+using PowerToys.DSC.Models;
+
+namespace PowerToys.DSC.DSCResources;
+
+///
+/// Base class for all DSC resources.
+///
+public abstract class BaseResource
+{
+ ///
+ /// Gets the name of the resource.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the module being used by the resource, if provided.
+ ///
+ public string? Module { get; }
+
+ public BaseResource(string name, string? module)
+ {
+ Name = name;
+ Module = module;
+ }
+
+ ///
+ /// Calls the get method on the resource.
+ ///
+ /// The input string, if any.
+ /// True if the operation was successful; otherwise false.
+ public abstract bool GetState(string? input);
+
+ ///
+ /// Calls the set method on the resource.
+ ///
+ /// The input string, if any.
+ /// True if the operation was successful; otherwise false.
+ public abstract bool SetState(string? input);
+
+ ///
+ /// Calls the test method on the resource.
+ ///
+ /// The input string, if any.
+ /// True if the operation was successful; otherwise false.
+ public abstract bool TestState(string? input);
+
+ ///
+ /// Calls the export method on the resource.
+ ///
+ /// The input string, if any.
+ /// True if the operation was successful; otherwise false.
+ public abstract bool ExportState(string? input);
+
+ ///
+ /// Calls the schema method on the resource.
+ ///
+ /// True if the operation was successful; otherwise false.
+ public abstract bool Schema();
+
+ ///
+ /// Generates a DSC resource JSON manifest for the resource. If the
+ /// outputDir is not provided, the manifest will be printed to the console.
+ ///
+ /// The directory where the manifest should be
+ /// saved. If null, the manifest will be printed to the console.
+ /// True if the manifest was successfully generated and saved,otherwise false.
+ public abstract bool Manifest(string? outputDir);
+
+ ///
+ /// Gets the list of supported modules for the resource.
+ ///
+ /// Gets a list of supported modules.
+ public abstract IList GetSupportedModules();
+
+ ///
+ /// Writes a JSON output line to the console.
+ ///
+ /// The JSON output to write.
+ protected void WriteJsonOutputLine(JsonNode output)
+ {
+ var json = output.ToJsonString(new() { WriteIndented = false });
+ WriteJsonOutputLine(json);
+ }
+
+ ///
+ /// Writes a JSON output line to the console.
+ ///
+ /// The JSON output to write.
+ protected void WriteJsonOutputLine(string output)
+ {
+ Console.WriteLine(output);
+ }
+
+ ///
+ /// Writes a message output line to the console with the specified message level.
+ ///
+ /// The level of the message.
+ /// The message to write.
+ protected void WriteMessageOutputLine(DscMessageLevel level, string message)
+ {
+ var messageObj = new Dictionary
+ {
+ [GetMessageLevel(level)] = message,
+ };
+ var messageJson = System.Text.Json.JsonSerializer.Serialize(messageObj);
+ Console.Error.WriteLine(messageJson);
+ }
+
+ ///
+ /// Gets the message level as a string based on the provided dsc message level enum value.
+ ///
+ /// The dsc message level.
+ /// A string representation of the message level.
+ /// Thrown when the provided message level is not recognized.
+ private static string GetMessageLevel(DscMessageLevel level)
+ {
+ return level switch
+ {
+ DscMessageLevel.Error => "error",
+ DscMessageLevel.Warning => "warn",
+ DscMessageLevel.Info => "info",
+ DscMessageLevel.Debug => "debug",
+ DscMessageLevel.Trace => "trace",
+ _ => throw new ArgumentOutOfRangeException(nameof(level), level, null),
+ };
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs b/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs
new file mode 100644
index 0000000000..5f69b20227
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/DSCResources/SettingsResource.cs
@@ -0,0 +1,248 @@
+// 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.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+using PowerToys.DSC.Models;
+using PowerToys.DSC.Models.FunctionData;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.DSCResources;
+
+///
+/// Represents the DSC resource for managing PowerToys settings.
+///
+public sealed class SettingsResource : BaseResource
+{
+ private static readonly CompositeFormat FailedToWriteManifests = CompositeFormat.Parse(Resources.FailedToWriteManifests);
+
+ public const string AppModule = "App";
+ public const string ResourceName = "settings";
+
+ private readonly Dictionary> _moduleFunctionData;
+
+ public string ModuleOrDefault => string.IsNullOrEmpty(Module) ? AppModule : Module;
+
+ public SettingsResource(string? module)
+ : base(ResourceName, module)
+ {
+ _moduleFunctionData = new()
+ {
+ { AppModule, CreateModuleFunctionData },
+ { nameof(ModuleType.AdvancedPaste), CreateModuleFunctionData },
+ { nameof(ModuleType.AlwaysOnTop), CreateModuleFunctionData },
+ { nameof(ModuleType.Awake), CreateModuleFunctionData },
+ { nameof(ModuleType.ColorPicker), CreateModuleFunctionData },
+ { nameof(ModuleType.CropAndLock), CreateModuleFunctionData },
+ { nameof(ModuleType.EnvironmentVariables), CreateModuleFunctionData },
+ { nameof(ModuleType.FancyZones), CreateModuleFunctionData },
+ { nameof(ModuleType.FileLocksmith), CreateModuleFunctionData },
+ { nameof(ModuleType.FindMyMouse), CreateModuleFunctionData },
+ { nameof(ModuleType.Hosts), CreateModuleFunctionData },
+ { nameof(ModuleType.ImageResizer), CreateModuleFunctionData },
+ { nameof(ModuleType.KeyboardManager), CreateModuleFunctionData },
+ { nameof(ModuleType.MouseHighlighter), CreateModuleFunctionData },
+ { nameof(ModuleType.MouseJump), CreateModuleFunctionData },
+ { nameof(ModuleType.MousePointerCrosshairs), CreateModuleFunctionData },
+ { nameof(ModuleType.Peek), CreateModuleFunctionData },
+ { nameof(ModuleType.PowerRename), CreateModuleFunctionData },
+ { nameof(ModuleType.PowerAccent), CreateModuleFunctionData },
+ { nameof(ModuleType.RegistryPreview), CreateModuleFunctionData },
+ { nameof(ModuleType.MeasureTool), CreateModuleFunctionData },
+ { nameof(ModuleType.ShortcutGuide), CreateModuleFunctionData },
+ { nameof(ModuleType.PowerOCR), CreateModuleFunctionData },
+ { nameof(ModuleType.Workspaces), CreateModuleFunctionData },
+ { nameof(ModuleType.ZoomIt), CreateModuleFunctionData },
+
+ // The following modules are not currently supported:
+ // - MouseWithoutBorders Contains sensitive configuration values, making export/import potentially insecure.
+ // - PowerLauncher Uses absolute file paths in its settings, which are not portable across systems.
+ // - NewPlus Uses absolute file paths in its settings, which are not portable across systems.
+ };
+ }
+
+ ///
+ public override bool ExportState(string? input)
+ {
+ var data = CreateFunctionData();
+ data.GetState();
+ WriteJsonOutputLine(data.Output.ToJson());
+ return true;
+ }
+
+ ///
+ public override bool GetState(string? input)
+ {
+ return ExportState(input);
+ }
+
+ ///
+ public override bool SetState(string? input)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError);
+ return false;
+ }
+
+ var data = CreateFunctionData(input);
+ data.GetState();
+
+ // Capture the diff before updating the output
+ var diff = data.GetDiffJson();
+
+ // Only call Set if the desired state is different from the current state
+ if (!data.TestState())
+ {
+ var inputSettings = data.Input.SettingsInternal;
+ data.Output.SettingsInternal = inputSettings;
+ data.SetState();
+ }
+
+ WriteJsonOutputLine(data.Output.ToJson());
+ WriteJsonOutputLine(diff);
+ return true;
+ }
+
+ ///
+ public override bool TestState(string? input)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError);
+ return false;
+ }
+
+ var data = CreateFunctionData(input);
+ data.GetState();
+ data.Output.InDesiredState = data.TestState();
+
+ WriteJsonOutputLine(data.Output.ToJson());
+ WriteJsonOutputLine(data.GetDiffJson());
+ return true;
+ }
+
+ ///
+ public override bool Schema()
+ {
+ var data = CreateFunctionData();
+ WriteJsonOutputLine(data.Schema());
+ return true;
+ }
+
+ ///
+ ///
+ /// If an output directory is specified, write the manifests to files,
+ /// otherwise output them to the console.
+ ///
+ public override bool Manifest(string? outputDir)
+ {
+ var manifests = GenerateManifests();
+
+ if (!string.IsNullOrEmpty(outputDir))
+ {
+ try
+ {
+ foreach (var (name, manifest) in manifests)
+ {
+ File.WriteAllText(Path.Combine(outputDir, $"microsoft.powertoys.{name}.settings.dsc.resource.json"), manifest);
+ }
+ }
+ catch (Exception ex)
+ {
+ var errorMessage = string.Format(CultureInfo.InvariantCulture, FailedToWriteManifests, outputDir, ex.Message);
+ WriteMessageOutputLine(DscMessageLevel.Error, errorMessage);
+ return false;
+ }
+ }
+ else
+ {
+ foreach (var (_, manifest) in manifests)
+ {
+ WriteJsonOutputLine(manifest);
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// Generates manifests for the specified module or all supported modules
+ /// if no module is specified.
+ ///
+ /// A list of tuples containing the module name and its corresponding manifest JSON.
+ private List<(string Name, string Manifest)> GenerateManifests()
+ {
+ List<(string Name, string Manifest)> manifests = [];
+ if (!string.IsNullOrEmpty(Module))
+ {
+ manifests.Add((Module, GenerateManifest(Module)));
+ }
+ else
+ {
+ foreach (var module in GetSupportedModules())
+ {
+ manifests.Add((module, GenerateManifest(module)));
+ }
+ }
+
+ return manifests;
+ }
+
+ ///
+ /// Generate a DSC resource JSON manifest for the specified module.
+ ///
+ /// The name of the module for which to generate the manifest.
+ /// A JSON string representing the DSC resource manifest.
+ private string GenerateManifest(string module)
+ {
+ // Note: The description is not localized because the generated
+ // manifest file will be part of the package
+ return new DscManifest($"{module}Settings", "0.1.0")
+ .AddDescription($"Allows management of {module} settings state via the DSC v3 command line interface protocol.")
+ .AddStdinMethod("export", ["export", "--module", module, "--resource", "settings"])
+ .AddStdinMethod("get", ["get", "--module", module, "--resource", "settings"])
+ .AddJsonInputMethod("set", "--input", ["set", "--module", module, "--resource", "settings"], implementsPretest: true, stateAndDiff: true)
+ .AddJsonInputMethod("test", "--input", ["test", "--module", module, "--resource", "settings"], stateAndDiff: true)
+ .AddCommandMethod("schema", ["schema", "--module", module, "--resource", "settings"])
+ .ToJson();
+ }
+
+ ///
+ public override IList GetSupportedModules()
+ {
+ return [.. _moduleFunctionData.Keys.Order()];
+ }
+
+ ///
+ /// Creates the function data for the specified module or the default module if none is specified.
+ ///
+ /// The input string, if any.
+ /// An instance of for the specified module.
+ public ISettingsFunctionData CreateFunctionData(string? input = null)
+ {
+ Debug.Assert(_moduleFunctionData.ContainsKey(ModuleOrDefault), "Module should be supported by the resource.");
+ return _moduleFunctionData[ModuleOrDefault](input);
+ }
+
+ ///
+ /// Creates the function data for a specific settings configuration type.
+ ///
+ /// The type of settings configuration to create function data for.
+ /// The input string, if any.
+ /// An instance of for the specified settings configuration type.
+ private ISettingsFunctionData CreateModuleFunctionData(string? input)
+ where TSettingsConfig : ISettingsConfig, new()
+ {
+ return new SettingsFunctionData(input);
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs b/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs
new file mode 100644
index 0000000000..dcb6abf4a1
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs
@@ -0,0 +1,152 @@
+// 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.Nodes;
+
+namespace PowerToys.DSC.Models;
+
+///
+/// Class for building a DSC manifest for PowerToys resources.
+///
+public sealed class DscManifest
+{
+ private const string Schema = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json";
+ private const string Executable = @"PowerToys.DSC.exe";
+
+ private readonly string _type;
+ private readonly string _version;
+ private readonly JsonObject _manifest;
+
+ public DscManifest(string type, string version)
+ {
+ _type = type;
+ _version = version;
+ _manifest = new JsonObject
+ {
+ ["$schema"] = Schema,
+ ["type"] = $"Microsoft.PowerToys/{_type}",
+ ["version"] = _version,
+ ["tags"] = new JsonArray("PowerToys"),
+ };
+ }
+
+ ///
+ /// Adds a description to the manifest.
+ ///
+ /// The description to add.
+ /// Returns the current instance of .
+ public DscManifest AddDescription(string description)
+ {
+ _manifest["description"] = description;
+ return this;
+ }
+
+ ///
+ /// Adds a method to the manifest with the specified executable and arguments.
+ ///
+ /// The name of the method to add.
+ /// The input argument for the method
+ /// The list of arguments for the method.
+ /// Whether the method implements a pretest.
+ /// Whether the method returns state and diff.
+ /// Returns the current instance of .
+ public DscManifest AddJsonInputMethod(string method, string inputArg, List args, bool? implementsPretest = null, bool? stateAndDiff = null)
+ {
+ var argsJson = CreateJsonArray(args);
+ argsJson.Add(new JsonObject
+ {
+ ["jsonInputArg"] = inputArg,
+ ["mandatory"] = true,
+ });
+ var methodObject = AddMethod(argsJson, implementsPretest, stateAndDiff);
+ _manifest[method] = methodObject;
+ return this;
+ }
+
+ ///
+ /// Adds a method to the manifest that reads from standard input (stdin).
+ ///
+ /// The name of the method to add.
+ /// The list of arguments for the method.
+ /// Whether the method implements a pretest.
+ /// Whether the method returns state and diff.
+ /// Returns the current instance of .
+ public DscManifest AddStdinMethod(string method, List args, bool? implementsPretest = null, bool? stateAndDiff = null)
+ {
+ var argsJson = CreateJsonArray(args);
+ var methodObject = AddMethod(argsJson, implementsPretest, stateAndDiff);
+ methodObject["input"] = "stdin";
+ _manifest[method] = methodObject;
+ return this;
+ }
+
+ ///
+ /// Adds a command method to the manifest.
+ ///
+ /// The name of the method to add.
+ /// The list of arguments for the method.
+ /// Returns the current instance of .
+ public DscManifest AddCommandMethod(string method, List args)
+ {
+ _manifest[method] = new JsonObject
+ {
+ ["command"] = AddMethod(CreateJsonArray(args)),
+ };
+ return this;
+ }
+
+ ///
+ /// Gets the JSON representation of the manifest.
+ ///
+ /// Returns the JSON string of the manifest.
+ public string ToJson()
+ {
+ return _manifest.ToJsonString(new() { WriteIndented = true });
+ }
+
+ ///
+ /// Add a method to the manifest with the specified arguments.
+ ///
+ /// The list of arguments for the method.
+ /// Whether the method implements a pretest.
+ /// Whether the method returns state and diff.
+ /// Returns the method object.
+ private JsonObject AddMethod(JsonArray args, bool? implementsPretest = null, bool? stateAndDiff = null)
+ {
+ var methodObject = new JsonObject
+ {
+ ["executable"] = Executable,
+ ["args"] = args,
+ };
+
+ if (implementsPretest.HasValue)
+ {
+ methodObject["implementsPretest"] = implementsPretest.Value;
+ }
+
+ if (stateAndDiff.HasValue)
+ {
+ methodObject["return"] = stateAndDiff.Value ? "stateAndDiff" : "state";
+ }
+
+ return methodObject;
+ }
+
+ ///
+ /// Creates a JSON array from a list of strings.
+ ///
+ /// The list of strings to convert.
+ /// Returns the JSON array.
+ private JsonArray CreateJsonArray(List args)
+ {
+ var jsonArray = new JsonArray();
+ foreach (var arg in args)
+ {
+ jsonArray.Add(arg);
+ }
+
+ return jsonArray;
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Models/DscMessageLevel.cs b/src/dsc/v3/PowerToys.DSC/Models/DscMessageLevel.cs
new file mode 100644
index 0000000000..9c5b12b3c0
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Models/DscMessageLevel.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace PowerToys.DSC.Models;
+
+///
+/// Specifies the severity level of a message.
+///
+public enum DscMessageLevel
+{
+ ///
+ /// Represents an error message.
+ ///
+ Error,
+
+ ///
+ /// Represents a warning message.
+ ///
+ Warning,
+
+ ///
+ /// Represents an informational message.
+ ///
+ Info,
+
+ ///
+ /// Represents a debug message.
+ ///
+ Debug,
+
+ ///
+ /// Represents a trace message.
+ ///
+ Trace,
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Models/FunctionData/BaseFunctionData.cs b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/BaseFunctionData.cs
new file mode 100644
index 0000000000..4456beed82
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/BaseFunctionData.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Newtonsoft.Json;
+using NJsonSchema.Generation;
+using PowerToys.DSC.Models.ResourceObjects;
+
+namespace PowerToys.DSC.Models.FunctionData;
+
+///
+/// Base class for function data objects.
+///
+public class BaseFunctionData
+{
+ ///
+ /// Generates a JSON schema for the specified resource object type.
+ ///
+ /// The type of the resource object.
+ /// A JSON schema string.
+ protected static string GenerateSchema()
+ where T : BaseResourceObject
+ {
+ var settings = new SystemTextJsonSchemaGeneratorSettings()
+ {
+ FlattenInheritanceHierarchy = true,
+ SerializerOptions =
+ {
+ IgnoreReadOnlyFields = true,
+ },
+ };
+ var generator = new JsonSchemaGenerator(settings);
+ var schema = generator.Generate(typeof(T));
+ return schema.ToJson(Formatting.None);
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Models/FunctionData/ISettingsFunctionData.cs b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/ISettingsFunctionData.cs
new file mode 100644
index 0000000000..7cf02d1c74
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/ISettingsFunctionData.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Nodes;
+using PowerToys.DSC.Models.ResourceObjects;
+
+namespace PowerToys.DSC.Models.FunctionData;
+
+///
+/// Interface for function data related to settings.
+///
+public interface ISettingsFunctionData
+{
+ ///
+ /// Gets the input settings resource object.
+ ///
+ public ISettingsResourceObject Input { get; }
+
+ ///
+ /// Gets the output settings resource object.
+ ///
+ public ISettingsResourceObject Output { get; }
+
+ ///
+ /// Gets the current settings.
+ ///
+ public void GetState();
+
+ ///
+ /// Sets the current settings.
+ ///
+ public void SetState();
+
+ ///
+ /// Tests if the current settings and the desired state are valid.
+ ///
+ /// True if the current settings match the desired state; otherwise false.
+ public bool TestState();
+
+ ///
+ /// Gets the difference between the current settings and the desired state in JSON format.
+ ///
+ /// A JSON array representing the differences.
+ public JsonArray GetDiffJson();
+
+ ///
+ /// Gets the schema for the settings resource object.
+ ///
+ ///
+ public string Schema();
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs
new file mode 100644
index 0000000000..7fcce03d33
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Models/FunctionData/SettingsFunctionData`1.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Diagnostics;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+using PowerToys.DSC.Models.ResourceObjects;
+
+namespace PowerToys.DSC.Models.FunctionData;
+
+///
+/// Represents function data for the settings DSC resource.
+///
+/// The module settings configuration type.
+public sealed class SettingsFunctionData : BaseFunctionData, ISettingsFunctionData
+ where TSettingsConfig : ISettingsConfig, new()
+{
+ private static readonly SettingsUtils _settingsUtils = new();
+ private static readonly TSettingsConfig _settingsConfig = new();
+
+ private readonly SettingsResourceObject _input;
+ private readonly SettingsResourceObject _output;
+
+ ///
+ public ISettingsResourceObject Input => _input;
+
+ ///
+ public ISettingsResourceObject Output => _output;
+
+ public SettingsFunctionData(string? input = null)
+ {
+ _output = new();
+ _input = string.IsNullOrEmpty(input) ? new() : JsonSerializer.Deserialize>(input) ?? new();
+ }
+
+ ///
+ public void GetState()
+ {
+ _output.Settings = GetSettings();
+ }
+
+ ///
+ public void SetState()
+ {
+ Debug.Assert(_output.Settings != null, "Output settings should not be null");
+ SaveSettings(_output.Settings);
+ }
+
+ ///
+ public bool TestState()
+ {
+ var input = JsonSerializer.SerializeToNode(_input.Settings);
+ var output = JsonSerializer.SerializeToNode(_output.Settings);
+ return JsonNode.DeepEquals(input, output);
+ }
+
+ ///
+ public JsonArray GetDiffJson()
+ {
+ var diff = new JsonArray();
+ if (!TestState())
+ {
+ diff.Add(SettingsResourceObject.SettingsJsonPropertyName);
+ }
+
+ return diff;
+ }
+
+ ///
+ public string Schema()
+ {
+ return GenerateSchema>();
+ }
+
+ ///
+ /// Gets the settings configuration from the settings utils for a specific module.
+ ///
+ /// The settings configuration for the module.
+ private static TSettingsConfig GetSettings()
+ {
+ return _settingsUtils.GetSettingsOrDefault(_settingsConfig.GetModuleName());
+ }
+
+ ///
+ /// Saves the settings configuration to the settings utils for a specific module.
+ ///
+ /// Settings of a specific module
+ private static void SaveSettings(TSettingsConfig settings)
+ {
+ var inputJson = JsonSerializer.Serialize(settings);
+ _settingsUtils.SaveSettings(inputJson, _settingsConfig.GetModuleName());
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/BaseResourceObject.cs b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/BaseResourceObject.cs
new file mode 100644
index 0000000000..d6e3e08dcc
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/BaseResourceObject.cs
@@ -0,0 +1,45 @@
+// 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.ComponentModel;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace PowerToys.DSC.Models.ResourceObjects;
+
+///
+/// Base class for all resource objects.
+///
+public class BaseResourceObject
+{
+ private readonly JsonSerializerOptions _options;
+
+ public BaseResourceObject()
+ {
+ _options = new()
+ {
+ WriteIndented = false,
+ TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
+ };
+ }
+
+ ///
+ /// Gets or sets whether an instance is in the desired state.
+ ///
+ [JsonPropertyName("_inDesiredState")]
+ [Description("Indicates whether an instance is in the desired state")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? InDesiredState { get; set; }
+
+ ///
+ /// Generates a JSON representation of the resource object.
+ ///
+ ///
+ public JsonNode ToJson()
+ {
+ return JsonSerializer.SerializeToNode(this, GetType(), _options) ?? new JsonObject();
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/ISettingsResourceObject.cs b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/ISettingsResourceObject.cs
new file mode 100644
index 0000000000..85c9c7eadc
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/ISettingsResourceObject.cs
@@ -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.Text.Json.Nodes;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+
+namespace PowerToys.DSC.Models.ResourceObjects;
+
+///
+/// Interface for settings resource objects.
+///
+public interface ISettingsResourceObject
+{
+ ///
+ /// Gets or sets the settings configuration.
+ ///
+ public ISettingsConfig SettingsInternal { get; set; }
+
+ ///
+ /// Gets or sets whether an instance is in the desired state.
+ ///
+ public bool? InDesiredState { get; set; }
+
+ ///
+ /// Generates a JSON representation of the resource object.
+ ///
+ /// String representation of the resource object in JSON format.
+ public JsonNode ToJson();
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/SettingsResourceObject`1.cs b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/SettingsResourceObject`1.cs
new file mode 100644
index 0000000000..d5017336ed
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Models/ResourceObjects/SettingsResourceObject`1.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+using NJsonSchema.Annotations;
+
+namespace PowerToys.DSC.Models.ResourceObjects;
+
+///
+/// Represents a settings resource object for a module's settings configuration.
+///
+/// The type of the settings configuration.
+public sealed class SettingsResourceObject : BaseResourceObject, ISettingsResourceObject
+ where TSettingsConfig : ISettingsConfig, new()
+{
+ public const string SettingsJsonPropertyName = "settings";
+
+ ///
+ /// Gets or sets the settings content for the module.
+ ///
+ [JsonPropertyName(SettingsJsonPropertyName)]
+ [Required]
+ [Description("The settings content for the module.")]
+ [JsonSchemaType(typeof(object))]
+ public TSettingsConfig Settings { get; set; } = new();
+
+ ///
+ [JsonIgnore]
+ public ISettingsConfig SettingsInternal { get => Settings; set => Settings = (TSettingsConfig)value; }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Options/InputOption.cs b/src/dsc/v3/PowerToys.DSC/Options/InputOption.cs
new file mode 100644
index 0000000000..048c50a2df
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Options/InputOption.cs
@@ -0,0 +1,51 @@
+// 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.CommandLine;
+using System.CommandLine.Parsing;
+using System.Globalization;
+using System.Text;
+using System.Text.Json;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Options;
+
+///
+/// Represents an option for specifying JSON input for the dsc command.
+///
+public sealed class InputOption : Option
+{
+ private static readonly CompositeFormat InvalidJsonInputError = CompositeFormat.Parse(Resources.InvalidJsonInputError);
+
+ public InputOption()
+ : base("--input", Resources.InputOptionDescription)
+ {
+ AddValidator(OptionValidator);
+ }
+
+ ///
+ /// Validates the JSON input provided to the option.
+ ///
+ /// The option result to validate.
+ private void OptionValidator(OptionResult result)
+ {
+ var value = result.GetValueOrDefault() ?? string.Empty;
+ if (string.IsNullOrEmpty(value))
+ {
+ result.ErrorMessage = Resources.InputEmptyOrNullError;
+ }
+ else
+ {
+ try
+ {
+ JsonDocument.Parse(value);
+ }
+ catch (Exception e)
+ {
+ result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidJsonInputError, e.Message);
+ }
+ }
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Options/ModuleOption.cs b/src/dsc/v3/PowerToys.DSC/Options/ModuleOption.cs
new file mode 100644
index 0000000000..a5273c2cb0
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Options/ModuleOption.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.CommandLine;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Options;
+
+///
+/// Represents an option for specifying the module name for the dsc command.
+///
+public sealed class ModuleOption : Option
+{
+ public ModuleOption()
+ : base("--module", Resources.ModuleOptionDescription)
+ {
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Options/OutputDirectoryOption.cs b/src/dsc/v3/PowerToys.DSC/Options/OutputDirectoryOption.cs
new file mode 100644
index 0000000000..7de1af64b7
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Options/OutputDirectoryOption.cs
@@ -0,0 +1,43 @@
+// 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.CommandLine;
+using System.CommandLine.Parsing;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Options;
+
+///
+/// Represents an option for specifying the output directory for the dsc command.
+///
+public sealed class OutputDirectoryOption : Option
+{
+ private static readonly CompositeFormat InvalidOutputDirectoryError = CompositeFormat.Parse(Resources.InvalidOutputDirectoryError);
+
+ public OutputDirectoryOption()
+ : base("--outputDir", Resources.OutputDirectoryOptionDescription)
+ {
+ AddValidator(OptionValidator);
+ }
+
+ ///
+ /// Validates the output directory option.
+ ///
+ /// The option result to validate.
+ private void OptionValidator(OptionResult result)
+ {
+ var value = result.GetValueOrDefault() ?? string.Empty;
+ if (string.IsNullOrEmpty(value))
+ {
+ result.ErrorMessage = Resources.OutputDirectoryEmptyOrNullError;
+ }
+ else if (!Directory.Exists(value))
+ {
+ result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidOutputDirectoryError, value);
+ }
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Options/ResourceOption.cs b/src/dsc/v3/PowerToys.DSC/Options/ResourceOption.cs
new file mode 100644
index 0000000000..cfce5dbfc7
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Options/ResourceOption.cs
@@ -0,0 +1,43 @@
+// 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.CommandLine;
+using System.CommandLine.Parsing;
+using System.Globalization;
+using System.Text;
+using PowerToys.DSC.Properties;
+
+namespace PowerToys.DSC.Options;
+
+///
+/// Represents an option for specifying the resource name for the dsc command.
+///
+public sealed class ResourceOption : Option
+{
+ private static readonly CompositeFormat InvalidResourceNameError = CompositeFormat.Parse(Resources.InvalidResourceNameError);
+
+ private readonly IList _resources = [];
+
+ public ResourceOption(IList resources)
+ : base("--resource", Resources.ResourceOptionDescription)
+ {
+ _resources = resources;
+ IsRequired = true;
+ AddValidator(OptionValidator);
+ }
+
+ ///
+ /// Validates the resource option to ensure that the specified resource name is valid.
+ ///
+ /// The option result to validate.
+ private void OptionValidator(OptionResult result)
+ {
+ var value = result.GetValueOrDefault() ?? string.Empty;
+ if (!_resources.Contains(value))
+ {
+ result.ErrorMessage = string.Format(CultureInfo.InvariantCulture, InvalidResourceNameError, string.Join(", ", _resources));
+ }
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
new file mode 100644
index 0000000000..230cd4556b
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+ Exe
+ ..\..\..\..\$(Platform)\$(Configuration)
+ false
+ false
+ PowerToys.DSC
+ PowerToys DSC
+ PowerToys.DSC
+ enable
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/dsc/v3/PowerToys.DSC/Program.cs b/src/dsc/v3/PowerToys.DSC/Program.cs
new file mode 100644
index 0000000000..09a22b64d6
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Program.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.CommandLine;
+using System.CommandLine.Parsing;
+using System.Threading.Tasks;
+using PowerToys.DSC.Commands;
+
+namespace PowerToys.DSC;
+
+///
+/// Main entry point for the PowerToys Desired State Configuration CLI application.
+///
+public class Program
+{
+ public static async Task Main(string[] args)
+ {
+ var rootCommand = new RootCommand(Properties.Resources.PowerToysDSC);
+ rootCommand.AddCommand(new GetCommand());
+ rootCommand.AddCommand(new SetCommand());
+ rootCommand.AddCommand(new ExportCommand());
+ rootCommand.AddCommand(new TestCommand());
+ rootCommand.AddCommand(new SchemaCommand());
+ rootCommand.AddCommand(new ManifestCommand());
+ rootCommand.AddCommand(new ModulesCommand());
+ return await rootCommand.InvokeAsync(args);
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Properties/Resources.Designer.cs b/src/dsc/v3/PowerToys.DSC/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..4089d98c6b
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Properties/Resources.Designer.cs
@@ -0,0 +1,234 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace PowerToys.DSC.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PowerToys.DSC.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Get all state instances.
+ ///
+ internal static string ExportCommandDescription {
+ get {
+ return ResourceManager.GetString("ExportCommandDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to write manifests to directory '{0}': {1}.
+ ///
+ internal static string FailedToWriteManifests {
+ get {
+ return ResourceManager.GetString("FailedToWriteManifests", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Get the resource state.
+ ///
+ internal static string GetCommandDescription {
+ get {
+ return ResourceManager.GetString("GetCommandDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Input cannot be empty or null.
+ ///
+ internal static string InputEmptyOrNullError {
+ get {
+ return ResourceManager.GetString("InputEmptyOrNullError", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The JSON input.
+ ///
+ internal static string InputOptionDescription {
+ get {
+ return ResourceManager.GetString("InputOptionDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid JSON input: {0}.
+ ///
+ internal static string InvalidJsonInputError {
+ get {
+ return ResourceManager.GetString("InvalidJsonInputError", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid output directory: {0}.
+ ///
+ internal static string InvalidOutputDirectoryError {
+ get {
+ return ResourceManager.GetString("InvalidOutputDirectoryError", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid resource name. Valid resource names are: {0}.
+ ///
+ internal static string InvalidResourceNameError {
+ get {
+ return ResourceManager.GetString("InvalidResourceNameError", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Get the manifest of the dsc resource.
+ ///
+ internal static string ManifestCommandDescription {
+ get {
+ return ResourceManager.GetString("ManifestCommandDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Module '{0}' is not supported for the resource {1}. Use the 'module' command to list available modules..
+ ///
+ internal static string ModuleNotSupportedByResource {
+ get {
+ return ResourceManager.GetString("ModuleNotSupportedByResource", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The module name.
+ ///
+ internal static string ModuleOptionDescription {
+ get {
+ return ResourceManager.GetString("ModuleOptionDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Get all supported modules for a specific resource.
+ ///
+ internal static string ModulesCommandDescription {
+ get {
+ return ResourceManager.GetString("ModulesCommandDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Output directory cannot be empty or null.
+ ///
+ internal static string OutputDirectoryEmptyOrNullError {
+ get {
+ return ResourceManager.GetString("OutputDirectoryEmptyOrNullError", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The output directory.
+ ///
+ internal static string OutputDirectoryOptionDescription {
+ get {
+ return ResourceManager.GetString("OutputDirectoryOptionDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to PowerToys Desired State Configuration commands.
+ ///
+ internal static string PowerToysDSC {
+ get {
+ return ResourceManager.GetString("PowerToysDSC", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The resource name.
+ ///
+ internal static string ResourceOptionDescription {
+ get {
+ return ResourceManager.GetString("ResourceOptionDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Outputs schema of the resource.
+ ///
+ internal static string SchemaCommandDescription {
+ get {
+ return ResourceManager.GetString("SchemaCommandDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Set the resource state.
+ ///
+ internal static string SetCommandDescription {
+ get {
+ return ResourceManager.GetString("SetCommandDescription", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Test the resource state.
+ ///
+ internal static string TestCommandDescription {
+ get {
+ return ResourceManager.GetString("TestCommandDescription", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/dsc/v3/PowerToys.DSC/Properties/Resources.resx b/src/dsc/v3/PowerToys.DSC/Properties/Resources.resx
new file mode 100644
index 0000000000..2648d6501b
--- /dev/null
+++ b/src/dsc/v3/PowerToys.DSC/Properties/Resources.resx
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ PowerToys Desired State Configuration commands
+ {Locked="PowerToys Desired State Configuration"}
+
+
+ Module '{0}' is not supported for the resource {1}. Use the 'module' command to list available modules.
+ {Locked="'module'","{0}","{1}"}
+
+
+ Get all state instances
+
+
+ Get the resource state
+
+
+ Get the manifest of the dsc resource
+
+
+ Get all supported modules for a specific resource
+
+
+ Outputs schema of the resource
+
+
+ Set the resource state
+
+
+ Test the resource state
+
+
+ Input cannot be empty or null
+
+
+ Failed to write manifests to directory '{0}': {1}
+ {Locked="{0}","{1}"}
+
+
+ The JSON input
+
+
+ The module name
+
+
+ The output directory
+
+
+ The resource name
+
+
+ Invalid JSON input: {0}
+ {Locked="{0}"}
+
+
+ Output directory cannot be empty or null
+
+
+ Invalid output directory: {0}
+ {Locked="{0}"}
+
+
+ Invalid resource name. Valid resource names are: {0}
+ {Locked="{0}"}
+
+
\ No newline at end of file
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp
index 61e292d7ee..9b155cb3f3 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp
@@ -94,6 +94,21 @@ public:
}
}
+ static void SetCrosshairsOrientation(CrosshairsOrientation orientation)
+ {
+ if (instance != nullptr)
+ {
+ auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
+ dispatcherQueue.TryEnqueue([orientation]() {
+ if (instance != nullptr)
+ {
+ instance->m_crosshairs_orientation = orientation;
+ instance->UpdateCrosshairsPosition();
+ }
+ });
+ }
+ }
+
private:
enum class MouseButton
{
@@ -147,6 +162,7 @@ private:
int m_crosshairs_border_size = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_BORDER_SIZE;
bool m_crosshairs_is_fixed_length_enabled = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED;
int m_crosshairs_fixed_length = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_FIXED_LENGTH;
+ CrosshairsOrientation m_crosshairs_orientation = static_cast(INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_ORIENTATION);
float m_crosshairs_opacity = max(0.f, min(1.f, (float)INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_OPACITY / 100.0f));
bool m_crosshairs_auto_hide = INCLUSIVE_MOUSE_DEFAULT_AUTO_HIDE;
};
@@ -286,6 +302,8 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
float halfPixelAdjustment = m_crosshairs_thickness % 2 == 1 ? 0.5f : 0.0f;
float borderSizePadding = m_crosshairs_border_size * 2.f;
+ // Left and Right crosshairs (horizontal line)
+ if (m_crosshairs_orientation == CrosshairsOrientation::Both || m_crosshairs_orientation == CrosshairsOrientation::HorizontalOnly)
{
float leftCrosshairsFullScreenLength = ptCursor.x - ptMonitorUpperLeft.x - m_crosshairs_radius + halfPixelAdjustment * 2.f;
float leftCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : leftCrosshairsFullScreenLength;
@@ -294,9 +312,7 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
m_left_crosshairs_border.Size({ leftCrosshairsBorderLength, m_crosshairs_thickness + borderSizePadding });
m_left_crosshairs.Offset({ ptCursor.x - m_crosshairs_radius + halfPixelAdjustment * 2.f, ptCursor.y + halfPixelAdjustment, .0f });
m_left_crosshairs.Size({ leftCrosshairsLength, static_cast(m_crosshairs_thickness) });
- }
- {
float rightCrosshairsFullScreenLength = static_cast(ptMonitorBottomRight.x) - ptCursor.x - m_crosshairs_radius;
float rightCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : rightCrosshairsFullScreenLength;
float rightCrosshairsBorderLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length + borderSizePadding : rightCrosshairsFullScreenLength + m_crosshairs_border_size;
@@ -305,7 +321,17 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
m_right_crosshairs.Offset({ static_cast(ptCursor.x) + m_crosshairs_radius, ptCursor.y + halfPixelAdjustment, .0f });
m_right_crosshairs.Size({ rightCrosshairsLength, static_cast(m_crosshairs_thickness) });
}
+ else
+ {
+ // Hide horizontal crosshairs by setting size to 0
+ m_left_crosshairs_border.Size({ 0.0f, 0.0f });
+ m_left_crosshairs.Size({ 0.0f, 0.0f });
+ m_right_crosshairs_border.Size({ 0.0f, 0.0f });
+ m_right_crosshairs.Size({ 0.0f, 0.0f });
+ }
+ // Top and Bottom crosshairs (vertical line)
+ if (m_crosshairs_orientation == CrosshairsOrientation::Both || m_crosshairs_orientation == CrosshairsOrientation::VerticalOnly)
{
float topCrosshairsFullScreenLength = ptCursor.y - ptMonitorUpperLeft.y - m_crosshairs_radius + halfPixelAdjustment * 2.f;
float topCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : topCrosshairsFullScreenLength;
@@ -314,9 +340,7 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
m_top_crosshairs_border.Size({ m_crosshairs_thickness + borderSizePadding, topCrosshairsBorderLength });
m_top_crosshairs.Offset({ ptCursor.x + halfPixelAdjustment, ptCursor.y - m_crosshairs_radius + halfPixelAdjustment * 2.f, .0f });
m_top_crosshairs.Size({ static_cast(m_crosshairs_thickness), topCrosshairsLength });
- }
- {
float bottomCrosshairsFullScreenLength = static_cast(ptMonitorBottomRight.y) - ptCursor.y - m_crosshairs_radius;
float bottomCrosshairsLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length : bottomCrosshairsFullScreenLength;
float bottomCrosshairsBorderLength = m_crosshairs_is_fixed_length_enabled ? m_crosshairs_fixed_length + borderSizePadding : bottomCrosshairsFullScreenLength + m_crosshairs_border_size;
@@ -325,6 +349,14 @@ void InclusiveCrosshairs::UpdateCrosshairsPosition()
m_bottom_crosshairs.Offset({ ptCursor.x + halfPixelAdjustment, static_cast(ptCursor.y) + m_crosshairs_radius, .0f });
m_bottom_crosshairs.Size({ static_cast(m_crosshairs_thickness), bottomCrosshairsLength });
}
+ else
+ {
+ // Hide vertical crosshairs by setting size to 0
+ m_top_crosshairs_border.Size({ 0.0f, 0.0f });
+ m_top_crosshairs.Size({ 0.0f, 0.0f });
+ m_bottom_crosshairs_border.Size({ 0.0f, 0.0f });
+ m_bottom_crosshairs.Size({ 0.0f, 0.0f });
+ }
}
LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept
@@ -398,6 +430,7 @@ void InclusiveCrosshairs::ApplySettings(InclusiveCrosshairsSettings& settings, b
m_crosshairs_auto_hide = settings.crosshairsAutoHide;
m_crosshairs_is_fixed_length_enabled = settings.crosshairsIsFixedLengthEnabled;
m_crosshairs_fixed_length = settings.crosshairsFixedLength;
+ m_crosshairs_orientation = settings.crosshairsOrientation;
if (applyToRunTimeObjects)
{
@@ -618,6 +651,11 @@ void InclusiveCrosshairsSetExternalControl(bool enabled)
InclusiveCrosshairs::SetExternalControl(enabled);
}
+void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation)
+{
+ InclusiveCrosshairs::SetCrosshairsOrientation(orientation);
+}
+
int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings)
{
Logger::info("Starting a crosshairs instance.");
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h
index a6618d85bf..4475a397a8 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h
@@ -10,8 +10,16 @@ constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_BORDER_SIZE = 1;
constexpr bool INCLUSIVE_MOUSE_DEFAULT_AUTO_HIDE = false;
constexpr bool INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED = false;
constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_FIXED_LENGTH = 1;
+constexpr int INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_ORIENTATION = 0; // 0=Both, 1=Vertical, 2=Horizontal
constexpr bool INCLUSIVE_MOUSE_DEFAULT_AUTO_ACTIVATE = false;
+enum struct CrosshairsOrientation : int
+{
+ Both = 0,
+ VerticalOnly = 1,
+ HorizontalOnly = 2,
+};
+
struct InclusiveCrosshairsSettings
{
winrt::Windows::UI::Color crosshairsColor = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_COLOR;
@@ -23,6 +31,7 @@ struct InclusiveCrosshairsSettings
bool crosshairsAutoHide = INCLUSIVE_MOUSE_DEFAULT_AUTO_HIDE;
bool crosshairsIsFixedLengthEnabled = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED;
int crosshairsFixedLength = INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_FIXED_LENGTH;
+ CrosshairsOrientation crosshairsOrientation = static_cast(INCLUSIVE_MOUSE_DEFAULT_CROSSHAIRS_ORIENTATION);
bool autoActivate = INCLUSIVE_MOUSE_DEFAULT_AUTO_ACTIVATE;
};
@@ -35,3 +44,4 @@ void InclusiveCrosshairsRequestUpdatePosition();
void InclusiveCrosshairsEnsureOn();
void InclusiveCrosshairsEnsureOff();
void InclusiveCrosshairsSetExternalControl(bool enabled);
+void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation);
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj b/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj
index 7da54a51e9..58668c663f 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj
@@ -80,7 +80,7 @@
- $(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)
+ ..\..\..\;..\..\..\modules;..\..\..\common\Telemetry;%(AdditionalIncludeDirectories)
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
index 3dcee0d6a4..fd144e807b 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
extern void InclusiveCrosshairsRequestUpdatePosition();
extern void InclusiveCrosshairsEnsureOn();
@@ -30,6 +31,7 @@ namespace
const wchar_t JSON_KEY_CROSSHAIRS_AUTO_HIDE[] = L"crosshairs_auto_hide";
const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled";
const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length";
+ const wchar_t JSON_KEY_CROSSHAIRS_ORIENTATION[] = L"crosshairs_orientation";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_GLIDE_TRAVEL_SPEED[] = L"gliding_travel_speed";
const wchar_t JSON_KEY_GLIDE_DELAY_SPEED[] = L"gliding_delay_speed";
@@ -62,6 +64,9 @@ const static wchar_t* MODULE_NAME = L"MousePointerCrosshairs";
// Add a description that will we shown in the module settings page.
const static wchar_t* MODULE_DESC = L"";
+class MousePointerCrosshairs; // fwd
+static std::atomic g_instance{ nullptr }; // for hook callback
+
// Implement the PowerToy Module Interface and all the required methods.
class MousePointerCrosshairs : public PowertoyModuleIface
{
@@ -70,8 +75,11 @@ private:
bool m_enabled = false;
// Additional hotkeys (legacy API) to support multiple shortcuts
- Hotkey m_activationHotkey{}; // Crosshairs toggle
- Hotkey m_glidingHotkey{}; // Gliding cursor state machine
+ Hotkey m_activationHotkey{}; // Crosshairs toggle
+ Hotkey m_glidingHotkey{}; // Gliding cursor state machine
+
+ // Low-level keyboard hook (Escape to cancel gliding)
+ HHOOK m_keyboardHook = nullptr;
// Shared state for worker threads (decoupled from this lifetime)
struct State
@@ -84,7 +92,7 @@ private:
int currentYPos{ 0 };
int currentXSpeed{ 0 }; // pixels per base window
int currentYSpeed{ 0 }; // pixels per base window
- int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan
+ int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan
// Fractional accumulators to spread movement across 10ms ticks
double xFraction{ 0.0 };
@@ -92,9 +100,9 @@ private:
// Speeds represent pixels per 200ms (min 5, max 60 enforced by UI/settings)
int fastHSpeed{ 30 }; // pixels per base window
- int slowHSpeed{ 5 }; // pixels per base window
+ int slowHSpeed{ 5 }; // pixels per base window
int fastVSpeed{ 30 }; // pixels per base window
- int slowVSpeed{ 5 }; // pixels per base window
+ int slowVSpeed{ 5 }; // pixels per base window
};
std::shared_ptr m_state;
@@ -120,13 +128,16 @@ public:
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName);
m_state = std::make_shared();
init_settings();
+ g_instance.store(this, std::memory_order_release);
};
// Destroy the powertoy and free memory
virtual void destroy() override
{
+ UninstallKeyboardHook();
StopXTimer();
StopYTimer();
+ g_instance.store(nullptr, std::memory_order_release);
// Release shared state so worker threads (if any) exit when weak_ptr lock fails
m_state.reset();
delete this;
@@ -196,6 +207,7 @@ public:
{
m_enabled = false;
Trace::EnableMousePointerCrosshairs(false);
+ UninstallKeyboardHook();
StopXTimer();
StopYTimer();
m_glideState = 0;
@@ -220,7 +232,7 @@ public:
if (buffer && buffer_size >= 2)
{
buffer[0] = m_activationHotkey; // Crosshairs toggle
- buffer[1] = m_glidingHotkey; // Gliding cursor toggle
+ buffer[1] = m_glidingHotkey; // Gliding cursor toggle
}
return 2;
}
@@ -256,6 +268,27 @@ private:
SendInput(2, inputs, sizeof(INPUT));
}
+ // Cancel gliding without performing the final click (Escape handling)
+ void CancelGliding()
+ {
+ int state = m_glideState.load();
+ if (state == 0)
+ {
+ return; // nothing to cancel
+ }
+ StopXTimer();
+ StopYTimer();
+ m_glideState = 0;
+ InclusiveCrosshairsEnsureOff();
+ InclusiveCrosshairsSetExternalControl(false);
+ if (auto s = m_state)
+ {
+ s->xFraction = 0.0;
+ s->yFraction = 0.0;
+ }
+ Logger::debug("Gliding cursor cancelled via Escape key");
+ }
+
// Stateless helpers operating on shared State
static void PositionCursorX(const std::shared_ptr& s)
{
@@ -398,10 +431,14 @@ private:
{
case 0:
{
+ // For detect for cancel key
+ InstallKeyboardHook();
// Ensure crosshairs on (do not toggle off if already on)
InclusiveCrosshairsEnsureOn();
// Disable internal mouse hook so we control position updates explicitly
InclusiveCrosshairsSetExternalControl(true);
+ // Override crosshairs to show both for Gliding Cursor
+ InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both);
s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed;
@@ -444,12 +481,15 @@ private:
case 4:
default:
{
+ UninstallKeyboardHook();
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
StopYTimer();
m_glideState = 0;
LeftClick();
InclusiveCrosshairsEnsureOff();
InclusiveCrosshairsSetExternalControl(false);
+ // Restore original crosshairs orientation setting
+ InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
s->xFraction = 0.0;
s->yFraction = 0.0;
break;
@@ -457,6 +497,51 @@ private:
}
}
+ // Low-level keyboard hook procedures
+ static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
+ {
+ if (nCode == HC_ACTION)
+ {
+ const KBDLLHOOKSTRUCT* kb = reinterpret_cast(lParam);
+ if (kb && kb->vkCode == VK_ESCAPE && (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN))
+ {
+ if (auto inst = g_instance.load(std::memory_order_acquire))
+ {
+ if (inst->m_enabled && inst->m_glideState.load() != 0)
+ {
+ inst->UninstallKeyboardHook();
+ inst->CancelGliding();
+ }
+ }
+ }
+ }
+
+ // Do not swallow Escape; pass it through
+ return CallNextHookEx(nullptr, nCode, wParam, lParam);
+ }
+
+ void InstallKeyboardHook()
+ {
+ if (m_keyboardHook)
+ {
+ return; // already installed
+ }
+ m_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, m_hModule, 0);
+ if (!m_keyboardHook)
+ {
+ Logger::error("Failed to install low-level keyboard hook for MousePointerCrosshairs (Escape cancel). GetLastError={}.", GetLastError());
+ }
+ }
+
+ void UninstallKeyboardHook()
+ {
+ if (m_keyboardHook)
+ {
+ UnhookWindowsHookEx(m_keyboardHook);
+ m_keyboardHook = nullptr;
+ }
+ }
+
// Load the settings file.
void init_settings()
{
@@ -475,264 +560,287 @@ private:
void parse_settings(PowerToysSettings::PowerToyValues& settings)
{
- // TODO: refactor to use common/utils/json.h instead
+ // Refactored JSON parsing: uses inline try-catch blocks for each property for clarity and error handling
auto settingsObject = settings.get_raw_json();
InclusiveCrosshairsSettings inclusiveCrosshairsSettings;
+
if (settingsObject.GetView().Size())
{
try
{
- // Parse primary activation HotKey (for centralized hook)
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
- auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
+ auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
+
+ // Parse activation hotkey
+ try
+ {
+ auto jsonHotkeyObject = propertiesObject.GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
+ auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonHotkeyObject);
+ m_activationHotkey.win = hotkey.win_pressed();
+ m_activationHotkey.ctrl = hotkey.ctrl_pressed();
+ m_activationHotkey.shift = hotkey.shift_pressed();
+ m_activationHotkey.alt = hotkey.alt_pressed();
+ m_activationHotkey.key = static_cast(hotkey.get_code());
+ }
+ catch (...)
+ {
+ Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
+ }
- // Map to legacy Hotkey for multi-hotkey API
- m_activationHotkey.win = hotkey.win_pressed();
- m_activationHotkey.ctrl = hotkey.ctrl_pressed();
- m_activationHotkey.shift = hotkey.shift_pressed();
- m_activationHotkey.alt = hotkey.alt_pressed();
- m_activationHotkey.key = static_cast(hotkey.get_code());
- }
- catch (...)
- {
- Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
- }
- try
- {
- // Parse Gliding Cursor HotKey
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT);
- auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
- m_glidingHotkey.win = hotkey.win_pressed();
- m_glidingHotkey.ctrl = hotkey.ctrl_pressed();
- m_glidingHotkey.shift = hotkey.shift_pressed();
- m_glidingHotkey.alt = hotkey.alt_pressed();
- m_glidingHotkey.key = static_cast(hotkey.get_code());
- }
- catch (...)
- {
- // note that this is also defined in src\settings-ui\Settings.UI.Library\MousePointerCrosshairsProperties.cs, DefaultGlidingCursorActivationShortcut
- // both need to be kept in sync!
- Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+.");
- m_glidingHotkey.win = true;
- m_glidingHotkey.alt = true;
- m_glidingHotkey.ctrl = false;
- m_glidingHotkey.shift = false;
- m_glidingHotkey.key = VK_OEM_PERIOD;
- }
- try
- {
- // Parse Opacity
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY);
- int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
- if (value >= 0)
+ // Parse gliding cursor hotkey
+ try
{
- inclusiveCrosshairsSettings.crosshairsOpacity = value;
+ auto jsonHotkeyObject = propertiesObject.GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT);
+ auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonHotkeyObject);
+ m_glidingHotkey.win = hotkey.win_pressed();
+ m_glidingHotkey.ctrl = hotkey.ctrl_pressed();
+ m_glidingHotkey.shift = hotkey.shift_pressed();
+ m_glidingHotkey.alt = hotkey.alt_pressed();
+ m_glidingHotkey.key = static_cast(hotkey.get_code());
}
- else
+ catch (...)
{
- throw std::runtime_error("Invalid Opacity value");
+ Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+.");
+ m_glidingHotkey.win = true;
+ m_glidingHotkey.alt = true;
+ m_glidingHotkey.ctrl = false;
+ m_glidingHotkey.shift = false;
+ m_glidingHotkey.key = VK_OEM_PERIOD;
+ }
+
+ // Parse individual properties with error handling and defaults
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_opacity"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_opacity");
+ if (propertyObj.HasKey(L"value"))
+ {
+ inclusiveCrosshairsSettings.crosshairsOpacity = static_cast(propertyObj.GetNamedNumber(L"value"));
+ }
+ }
+ }
+ catch (...) { /* Use default value */ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_radius"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_radius");
+ if (propertyObj.HasKey(L"value"))
+ {
+ inclusiveCrosshairsSettings.crosshairsRadius = static_cast(propertyObj.GetNamedNumber(L"value"));
+ }
+ }
+ }
+ catch (...) { /* Use default value */ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_thickness"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_thickness");
+ if (propertyObj.HasKey(L"value"))
+ {
+ inclusiveCrosshairsSettings.crosshairsThickness = static_cast(propertyObj.GetNamedNumber(L"value"));
+ }
+ }
+ }
+ catch (...) { /* Use default value */ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_border_size"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_border_size");
+ if (propertyObj.HasKey(L"value"))
+ {
+ inclusiveCrosshairsSettings.crosshairsBorderSize = static_cast(propertyObj.GetNamedNumber(L"value"));
+ }
+ }
+ }
+ catch (...) { /* Use default value */ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_fixed_length"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_fixed_length");
+ if (propertyObj.HasKey(L"value"))
+ {
+ inclusiveCrosshairsSettings.crosshairsFixedLength = static_cast(propertyObj.GetNamedNumber(L"value"));
+ }
+ }
+ }
+ catch (...) { /* Use default value */ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_auto_hide"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_auto_hide");
+ if (propertyObj.HasKey(L"value"))
+ {
+ inclusiveCrosshairsSettings.crosshairsAutoHide = propertyObj.GetNamedBoolean(L"value");
+ }
+ }
+ }
+ catch (...) { /* Use default value */ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_is_fixed_length_enabled"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_is_fixed_length_enabled");
+ if (propertyObj.HasKey(L"value"))
+ {
+ inclusiveCrosshairsSettings.crosshairsIsFixedLengthEnabled = propertyObj.GetNamedBoolean(L"value");
+ }
+ }
+ }
+ catch (...) { /* Use default value */ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"auto_activate"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"auto_activate");
+ if (propertyObj.HasKey(L"value"))
+ {
+ inclusiveCrosshairsSettings.autoActivate = propertyObj.GetNamedBoolean(L"value");
+ }
+ }
+ }
+ catch (...) { /* Use default value */ }
+
+ // Parse orientation with validation - this fixes the original issue!
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_orientation"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_orientation");
+ if (propertyObj.HasKey(L"value"))
+ {
+ int orientationValue = static_cast(propertyObj.GetNamedNumber(L"value"));
+ if (orientationValue >= 0 && orientationValue <= 2)
+ {
+ inclusiveCrosshairsSettings.crosshairsOrientation = static_cast(orientationValue);
+ }
+ }
+ }
+ }
+ catch (...) { /* Use default value (Both = 0) */ }
+
+ // Parse colors with validation
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_color"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_color");
+ if (propertyObj.HasKey(L"value"))
+ {
+ std::wstring crosshairsColorValue = std::wstring(propertyObj.GetNamedString(L"value").c_str());
+ uint8_t r, g, b;
+ if (checkValidRGB(crosshairsColorValue, &r, &g, &b))
+ {
+ inclusiveCrosshairsSettings.crosshairsColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b);
+ }
+ }
+ }
+ }
+ catch (...) { /* Use default color */ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"crosshairs_border_color"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"crosshairs_border_color");
+ if (propertyObj.HasKey(L"value"))
+ {
+ std::wstring borderColorValue = std::wstring(propertyObj.GetNamedString(L"value").c_str());
+ uint8_t r, g, b;
+ if (checkValidRGB(borderColorValue, &r, &g, &b))
+ {
+ inclusiveCrosshairsSettings.crosshairsBorderColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b);
+ }
+ }
+ }
+ }
+ catch (...) { /* Use default border color */ }
+
+ // Parse speed settings with validation
+ try
+ {
+ if (propertiesObject.HasKey(L"gliding_travel_speed"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"gliding_travel_speed");
+ if (propertyObj.HasKey(L"value") && m_state)
+ {
+ int travelSpeedValue = static_cast(propertyObj.GetNamedNumber(L"value"));
+ if (travelSpeedValue >= 5 && travelSpeedValue <= 60)
+ {
+ m_state->fastHSpeed = travelSpeedValue;
+ m_state->fastVSpeed = travelSpeedValue;
+ }
+ else
+ {
+ // Clamp to valid range
+ int clampedValue = travelSpeedValue;
+ if (clampedValue < 5) clampedValue = 5;
+ if (clampedValue > 60) clampedValue = 60;
+ m_state->fastHSpeed = clampedValue;
+ m_state->fastVSpeed = clampedValue;
+ Logger::warn("Travel speed value out of range, clamped to valid range");
+ }
+ }
+ }
+ }
+ catch (...)
+ {
+ if (m_state)
+ {
+ m_state->fastHSpeed = 25;
+ m_state->fastVSpeed = 25;
+ }
+ }
+
+ try
+ {
+ if (propertiesObject.HasKey(L"gliding_delay_speed"))
+ {
+ auto propertyObj = propertiesObject.GetNamedObject(L"gliding_delay_speed");
+ if (propertyObj.HasKey(L"value") && m_state)
+ {
+ int delaySpeedValue = static_cast(propertyObj.GetNamedNumber(L"value"));
+ if (delaySpeedValue >= 5 && delaySpeedValue <= 60)
+ {
+ m_state->slowHSpeed = delaySpeedValue;
+ m_state->slowVSpeed = delaySpeedValue;
+ }
+ else
+ {
+ // Clamp to valid range
+ int clampedValue = delaySpeedValue;
+ if (clampedValue < 5) clampedValue = 5;
+ if (clampedValue > 60) clampedValue = 60;
+ m_state->slowHSpeed = clampedValue;
+ m_state->slowVSpeed = clampedValue;
+ Logger::warn("Delay speed value out of range, clamped to valid range");
+ }
+ }
+ }
+ }
+ catch (...)
+ {
+ if (m_state)
+ {
+ m_state->slowHSpeed = 5;
+ m_state->slowVSpeed = 5;
+ }
}
}
catch (...)
{
- Logger::warn("Failed to initialize Opacity from settings. Will use default value");
- }
- try
- {
- // Parse crosshairs color
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_COLOR);
- auto crosshairsColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
- uint8_t r, g, b;
- if (!checkValidRGB(crosshairsColor, &r, &g, &b))
- {
- Logger::error("Crosshairs color RGB value is invalid. Will use default value");
- }
- else
- {
- inclusiveCrosshairsSettings.crosshairsColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b);
- }
- }
- catch (...)
- {
- Logger::warn("Failed to initialize crosshairs color from settings. Will use default value");
- }
- try
- {
- // Parse Radius
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_RADIUS);
- int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
- if (value >= 0)
- {
- inclusiveCrosshairsSettings.crosshairsRadius = value;
- }
- else
- {
- throw std::runtime_error("Invalid Radius value");
- }
- }
- catch (...)
- {
- Logger::warn("Failed to initialize Radius from settings. Will use default value");
- }
- try
- {
- // Parse Thickness
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_THICKNESS);
- int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
- if (value >= 0)
- {
- inclusiveCrosshairsSettings.crosshairsThickness = value;
- }
- else
- {
- throw std::runtime_error("Invalid Thickness value");
- }
- }
- catch (...)
- {
- Logger::warn("Failed to initialize Thickness from settings. Will use default value");
- }
- try
- {
- // Parse crosshairs border color
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_COLOR);
- auto crosshairsBorderColor = (std::wstring)jsonPropertiesObject.GetNamedString(JSON_KEY_VALUE);
- uint8_t r, g, b;
- if (!checkValidRGB(crosshairsBorderColor, &r, &g, &b))
- {
- Logger::error("Crosshairs border color RGB value is invalid. Will use default value");
- }
- else
- {
- inclusiveCrosshairsSettings.crosshairsBorderColor = winrt::Windows::UI::ColorHelper::FromArgb(255, r, g, b);
- }
- }
- catch (...)
- {
- Logger::warn("Failed to initialize crosshairs border color from settings. Will use default value");
- }
- try
- {
- // Parse border size
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE);
- int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
- if (value >= 0)
- {
- inclusiveCrosshairsSettings.crosshairsBorderSize = value;
- }
- else
- {
- throw std::runtime_error("Invalid Border Color value");
- }
- }
- catch (...)
- {
- Logger::warn("Failed to initialize border color from settings. Will use default value");
- }
- try
- {
- // Parse auto hide
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_AUTO_HIDE);
- inclusiveCrosshairsSettings.crosshairsAutoHide = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
- }
- catch (...)
- {
- Logger::warn("Failed to initialize auto hide from settings. Will use default value");
- }
- try
- {
- // Parse whether the fixed length is enabled
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED);
- bool value = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
- inclusiveCrosshairsSettings.crosshairsIsFixedLengthEnabled = value;
- }
- catch (...)
- {
- Logger::warn("Failed to initialize fixed length enabled from settings. Will use default value");
- }
- try
- {
- // Parse fixed length
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_FIXED_LENGTH);
- int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
- if (value >= 0)
- {
- inclusiveCrosshairsSettings.crosshairsFixedLength = value;
- }
- else
- {
- throw std::runtime_error("Invalid Fixed Length value");
- }
- }
- catch (...)
- {
- Logger::warn("Failed to initialize fixed length from settings. Will use default value");
- }
- try
- {
- // Parse auto activate
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_AUTO_ACTIVATE);
- inclusiveCrosshairsSettings.autoActivate = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
- }
- catch (...)
- {
- Logger::warn("Failed to initialize auto activate from settings. Will use default value");
- }
- try
- {
- // Parse Travel speed (fast speed mapping)
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_TRAVEL_SPEED);
- int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
- if (value >= 5 && value <= 60)
- {
- m_state->fastHSpeed = value;
- m_state->fastVSpeed = value;
- }
- else if (value < 5)
- {
- m_state->fastHSpeed = 5; m_state->fastVSpeed = 5;
- }
- else
- {
- m_state->fastHSpeed = 60; m_state->fastVSpeed = 60;
- }
- }
- catch (...)
- {
- Logger::warn("Failed to initialize gliding travel speed from settings. Using default 25.");
- if (m_state)
- {
- m_state->fastHSpeed = 25;
- m_state->fastVSpeed = 25;
- }
- }
- try
- {
- // Parse Delay speed (slow speed mapping)
- auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_DELAY_SPEED);
- int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
- if (value >= 5 && value <= 60)
- {
- m_state->slowHSpeed = value;
- m_state->slowVSpeed = value;
- }
- else if (value < 5)
- {
- m_state->slowHSpeed = 5; m_state->slowVSpeed = 5;
- }
- else
- {
- m_state->slowHSpeed = 60; m_state->slowVSpeed = 60;
- }
- }
- catch (...)
- {
- Logger::warn("Failed to initialize gliding delay speed from settings. Using default 5.");
- if (m_state)
- {
- m_state->slowHSpeed = 5;
- m_state->slowVSpeed = 5;
- }
+ Logger::warn("Error parsing some MousePointerCrosshairs properties. Using defaults for failed properties.");
}
}
else
@@ -740,6 +848,7 @@ private:
Logger::info("Mouse Pointer Crosshairs settings are empty");
}
+ // Set default hotkeys if not configured
if (m_activationHotkey.key == 0)
{
m_activationHotkey.win = true;
@@ -756,6 +865,7 @@ private:
m_glidingHotkey.shift = false;
m_glidingHotkey.key = VK_OEM_PERIOD;
}
+
m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings;
}
};
diff --git a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
index 84cf9ed949..1ed96e79bd 100644
--- a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
+++ b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
@@ -258,16 +258,6 @@ private:
{
Logger::info("AlwaysOnTop settings are empty");
}
-
- if (!m_hotkey.key)
- {
- Logger::info("AlwaysOnTop is going to use default shortcut");
- m_hotkey.win = true;
- m_hotkey.alt = false;
- m_hotkey.shift = false;
- m_hotkey.ctrl = true;
- m_hotkey.key = 'T';
- }
}
bool is_process_running()
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj
index b4a8651221..4ace6c5783 100644
--- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj
@@ -9,5 +9,13 @@
+
+
+
+ True
+ True
+ Resources.resx
+
+
diff --git a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Properties/Resources.Designer.cs b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Properties/Resources.Designer.cs
index 81cb6e1b75..ebfc32b862 100644
--- a/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Properties/Resources.Designer.cs
+++ b/src/modules/cmdpal/Core/Microsoft.CmdPal.Core.ViewModels/Properties/Resources.Designer.cs
@@ -69,4 +69,4 @@ namespace Microsoft.CmdPal.Core.ViewModels.Properties {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
index 36b1160ffa..af0eff2181 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
@@ -89,10 +89,11 @@
Visibility="{x:Bind IsText, Mode=OneWay}" />
+ Visibility="{x:Bind IsLink, Mode=OneWay}">
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml
index adf22122d9..882c64e3e7 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml
@@ -11,9 +11,5 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\
true
False
- False
- True
- True
- False
\ No newline at end of file
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml
index 7f6d14d1ad..c686bf808b 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml
@@ -11,9 +11,5 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\
true
False
- False
- True
- True
- False
\ No newline at end of file
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.dark.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.dark.png
new file mode 100644
index 0000000000..ff56efcd57
Binary files /dev/null and b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.dark.png differ
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.dark.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.dark.svg
new file mode 100644
index 0000000000..8253a598d5
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.dark.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.light.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.light.png
new file mode 100644
index 0000000000..12d96a6d0e
Binary files /dev/null and b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.light.png differ
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.light.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.light.svg
new file mode 100644
index 0000000000..311f98cdef
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.light.svg
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.png
deleted file mode 100644
index ce22b2dd9c..0000000000
Binary files a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.png and /dev/null differ
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.svg
deleted file mode 100644
index 5028e6371f..0000000000
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Assets/WebSearch.svg
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Icons.cs
index 10f6fd32c1..856d7614a2 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Icons.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Icons.cs
@@ -6,7 +6,11 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch;
-internal sealed class Icons
+internal static class Icons
{
- internal static IconInfo WebSearch { get; } = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
+ internal static IconInfo WebSearch { get; } = IconHelpers.FromRelativePaths("Assets\\WebSearch.light.png", "Assets\\WebSearch.dark.png");
+
+ internal static IconInfo Search { get; } = new("\uE721");
+
+ internal static IconInfo History { get; } = new("\uE81C");
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
index 6e0a94987b..2f6665d8d0 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
@@ -31,13 +31,7 @@
-
-
-
-
- PreserveNewest
-
-
+
PreserveNewest
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
index 6fe4ae5a7c..641d5f6135 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs
@@ -18,7 +18,6 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{
- private readonly IconInfo _newSearchIcon = new(string.Empty);
private readonly ISettingsInterface _settingsManager;
private readonly Lock _sync = new();
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
@@ -32,7 +31,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
Name = Resources.command_item_title;
Title = Resources.command_item_title;
- Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
+ Icon = Icons.WebSearch;
Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager;
@@ -70,6 +69,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
var historyItem = items[index];
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
{
+ Icon = Icons.History,
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture),
});
@@ -82,7 +82,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
}
}
- private static IListItem[] Query(string query, List historySnapshot, ISettingsInterface settingsManager, IconInfo newSearchIcon)
+ private static IListItem[] Query(string query, List historySnapshot, ISettingsInterface settingsManager)
{
ArgumentNullException.ThrowIfNull(query);
@@ -99,7 +99,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{
Title = searchTerm,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
- Icon = newSearchIcon,
+ Icon = Icons.Search,
};
results.Add(result);
}
@@ -117,7 +117,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
historySnapshot = _historyItems;
}
- var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _newSearchIcon);
+ var items = Query(search ?? string.Empty, historySnapshot, _settingsManager);
lock (_sync)
{
diff --git a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj
index 255ded7abd..34e37eafb2 100644
--- a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj
+++ b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj
@@ -48,7 +48,6 @@
-
Create
@@ -66,6 +65,7 @@
+
@@ -82,4 +82,7 @@
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.cpp b/src/modules/powerrename/lib/PowerRenameRegEx.cpp
index 76136ff39c..34d3cc5c0c 100644
--- a/src/modules/powerrename/lib/PowerRenameRegEx.cpp
+++ b/src/modules/powerrename/lib/PowerRenameRegEx.cpp
@@ -154,8 +154,7 @@ HRESULT CPowerRenameRegEx::_OnEnumerateOrRandomizeItemsChanged()
std::find_if(
m_randomizer.begin(),
m_randomizer.end(),
- [option](const Randomizer& r) -> bool { return r.options.replaceStrSpan.offset == option.replaceStrSpan.offset; }
- ))
+ [option](const Randomizer& r) -> bool { return r.options.replaceStrSpan.offset == option.replaceStrSpan.offset; }))
{
// Only add as enumerator if we didn't find a randomizer already at this offset.
// Every randomizer will also be a valid enumerator according to the definition of enumerators, which allows any string to mean the default enumerator, so it should be interpreted that the user wanted a randomizer if both were found at the same offset of the replace string.
@@ -395,11 +394,8 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
}
std::wstring sourceToUse;
- std::wstring originalSource;
sourceToUse.reserve(MAX_PATH);
- originalSource.reserve(MAX_PATH);
sourceToUse = source;
- originalSource = sourceToUse;
std::wstring searchTerm(m_searchTerm);
std::wstring replaceTerm;
@@ -487,27 +483,46 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
}
}
- bool replacedSomething = false;
+ bool shouldIncrementCounter = false;
+ const bool isCaseInsensitive = !(m_flags & CaseSensitive);
+
if (m_flags & UseRegularExpressions)
{
replaceTerm = regex_replace(replaceTerm, zeroGroupRegex, L"$1$$$0");
replaceTerm = regex_replace(replaceTerm, otherGroupsRegex, L"$1$0$4");
- res = RegexReplaceDispatch[_useBoostLib](source, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, !(m_flags & CaseSensitive));
- replacedSomething = originalSource != res;
+ res = RegexReplaceDispatch[_useBoostLib](source, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, isCaseInsensitive);
+
+ // Use regex search to determine if a match exists. This is the basis for incrementing
+ // the counter.
+ if (_useBoostLib)
+ {
+ boost::wregex pattern(m_searchTerm, boost::wregex::ECMAScript | (isCaseInsensitive ? boost::wregex::icase : boost::wregex::normal));
+ shouldIncrementCounter = boost::regex_search(sourceToUse, pattern);
+ }
+ else
+ {
+ auto regexFlags = std::wregex::ECMAScript;
+ if (isCaseInsensitive)
+ {
+ regexFlags |= std::wregex::icase;
+ }
+ std::wregex pattern(m_searchTerm, regexFlags);
+ shouldIncrementCounter = std::regex_search(sourceToUse, pattern);
+ }
}
else
{
- // Simple search and replace
+ // Simple search and replace.
size_t pos = 0;
do
{
- pos = _Find(sourceToUse, searchTerm, (!(m_flags & CaseSensitive)), pos);
+ pos = _Find(sourceToUse, searchTerm, isCaseInsensitive, pos);
if (pos != std::string::npos)
{
res = sourceToUse.replace(pos, searchTerm.length(), replaceTerm);
pos += replaceTerm.length();
- replacedSomething = true;
+ shouldIncrementCounter = true;
}
if (!(m_flags & MatchAllOccurrences))
{
@@ -516,7 +531,8 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u
} while (pos != std::string::npos);
}
hr = SHStrDup(res.c_str(), result);
- if (replacedSomething)
+
+ if (shouldIncrementCounter)
enumIndex++;
}
catch (regex_error e)
diff --git a/src/modules/powerrename/unittests/CommonRegExTests.h b/src/modules/powerrename/unittests/CommonRegExTests.h
index b1b2b8d731..1b0ad30b92 100644
--- a/src/modules/powerrename/unittests/CommonRegExTests.h
+++ b/src/modules/powerrename/unittests/CommonRegExTests.h
@@ -611,6 +611,42 @@ TEST_METHOD (VerifyRandomizerRegExAllBackToBack)
CoTaskMemFree(result);
}
+TEST_METHOD(VerifyCounterIncrementsWhenResultIsUnchanged)
+{
+ CComPtr renameRegEx;
+ Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
+ DWORD flags = EnumerateItems | UseRegularExpressions;
+ Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
+
+ renameRegEx->PutSearchTerm(L"(.*)");
+ renameRegEx->PutReplaceTerm(L"NewFile-${start=1}");
+
+ PWSTR result = nullptr;
+ unsigned long index = 0;
+
+ renameRegEx->Replace(L"DocA", &result, index);
+ Assert::AreEqual(1ul, index, L"Counter should advance to 1 on first match.");
+ Assert::AreEqual(L"NewFile-1", result, L"First file should be renamed correctly.");
+ CoTaskMemFree(result);
+
+ renameRegEx->Replace(L"DocB", &result, index);
+ Assert::AreEqual(2ul, index, L"Counter should advance to 2 on second match.");
+ Assert::AreEqual(L"NewFile-2", result, L"Second file should be renamed correctly.");
+ CoTaskMemFree(result);
+
+ // The original term and the replacement are identical.
+ renameRegEx->Replace(L"NewFile-3", &result, index);
+ Assert::AreEqual(3ul, index, L"Counter must advance on a match, even if the new name is identical to the old one.");
+ Assert::AreEqual(L"NewFile-3", result, L"Filename should be unchanged on a coincidental match.");
+ CoTaskMemFree(result);
+
+ // Test that there wasn't a "stall" in the numbering.
+ renameRegEx->Replace(L"DocC", &result, index);
+ Assert::AreEqual(4ul, index, L"Counter should continue sequentially after the coincidental match.");
+ Assert::AreEqual(L"NewFile-4", result, L"The subsequent file should receive the correct next number.");
+ CoTaskMemFree(result);
+}
+
#ifndef TESTS_PARTIAL
};
}
diff --git a/src/runner/general_settings.cpp b/src/runner/general_settings.cpp
index bb45f7f5ae..50dd8dbbc8 100644
--- a/src/runner/general_settings.cpp
+++ b/src/runner/general_settings.cpp
@@ -14,6 +14,29 @@
#include
#include
+namespace
+{
+ json::JsonValue create_empty_shortcut_array_value()
+ {
+ return json::JsonValue::Parse(L"[]");
+ }
+
+ void ensure_ignored_conflict_properties_shape(json::JsonObject& obj)
+ {
+ if (!json::has(obj, L"ignored_shortcuts", json::JsonValueType::Array))
+ {
+ obj.SetNamedValue(L"ignored_shortcuts", create_empty_shortcut_array_value());
+ }
+ }
+
+ json::JsonObject create_default_ignored_conflict_properties()
+ {
+ json::JsonObject obj;
+ ensure_ignored_conflict_properties_shape(obj);
+ return obj;
+ }
+}
+
// TODO: would be nice to get rid of these globals, since they're basically cached json settings
static std::wstring settings_theme = L"system";
static bool show_tray_icon = true;
@@ -23,11 +46,15 @@ static bool download_updates_automatically = true;
static bool show_whats_new_after_updates = true;
static bool enable_experimentation = true;
static bool enable_warnings_elevated_apps = true;
+static json::JsonObject ignored_conflict_properties = create_default_ignored_conflict_properties();
json::JsonObject GeneralSettings::to_json()
{
json::JsonObject result;
+ auto ignoredProps = ignoredConflictProperties;
+ ensure_ignored_conflict_properties_shape(ignoredProps);
+
result.SetNamedValue(L"startup", json::value(isStartupEnabled));
if (!startupDisabledReason.empty())
{
@@ -53,6 +80,7 @@ json::JsonObject GeneralSettings::to_json()
result.SetNamedValue(L"theme", json::value(theme));
result.SetNamedValue(L"system_theme", json::value(systemTheme));
result.SetNamedValue(L"powertoys_version", json::value(powerToysVersion));
+ result.SetNamedValue(L"ignored_conflict_properties", json::value(ignoredProps));
return result;
}
@@ -72,6 +100,17 @@ json::JsonObject load_general_settings()
enable_experimentation = loaded.GetNamedBoolean(L"enable_experimentation", true);
enable_warnings_elevated_apps = loaded.GetNamedBoolean(L"enable_warnings_elevated_apps", true);
+ if (json::has(loaded, L"ignored_conflict_properties", json::JsonValueType::Object))
+ {
+ ignored_conflict_properties = loaded.GetNamedObject(L"ignored_conflict_properties");
+ }
+ else
+ {
+ ignored_conflict_properties = create_default_ignored_conflict_properties();
+ }
+
+ ensure_ignored_conflict_properties_shape(ignored_conflict_properties);
+
return loaded;
}
@@ -91,9 +130,12 @@ GeneralSettings get_general_settings()
.enableExperimentation = enable_experimentation,
.theme = settings_theme,
.systemTheme = WindowsColors::is_dark_mode() ? L"dark" : L"light",
- .powerToysVersion = get_product_version()
+ .powerToysVersion = get_product_version(),
+ .ignoredConflictProperties = ignored_conflict_properties
};
+ ensure_ignored_conflict_properties_shape(settings.ignoredConflictProperties);
+
settings.isStartupEnabled = is_auto_start_task_active_for_this_user();
for (auto& [name, powertoy] : modules())
@@ -232,6 +274,12 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
set_tray_icon_visible(show_tray_icon);
}
+ if (json::has(general_configs, L"ignored_conflict_properties", json::JsonValueType::Object))
+ {
+ ignored_conflict_properties = general_configs.GetNamedObject(L"ignored_conflict_properties");
+ ensure_ignored_conflict_properties_shape(ignored_conflict_properties);
+ }
+
if (save)
{
GeneralSettings save_settings = get_general_settings();
diff --git a/src/runner/general_settings.h b/src/runner/general_settings.h
index ef2224b132..38fbd5789a 100644
--- a/src/runner/general_settings.h
+++ b/src/runner/general_settings.h
@@ -19,6 +19,7 @@ struct GeneralSettings
std::wstring theme;
std::wstring systemTheme;
std::wstring powerToysVersion;
+ json::JsonObject ignoredConflictProperties;
json::JsonObject to_json();
};
diff --git a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs
index 3d295284e3..24ff4584fe 100644
--- a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs
@@ -76,6 +76,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("enable_experimentation")]
public bool EnableExperimentation { get; set; }
+ [JsonPropertyName("ignored_conflict_properties")]
+ public ShortcutConflictProperties IgnoredConflictProperties { get; set; }
+
public GeneralSettings()
{
Startup = false;
@@ -100,6 +103,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Enabled = new EnabledModules();
CustomActionName = string.Empty;
+ IgnoredConflictProperties = new ShortcutConflictProperties();
}
// converts the current to a json string.
@@ -137,6 +141,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
// If there is an issue with the version number format, don't migrate settings.
}
+ // Ensure IgnoredConflictProperties is initialized (for backward compatibility)
+ if (IgnoredConflictProperties == null)
+ {
+ IgnoredConflictProperties = new ShortcutConflictProperties();
+ return true; // Indicate that settings were upgraded
+ }
+
return false;
}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs
index a420ec7a2b..0c76ddf8ea 100644
--- a/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs
+++ b/src/settings-ui/Settings.UI.Library/HotkeyConflicts/HotkeyConflictGroupData.cs
@@ -2,11 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
{
@@ -16,6 +12,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts
public bool IsSystemConflict { get; set; }
+ public bool ConflictIgnored { get; set; }
+
+ public bool ConflictVisible => !ConflictIgnored;
+
+ public bool ShouldShowSysConflict => !ConflictIgnored && IsSystemConflict;
+
public List Modules { get; set; }
}
}
diff --git a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs
index 724e1b5159..b5fa41fcf6 100644
--- a/src/settings-ui/Settings.UI.Library/HotkeySettings.cs
+++ b/src/settings-ui/Settings.UI.Library/HotkeySettings.cs
@@ -20,6 +20,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
private bool _hasConflict;
private string _conflictDescription;
private bool _isSystemConflict;
+ private bool _ignoreConflict;
public event PropertyChangedEventHandler PropertyChanged;
@@ -57,6 +58,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library
HasConflict = false;
}
+ [JsonIgnore]
+ public bool IgnoreConflict
+ {
+ get => _ignoreConflict;
+ set
+ {
+ if (_ignoreConflict != value)
+ {
+ _ignoreConflict = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ [JsonIgnore]
public bool HasConflict
{
get => _hasConflict;
@@ -70,9 +86,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
+ [JsonIgnore]
public string ConflictDescription
{
- get => _conflictDescription ?? string.Empty;
+ get => _ignoreConflict ? null : _conflictDescription;
set
{
if (_conflictDescription != value)
@@ -83,6 +100,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
+ [JsonIgnore]
public bool IsSystemConflict
{
get => _isSystemConflict;
diff --git a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs
index 54542194c0..83427a9f30 100644
--- a/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/MousePointerCrosshairsProperties.cs
@@ -40,6 +40,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("crosshairs_border_size")]
public IntProperty CrosshairsBorderSize { get; set; }
+ [JsonPropertyName("crosshairs_orientation")]
+ public IntProperty CrosshairsOrientation { get; set; }
+
[JsonPropertyName("crosshairs_auto_hide")]
public BoolProperty CrosshairsAutoHide { get; set; }
@@ -68,6 +71,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
CrosshairsThickness = new IntProperty(5);
CrosshairsBorderColor = new StringProperty("#FFFFFF");
CrosshairsBorderSize = new IntProperty(1);
+ CrosshairsOrientation = new IntProperty(0); // Default to both (0=Both, 1=Vertical, 2=Horizontal)
CrosshairsAutoHide = new BoolProperty(false);
CrosshairsIsFixedLengthEnabled = new BoolProperty(false);
CrosshairsFixedLength = new IntProperty(1);
diff --git a/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs b/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs
index 316fbfd626..10ebf74314 100644
--- a/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs
+++ b/src/settings-ui/Settings.UI.Library/SettingsBackupAndRestoreUtils.cs
@@ -653,11 +653,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library
return (false, "General_SettingsBackupAndRestore_InvalidBackupLocation", "Error", lastBackupExists, "\n" + appBasePath);
}
- var dirExists = TryCreateDirectory(settingsBackupAndRestoreDir);
- if (!dirExists)
+ // Only create the backup directory if this is not a dry run
+ if (!dryRun)
{
- Logger.LogError($"Failed to create dir {settingsBackupAndRestoreDir}");
- return (false, $"General_SettingsBackupAndRestore_BackupError", "Error", lastBackupExists, "\n" + settingsBackupAndRestoreDir);
+ var dirExists = TryCreateDirectory(settingsBackupAndRestoreDir);
+ if (!dirExists)
+ {
+ Logger.LogError($"Failed to create dir {settingsBackupAndRestoreDir}");
+ return (false, $"General_SettingsBackupAndRestore_BackupError", "Error", lastBackupExists, "\n" + settingsBackupAndRestoreDir);
+ }
}
// get data needed for process
@@ -717,12 +721,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
var relativePath = currentFile.Value.Substring(appBasePath.Length + 1);
var backupFullPath = Path.Combine(fullBackupDir, relativePath);
- TryCreateDirectory(fullBackupDir);
- TryCreateDirectory(Path.GetDirectoryName(backupFullPath));
-
Logger.LogInfo($"BackupSettings writing, {backupFullPath}, dryRun:{dryRun}.");
if (!dryRun)
{
+ TryCreateDirectory(fullBackupDir);
+ TryCreateDirectory(Path.GetDirectoryName(backupFullPath));
File.WriteAllText(backupFullPath, currentSettingsFileToBackup);
}
}
diff --git a/src/settings-ui/Settings.UI.Library/ShortcutConflictProperties.cs b/src/settings-ui/Settings.UI.Library/ShortcutConflictProperties.cs
new file mode 100644
index 0000000000..7ce5fb5b1f
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/ShortcutConflictProperties.cs
@@ -0,0 +1,20 @@
+// 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.Serialization;
+
+namespace Microsoft.PowerToys.Settings.UI.Library
+{
+ public class ShortcutConflictProperties
+ {
+ [JsonPropertyName("ignored_shortcuts")]
+ public List IgnoredShortcuts { get; set; }
+
+ public ShortcutConflictProperties()
+ {
+ IgnoredShortcuts = new List();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Converters/BoolToKeyVisualStateConverter.cs b/src/settings-ui/Settings.UI/Converters/BoolToKeyVisualStateConverter.cs
new file mode 100644
index 0000000000..04a62b02c7
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Converters/BoolToKeyVisualStateConverter.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.Settings.UI.Controls;
+using Microsoft.UI.Xaml.Data;
+
+namespace Microsoft.PowerToys.Settings.UI.Converters
+{
+ public partial class BoolToKeyVisualStateConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ if (value is bool b && parameter is string param)
+ {
+ if (b && param == "Warning")
+ {
+ return State.Warning;
+ }
+ else if (b && param == "Error")
+ {
+ return State.Error;
+ }
+ else
+ {
+ return State.Normal;
+ }
+ }
+ else
+ {
+ return State.Normal;
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs
new file mode 100644
index 0000000000..d2e737180a
--- /dev/null
+++ b/src/settings-ui/Settings.UI/Helpers/HotkeyConflictIgnoreHelper.cs
@@ -0,0 +1,229 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+using Microsoft.PowerToys.Settings.UI.Views;
+
+namespace Microsoft.PowerToys.Settings.UI.Helpers
+{
+ ///
+ /// Static helper class to manage and check hotkey conflict ignore settings
+ ///
+ public static class HotkeyConflictIgnoreHelper
+ {
+ private static readonly ISettingsRepository _generalSettingsRepository;
+ private static readonly ISettingsUtils _settingsUtils;
+
+ static HotkeyConflictIgnoreHelper()
+ {
+ _settingsUtils = new SettingsUtils();
+ _generalSettingsRepository = SettingsRepository.GetInstance(_settingsUtils);
+ }
+
+ ///
+ /// Ensures ignored conflict properties are initialized
+ ///
+ private static void EnsureInitialized()
+ {
+ var settings = _generalSettingsRepository.SettingsConfig;
+ if (settings.IgnoredConflictProperties == null)
+ {
+ settings.IgnoredConflictProperties = new ShortcutConflictProperties();
+ SaveSettings();
+ }
+ }
+
+ ///
+ /// Checks if a specific hotkey setting is configured to ignore conflicts
+ ///
+ /// The hotkey settings to check
+ /// True if the hotkey is set to ignore conflicts, false otherwise
+ public static bool IsIgnoringConflicts(HotkeySettings hotkeySettings)
+ {
+ if (hotkeySettings == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ EnsureInitialized();
+ var settings = _generalSettingsRepository.SettingsConfig;
+ return settings.IgnoredConflictProperties.IgnoredShortcuts
+ .Any(h => AreHotkeySettingsEqual(h, hotkeySettings));
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error checking if hotkey is ignoring conflicts: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Adds a hotkey setting to the ignored shortcuts list
+ ///
+ /// The hotkey settings to add to the ignored list
+ /// True if successfully added, false if it was already ignored or on error
+ public static bool AddToIgnoredList(HotkeySettings hotkeySettings)
+ {
+ if (hotkeySettings == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ EnsureInitialized();
+ var settings = _generalSettingsRepository.SettingsConfig;
+
+ // Check if already ignored (avoid duplicates)
+ if (IsIgnoringConflicts(hotkeySettings))
+ {
+ Logger.LogInfo($"Hotkey already in ignored list: {hotkeySettings}");
+ return false;
+ }
+
+ // Add to ignored list
+ settings.IgnoredConflictProperties.IgnoredShortcuts.Add(hotkeySettings);
+ SaveSettings();
+
+ Logger.LogInfo($"Added hotkey to ignored list: {hotkeySettings}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error adding hotkey to ignored list: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Removes a hotkey setting from the ignored shortcuts list
+ ///
+ /// The hotkey settings to remove from the ignored list
+ /// True if successfully removed, false if it wasn't in the list or on error
+ public static bool RemoveFromIgnoredList(HotkeySettings hotkeySettings)
+ {
+ if (hotkeySettings == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ EnsureInitialized();
+ var settings = _generalSettingsRepository.SettingsConfig;
+ var ignoredShortcut = settings.IgnoredConflictProperties.IgnoredShortcuts
+ .FirstOrDefault(h => AreHotkeySettingsEqual(h, hotkeySettings));
+
+ if (ignoredShortcut != null)
+ {
+ settings.IgnoredConflictProperties.IgnoredShortcuts.Remove(ignoredShortcut);
+ SaveSettings();
+
+ Logger.LogInfo($"Removed hotkey from ignored list: {ignoredShortcut}");
+ return true;
+ }
+
+ Logger.LogInfo($"Hotkey not found in ignored list: {hotkeySettings}");
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error removing hotkey from ignored list: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Gets all hotkey settings that are currently being ignored
+ ///
+ /// List of ignored hotkey settings
+ public static List GetAllIgnoredShortcuts()
+ {
+ try
+ {
+ EnsureInitialized();
+ var settings = _generalSettingsRepository.SettingsConfig;
+ return new List(settings.IgnoredConflictProperties.IgnoredShortcuts);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error getting ignored shortcuts: {ex.Message}");
+ return new List();
+ }
+ }
+
+ ///
+ /// Clears all ignored shortcuts from the list
+ ///
+ /// True if successfully cleared, false on error
+ public static bool ClearAllIgnoredShortcuts()
+ {
+ try
+ {
+ EnsureInitialized();
+ var settings = _generalSettingsRepository.SettingsConfig;
+ var count = settings.IgnoredConflictProperties.IgnoredShortcuts.Count;
+ settings.IgnoredConflictProperties.IgnoredShortcuts.Clear();
+ SaveSettings();
+
+ Logger.LogInfo($"Cleared all {count} ignored shortcuts");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error clearing ignored shortcuts: {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// Compares two HotkeySettings for equality
+ ///
+ /// First hotkey settings
+ /// Second hotkey settings
+ /// True if they represent the same shortcut, false otherwise
+ private static bool AreHotkeySettingsEqual(HotkeySettings hotkey1, HotkeySettings hotkey2)
+ {
+ if (hotkey1 == null || hotkey2 == null)
+ {
+ return false;
+ }
+
+ return hotkey1.Win == hotkey2.Win &&
+ hotkey1.Ctrl == hotkey2.Ctrl &&
+ hotkey1.Alt == hotkey2.Alt &&
+ hotkey1.Shift == hotkey2.Shift &&
+ hotkey1.Code == hotkey2.Code;
+ }
+
+ ///
+ /// Saves the general settings using PowerToys standard settings persistence
+ ///
+ private static void SaveSettings()
+ {
+ try
+ {
+ var settings = _generalSettingsRepository.SettingsConfig;
+
+ // Send IPC message to notify runner of changes (this is thread-safe)
+ var outgoing = new OutGoingGeneralSettings(settings);
+ ShellPage.SendDefaultIPCMessage(outgoing.ToString());
+ ShellPage.ShellHandler?.SignalGeneralDataUpdate();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error saving shortcut conflict settings: {ex.Message}");
+ Logger.LogError($"Stack trace: {ex.StackTrace}");
+ throw;
+ }
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
index 636ed2abec..838149a04e 100644
--- a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
+++ b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs
@@ -35,6 +35,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(PowerOcrSettings))]
[JsonSerializable(typeof(RegistryPreviewSettings))]
+[JsonSerializable(typeof(ShortcutConflictProperties))]
[JsonSerializable(typeof(ShortcutGuideSettings))]
[JsonSerializable(typeof(WINDOWPLACEMENT))]
[JsonSerializable(typeof(WorkspacesSettings))]
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml
index 69a7a1084d..b071e7f6fe 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml
@@ -8,7 +8,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
-
+
@@ -16,10 +16,10 @@
+ Glyph="" />
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs
index 7195b159e1..d7806f17ea 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml.cs
@@ -47,12 +47,24 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
int count = 0;
if (AllHotkeyConflictsData.InAppConflicts != null)
{
- count += AllHotkeyConflictsData.InAppConflicts.Count;
+ foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts)
+ {
+ if (!inAppConflict.ConflictIgnored)
+ {
+ count++;
+ }
+ }
}
if (AllHotkeyConflictsData.SystemConflicts != null)
{
- count += AllHotkeyConflictsData.SystemConflicts.Count;
+ foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts)
+ {
+ if (!systemConflict.ConflictIgnored)
+ {
+ count++;
+ }
+ }
}
return count;
@@ -95,7 +107,14 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
OnPropertyChanged(nameof(HasConflicts));
// Update visibility based on conflict count
- Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed;
+ if (HasConflicts)
+ {
+ VisualStateManager.GoToState(this, "ConflictState", true);
+ }
+ else
+ {
+ VisualStateManager.GoToState(this, "NoConflictState", true);
+ }
if (!_telemetryEventSent && HasConflicts)
{
@@ -119,13 +138,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
InitializeComponent();
DataContext = this;
- // Initially hide the control if no conflicts
- Visibility = HasConflicts ? Visibility.Visible : Visibility.Collapsed;
+ UpdateProperties();
}
private void ShortcutConflictBtn_Click(object sender, RoutedEventArgs e)
{
- if (AllHotkeyConflictsData == null || !HasConflicts)
+ if (AllHotkeyConflictsData == null)
{
return;
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml
index 46f8d4f962..11d9b5f7b0 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml
@@ -53,34 +53,22 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -97,22 +85,40 @@
-
+
-
+
+
+
+
-
+
@@ -137,15 +143,15 @@
+ Background="Transparent"
+ BorderThickness="0,1,0,0"
+ CornerRadius="0"
+ IsEnabled="{x:Bind ShouldShowSysConflict, Mode=OneWay}">
-
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs
index 5bcc282261..b9bee4ff08 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictWindow.xaml.cs
@@ -14,6 +14,7 @@ using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
using Windows.Graphics;
using WinUIEx;
@@ -21,8 +22,6 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
{
public sealed partial class ShortcutConflictWindow : WindowEx
{
- public ShortcutConflictViewModel DataContext { get; }
-
public ShortcutConflictViewModel ViewModel { get; private set; }
public ShortcutConflictWindow()
@@ -33,14 +32,17 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
SettingsRepository.GetInstance(settingsUtils),
ShellPage.SendDefaultIPCMessage);
- DataContext = ViewModel;
InitializeComponent();
+ // Set DataContext on the root Grid instead of the Window
+ RootGrid.DataContext = ViewModel;
+
this.Activated += Window_Activated_SetIcon;
// Set localized window title
var resourceLoader = ResourceLoaderInstance.ResourceLoader;
- this.ExtendsContentIntoTitleBar = true;
+ ExtendsContentIntoTitleBar = true;
+ SetTitleBar(titleBar);
this.Title = resourceLoader.GetString("ShortcutConflictWindow_Title");
this.CenterOnScreen();
@@ -74,6 +76,54 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
}
}
+ private void OnIgnoreConflictClicked(object sender, RoutedEventArgs e)
+ {
+ if (sender is CheckBox checkBox && checkBox.DataContext is HotkeyConflictGroupData conflictGroup)
+ {
+ // The Click event only fires from user interaction, not programmatic changes
+ if (checkBox.IsChecked == true)
+ {
+ IgnoreConflictGroup(conflictGroup);
+ }
+ else
+ {
+ UnignoreConflictGroup(conflictGroup);
+ }
+ }
+ }
+
+ private void IgnoreConflictGroup(HotkeyConflictGroupData conflictGroup)
+ {
+ try
+ {
+ // Ignore all hotkey settings in this conflict group
+ if (conflictGroup.Modules != null)
+ {
+ HotkeySettings hotkey = new(conflictGroup.Hotkey.Win, conflictGroup.Hotkey.Ctrl, conflictGroup.Hotkey.Alt, conflictGroup.Hotkey.Shift, conflictGroup.Hotkey.Key);
+ ViewModel.IgnoreShortcut(hotkey);
+ }
+ }
+ catch
+ {
+ }
+ }
+
+ private void UnignoreConflictGroup(HotkeyConflictGroupData conflictGroup)
+ {
+ try
+ {
+ // Unignore all hotkey settings in this conflict group
+ if (conflictGroup.Modules != null)
+ {
+ HotkeySettings hotkey = new(conflictGroup.Hotkey.Win, conflictGroup.Hotkey.Ctrl, conflictGroup.Hotkey.Alt, conflictGroup.Hotkey.Shift, conflictGroup.Hotkey.Key);
+ ViewModel.UnignoreShortcut(hotkey);
+ }
+ }
+ catch
+ {
+ }
+ }
+
private void WindowEx_Closed(object sender, WindowEventArgs args)
{
ViewModel?.Dispose();
@@ -82,10 +132,7 @@ namespace Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard
private void Window_Activated_SetIcon(object sender, WindowActivatedEventArgs args)
{
// Set window icon
- var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
- WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd);
- AppWindow appWindow = AppWindow.GetFromWindowId(windowId);
- appWindow.SetIcon("Assets\\Settings\\icon.ico");
+ AppWindow.SetIcon("Assets\\Settings\\icon.ico");
}
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
index 931286ceaf..1228911082 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml
@@ -63,10 +63,18 @@
-
+
+
+
+
+
+
+
+
+
@@ -120,6 +128,11 @@
+
+
+
+
+
@@ -177,10 +190,18 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs
index b638c32f2b..87dc9a4c21 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs
@@ -12,12 +12,14 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
[TemplateVisualState(Name = NormalState, GroupName = "CommonStates")]
[TemplateVisualState(Name = DisabledState, GroupName = "CommonStates")]
[TemplateVisualState(Name = InvalidState, GroupName = "CommonStates")]
+ [TemplateVisualState(Name = WarningState, GroupName = "CommonStates")]
public sealed partial class KeyVisual : Control
{
private const string KeyPresenter = "KeyPresenter";
private const string NormalState = "Normal";
private const string DisabledState = "Disabled";
private const string InvalidState = "Invalid";
+ private const string WarningState = "Warning";
private KeyCharPresenter _keyPresenter;
public object Content
@@ -28,13 +30,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged));
- public bool IsInvalid
+ public State State
{
- get => (bool)GetValue(IsInvalidProperty);
- set => SetValue(IsInvalidProperty, value);
+ get => (State)GetValue(StateProperty);
+ set => SetValue(StateProperty, value);
}
- public static readonly DependencyProperty IsInvalidProperty = DependencyProperty.Register(nameof(IsInvalid), typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnIsInvalidChanged));
+ public static readonly DependencyProperty StateProperty = DependencyProperty.Register(nameof(State), typeof(State), typeof(KeyVisual), new PropertyMetadata(State.Normal, OnStateChanged));
public bool RenderKeyAsGlyph
{
@@ -64,7 +66,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
((KeyVisual)d).SetVisualStates();
}
- private static void OnIsInvalidChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ private static void OnStateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).SetVisualStates();
}
@@ -73,10 +75,14 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
if (this != null)
{
- if (IsInvalid)
+ if (State == State.Error)
{
VisualStateManager.GoToState(this, InvalidState, true);
}
+ else if (State == State.Warning)
+ {
+ VisualStateManager.GoToState(this, WarningState, true);
+ }
else if (!IsEnabled)
{
VisualStateManager.GoToState(this, DisabledState, true);
@@ -177,4 +183,11 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
SetVisualStates();
}
}
+
+ public enum State
+ {
+ Normal,
+ Error,
+ Warning,
+ }
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml
index d81be4aa6c..b7983585ac 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml
@@ -6,11 +6,14 @@
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
x:Name="LayoutRoot"
d:DesignHeight="300"
d:DesignWidth="400"
mc:Ignorable="d">
-
+
+
+
@@ -49,6 +52,7 @@
-
+ Text="" />
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs
index 6b4b9b7957..ba053e1124 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs
@@ -12,6 +12,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
using Microsoft.PowerToys.Settings.UI.Services;
+using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
@@ -51,6 +52,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public static readonly DependencyProperty AllowDisableProperty = DependencyProperty.Register("AllowDisable", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnAllowDisableChanged));
public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnHasConflictChanged));
public static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip", typeof(string), typeof(ShortcutControl), new PropertyMetadata(null, OnTooltipChanged));
+ public static readonly DependencyProperty KeyVisualShouldShowConflictProperty = DependencyProperty.Register("KeyVisualShouldShowConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false));
+ public static readonly DependencyProperty IgnoreConflictProperty = DependencyProperty.Register("IgnoreConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false));
// Dependency property to track the source/context of the ShortcutControl
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ShortcutControlSource), typeof(ShortcutControl), new PropertyMetadata(ShortcutControlSource.SettingsPage));
@@ -161,6 +164,18 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set => SetValue(TooltipProperty, value);
}
+ public bool KeyVisualShouldShowConflict
+ {
+ get => (bool)GetValue(KeyVisualShouldShowConflictProperty);
+ set => SetValue(KeyVisualShouldShowConflictProperty, value);
+ }
+
+ public bool IgnoreConflict
+ {
+ get => (bool)GetValue(IgnoreConflictProperty);
+ set => SetValue(IgnoreConflictProperty, value);
+ }
+
public ShortcutControlSource Source
{
get => (ShortcutControlSource)GetValue(SourceProperty);
@@ -241,6 +256,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
// Update the ShortcutControl's conflict properties from HotkeySettings
HasConflict = hotkeySettings.HasConflict;
Tooltip = hotkeySettings.HasConflict ? hotkeySettings.ConflictDescription : null;
+ IgnoreConflict = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySettings);
+ KeyVisualShouldShowConflict = !IgnoreConflict && HasConflict;
}
else
{
@@ -257,6 +274,10 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
this.Unloaded += ShortcutControl_Unloaded;
this.Loaded += ShortcutControl_Loaded;
+ c.ResetClick += C_ResetClick;
+ c.ClearClick += C_ClearClick;
+ c.LearnMoreClick += C_LearnMoreClick;
+
// We create the Dialog in C# because doing it in XAML is giving WinUI/XAML Island bugs when using dark theme.
shortcutDialog = new ContentDialog
{
@@ -264,11 +285,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
Title = resourceLoader.GetString("Activation_Shortcut_Title"),
Content = c,
PrimaryButtonText = resourceLoader.GetString("Activation_Shortcut_Save"),
- SecondaryButtonText = resourceLoader.GetString("Activation_Shortcut_Reset"),
CloseButtonText = resourceLoader.GetString("Activation_Shortcut_Cancel"),
DefaultButton = ContentDialogButton.Primary,
};
- shortcutDialog.SecondaryButtonClick += ShortcutDialog_Reset;
shortcutDialog.RightTapped += ShortcutDialog_Disable;
AutomationProperties.SetName(EditButton, resourceLoader.GetString("Activation_Shortcut_Title"));
@@ -276,6 +295,16 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
OnAllowDisableChanged(this, null);
}
+ private void C_LearnMoreClick(object sender, RoutedEventArgs e)
+ {
+ // Close the current shortcut dialog
+ shortcutDialog.Hide();
+
+ // Create and show the ShortcutConflictWindow
+ var conflictWindow = new ShortcutConflictWindow();
+ conflictWindow.Activate();
+ }
+
private void UpdateKeyVisualStyles()
{
if (PreviewKeysControl?.ItemsSource != null)
@@ -305,6 +334,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
shortcutDialog.Opened -= ShortcutDialog_Opened;
shortcutDialog.Closing -= ShortcutDialog_Closing;
+ c.LearnMoreClick -= C_LearnMoreClick;
+
if (App.GetSettingsWindow() != null)
{
App.GetSettingsWindow().Activated -= ShortcutDialog_SettingsWindow_Activated;
@@ -510,6 +541,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
else
{
EnableKeys();
+
if (lastValidSettings.IsValid())
{
if (string.Equals(lastValidSettings.ToString(), hotkeySettings.ToString(), StringComparison.OrdinalIgnoreCase))
@@ -578,16 +610,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
shortcutDialog.IsPrimaryButtonEnabled = true;
c.IsError = false;
-
- // WarningLabel.Style = (Style)App.Current.Resources["SecondaryTextStyle"];
}
private void DisableKeys()
{
shortcutDialog.IsPrimaryButtonEnabled = false;
c.IsError = true;
-
- // WarningLabel.Style = (Style)App.Current.Resources["SecondaryWarningTextStyle"];
}
private void Hotkey_KeyUp(int key)
@@ -648,6 +676,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
c.Keys = null;
c.Keys = HotkeySettings.GetKeysList();
+ c.IgnoreConflict = IgnoreConflict;
c.HasConflict = hotkeySettings.HasConflict;
c.ConflictMessage = hotkeySettings.ConflictDescription;
@@ -660,7 +689,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
await shortcutDialog.ShowAsync();
}
- private void ShortcutDialog_Reset(ContentDialog sender, ContentDialogButtonClickEventArgs args)
+ private void C_ResetClick(object sender, RoutedEventArgs e)
{
hotkeySettings = null;
@@ -674,6 +703,20 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
}
+ private void C_ClearClick(object sender, RoutedEventArgs e)
+ {
+ hotkeySettings = new HotkeySettings();
+
+ SetValue(HotkeySettingsProperty, hotkeySettings);
+ SetKeys();
+
+ lastValidSettings = hotkeySettings;
+ shortcutDialog.Hide();
+
+ // Send RequestAllConflicts IPC to update the UI after changed hotkey settings.
+ GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
+ }
+
private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (ComboIsValid(lastValidSettings))
@@ -728,7 +771,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
args.Handled = true;
if (args.WindowActivationState != WindowActivationState.Deactivated && (hook == null || hook.GetDisposedState() == true))
{
- // If the PT settings window gets focussed/activated again, we enable the keyboard hook to catch the keyboard input.
+ // If the PT settings window gets focused/activated again, we enable the keyboard hook to catch the keyboard input.
hook = new HotkeySettingsControlHook(Hotkey_KeyDown, Hotkey_KeyUp, Hotkey_IsActive, FilterAccessibleKeyboardEvents);
}
else if (args.WindowActivationState == WindowActivationState.Deactivated && hook != null && hook.GetDisposedState() == false)
@@ -742,6 +785,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
private void ShortcutDialog_Closing(ContentDialog sender, ContentDialogClosingEventArgs args)
{
_isActive = false;
+ lastValidSettings = hotkeySettings;
}
private void Dispose(bool disposing)
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml
index 3f345b8650..31e2f742e6 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml
@@ -3,78 +3,332 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls"
+ xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
+ xmlns:tk7controls="using:CommunityToolkit.WinUI.Controls"
+ xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
x:Name="ShortcutContentControl"
mc:Ignorable="d">
-
+
+
+
+
+
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+ Margin="0,16,0,0"
+ Background="{ThemeResource SolidBackgroundFillColorTertiaryBrush}"
+ CornerRadius="{StaticResource OverlayCornerRadius}">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ HorizontalAlignment="Center"
+ Orientation="Horizontal"
+ Spacing="12">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs
index 8907f12415..9a369f0ebc 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs
@@ -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;
using System.Collections.Generic;
-
+using System.Diagnostics.Eventing.Reader;
+using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -14,8 +16,22 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutDialogContentControl), new PropertyMetadata(default(string)));
public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
- public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
+ public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false, OnConflictPropertyChanged));
public static readonly DependencyProperty ConflictMessageProperty = DependencyProperty.Register("ConflictMessage", typeof(string), typeof(ShortcutDialogContentControl), new PropertyMetadata(string.Empty));
+ public static readonly DependencyProperty IgnoreConflictProperty = DependencyProperty.Register("IgnoreConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false, OnIgnoreConflictChanged));
+
+ public static readonly DependencyProperty ShouldShowConflictProperty = DependencyProperty.Register("ShouldShowConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
+ public static readonly DependencyProperty ShouldShowPotentialConflictProperty = DependencyProperty.Register("ShouldShowPotentialConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false));
+
+ public event EventHandler IgnoreConflictChanged;
+
+ public event RoutedEventHandler LearnMoreClick;
+
+ public bool IgnoreConflict
+ {
+ get => (bool)GetValue(IgnoreConflictProperty);
+ set => SetValue(IgnoreConflictProperty, value);
+ }
public bool HasConflict
{
@@ -29,9 +45,22 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
set => SetValue(ConflictMessageProperty, value);
}
+ public bool ShouldShowConflict
+ {
+ get => (bool)GetValue(ShouldShowConflictProperty);
+ private set => SetValue(ShouldShowConflictProperty, value);
+ }
+
+ public bool ShouldShowPotentialConflict
+ {
+ get => (bool)GetValue(ShouldShowPotentialConflictProperty);
+ private set => SetValue(ShouldShowPotentialConflictProperty, value);
+ }
+
public ShortcutDialogContentControl()
{
this.InitializeComponent();
+ UpdateShouldShowConflict();
}
public List Keys
@@ -51,5 +80,54 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
get => (bool)GetValue(IsWarningAltGrProperty);
set => SetValue(IsWarningAltGrProperty, value);
}
+
+ public event RoutedEventHandler ResetClick;
+
+ public event RoutedEventHandler ClearClick;
+
+ private static void OnIgnoreConflictChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = d as ShortcutDialogContentControl;
+ if (control == null)
+ {
+ return;
+ }
+
+ control.UpdateShouldShowConflict();
+
+ control.IgnoreConflictChanged?.Invoke(control, (bool)e.NewValue);
+ }
+
+ private static void OnConflictPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = d as ShortcutDialogContentControl;
+ if (control == null)
+ {
+ return;
+ }
+
+ control.UpdateShouldShowConflict();
+ }
+
+ private void UpdateShouldShowConflict()
+ {
+ ShouldShowConflict = !IgnoreConflict && HasConflict;
+ ShouldShowPotentialConflict = IgnoreConflict && HasConflict;
+ }
+
+ private void ResetBtn_Click(object sender, RoutedEventArgs e)
+ {
+ ResetClick?.Invoke(this, new RoutedEventArgs());
+ }
+
+ private void ClearBtn_Click(object sender, RoutedEventArgs e)
+ {
+ ClearClick?.Invoke(this, new RoutedEventArgs());
+ }
+
+ private void LearnMoreBtn_Click(object sender, RoutedEventArgs e)
+ {
+ LearnMoreClick?.Invoke(this, new RoutedEventArgs());
+ }
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
index baade0fb16..933a4d0c24 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml.cs
@@ -14,7 +14,6 @@ using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.SettingsXAML.Controls.Dashboard;
using Microsoft.PowerToys.Settings.UI.Views;
using Microsoft.PowerToys.Telemetry;
-using Microsoft.UI;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
@@ -29,6 +28,8 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
private Windows.ApplicationModel.Resources.ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
+ private int _conflictCount;
+
public bool EnableDataDiagnostics
{
get
@@ -60,6 +61,9 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
if (_allHotkeyConflictsData != value)
{
_allHotkeyConflictsData = value;
+
+ UpdateConflictCount();
+
OnPropertyChanged(nameof(AllHotkeyConflictsData));
OnPropertyChanged(nameof(ConflictCount));
OnPropertyChanged(nameof(ConflictText));
@@ -71,28 +75,43 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
}
}
- public int ConflictCount
+ public int ConflictCount => _conflictCount;
+
+ private void UpdateConflictCount()
{
- get
+ int count = 0;
+ if (AllHotkeyConflictsData == null)
{
- if (AllHotkeyConflictsData == null)
- {
- return 0;
- }
-
- int count = 0;
- if (AllHotkeyConflictsData.InAppConflicts != null)
- {
- count += AllHotkeyConflictsData.InAppConflicts.Count;
- }
-
- if (AllHotkeyConflictsData.SystemConflicts != null)
- {
- count += AllHotkeyConflictsData.SystemConflicts.Count;
- }
-
- return count;
+ _conflictCount = count;
}
+
+ if (AllHotkeyConflictsData.InAppConflicts != null)
+ {
+ foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts)
+ {
+ var hotkey = inAppConflict.Hotkey;
+ var hotkeySettings = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
+ if (!HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySettings))
+ {
+ count++;
+ }
+ }
+ }
+
+ if (AllHotkeyConflictsData.SystemConflicts != null)
+ {
+ foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts)
+ {
+ var hotkey = systemConflict.Hotkey;
+ var hotkeySettings = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
+ if (!HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySettings))
+ {
+ count++;
+ }
+ }
+ }
+
+ _conflictCount = count;
}
public string ConflictText
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
index ba7962840a..cf4488b759 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs
@@ -16,7 +16,9 @@ using CommunityToolkit.WinUI.Controls;
using global::PowerToys.GPOWrapper;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
+using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
@@ -39,6 +41,8 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar();
+ private int _conflictCount;
+
public AllHotkeyConflictsData AllHotkeyConflictsData
{
get => _allHotkeyConflictsData;
@@ -47,34 +51,48 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
if (_allHotkeyConflictsData != value)
{
_allHotkeyConflictsData = value;
+
+ UpdateConflictCount();
+
OnPropertyChanged(nameof(AllHotkeyConflictsData));
OnPropertyChanged(nameof(HasConflicts));
}
}
}
- public bool HasConflicts
+ public bool HasConflicts => _conflictCount > 0;
+
+ private void UpdateConflictCount()
{
- get
+ int count = 0;
+ if (AllHotkeyConflictsData == null)
{
- if (AllHotkeyConflictsData == null)
- {
- return false;
- }
-
- int count = 0;
- if (AllHotkeyConflictsData.InAppConflicts != null)
- {
- count += AllHotkeyConflictsData.InAppConflicts.Count;
- }
-
- if (AllHotkeyConflictsData.SystemConflicts != null)
- {
- count += AllHotkeyConflictsData.SystemConflicts.Count;
- }
-
- return count > 0;
+ _conflictCount = count;
}
+
+ if (AllHotkeyConflictsData.InAppConflicts != null)
+ {
+ foreach (var inAppConflict in AllHotkeyConflictsData.InAppConflicts)
+ {
+ if (!inAppConflict.ConflictIgnored)
+ {
+ count++;
+ }
+ }
+ }
+
+ if (AllHotkeyConflictsData.SystemConflicts != null)
+ {
+ foreach (var systemConflict in AllHotkeyConflictsData.SystemConflicts)
+ {
+ if (!systemConflict.ConflictIgnored)
+ {
+ count++;
+ }
+ }
+ }
+
+ _conflictCount = count;
}
public event PropertyChangedEventHandler PropertyChanged;
@@ -100,6 +118,21 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () =>
{
+ var allConflictData = e.Conflicts;
+ foreach (var inAppConflict in allConflictData.InAppConflicts)
+ {
+ var hotkey = inAppConflict.Hotkey;
+ var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
+ inAppConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
+ }
+
+ foreach (var systemConflict in allConflictData.SystemConflicts)
+ {
+ var hotkey = systemConflict.Hotkey;
+ var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
+ systemConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
+ }
+
AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
});
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml
index 514e959629..498adf4803 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml
@@ -162,7 +162,7 @@
Severity="Informational"
Visibility="{x:Bind ViewModel.IsAnimationEnabledBySystem, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}">
-
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 70fc0c4b5b..2a867c623e 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -2667,23 +2667,20 @@ From there, simply click on one of the supported files in the File Explorer and
Press a combination of keys to change this shortcut.
Right-click to remove the key combination, thereby deactivating the shortcut.
-
- Reset
-
Save
Activation shortcut
-
+
Invalid shortcut
- Only shortcuts that start with **Windows key**, **Ctrl**, **Alt** or **Shift** are valid.
+ A shortcut should start with **Windows key**, **Ctrl**, **Alt** or **Shift**.
The ** sequences are used for text formatting of the key names. Don't remove them on translation.
-
+
Possible shortcut interference with Alt Gr
Alt Gr refers to the right alt key on some international keyboards
@@ -2691,8 +2688,8 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Shortcuts with **Ctrl** and **Alt** may remove functionality from some international keyboards, because **Ctrl** + **Alt** = **Alt Gr** in those keyboards.
The ** sequences are used for text formatting of the key names. Don't remove them on translation.
-
- Shortcut conflict
+
+ This shortcut has a potential conflict, but the warning is ignored.
A conflict has been detected for this shortcut.
@@ -2895,6 +2892,18 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Crosshairs fixed length (px)
px = pixels
+
+
+ Crosshairs orientation
+
+
+ Vertical and horizontal lines
+
+
+ Vertical only
+
+
+ Horizontal only
Gliding cursor
@@ -5376,23 +5385,23 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
PowerToys shortcut conflicts
-
+
PowerToys shortcut conflicts
- Conflicting shortcuts may cause unexpected behavior. Edit them here or go to the module settings to update them.
+ If any shortcut conflicts are detected, they’ll appear below. Conflicts can happen between PowerToys utilities or Windows system shortcuts, and may cause unexpected behavior. If everything works as expected, you can safely ignore the conflict.
Conflicts found for
- System
+ System shortcut
- Windows system shortcut
+ This shortcut is reserved by Windows and can't be reassigned.
-
- This shortcut can't be changed.
+
+ See all Windows shortcuts
This shortcut is used by Windows and can't be changed.
@@ -5448,4 +5457,31 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
Use the **keyboard shortcut** to instantly toggle between light and dark modes, or set up **sunrise/sunset automation** for natural theme transitions.
Light Switch is a product name, do not localize
+
+ Dismiss
+
+
+ Dismiss
+
+
+ Reset shortcut
+
+
+ Reset to the default shortcut
+
+
+ Reset
+
+
+ Clear shortcut
+
+
+ Clear and unassign this shortcut
+
+
+ Clear
+
+
+ Learn more
+
\ No newline at end of file
diff --git a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
index d9be787e70..d7b03efad4 100644
--- a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
@@ -128,14 +128,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
if (value != _hotkey)
{
- if (value == null || value.IsEmpty())
- {
- _hotkey = AlwaysOnTopProperties.DefaultHotkeyValue;
- }
- else
- {
- _hotkey = value;
- }
+ _hotkey = value ?? AlwaysOnTopProperties.DefaultHotkeyValue;
Settings.Properties.Hotkey.Value = _hotkey;
NotifyPropertyChanged();
diff --git a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs
index 8dac08917f..344eaa183f 100644
--- a/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/DashboardViewModel.cs
@@ -30,7 +30,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
protected override string ModuleName => "Dashboard";
- private const string JsonFileType = ".json";
private Dispatcher dispatcher;
public Func SendConfigMSG { get; }
@@ -89,6 +88,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
dispatcher.BeginInvoke(() =>
{
+ var allConflictData = e.Conflicts;
+ foreach (var inAppConflict in allConflictData.InAppConflicts)
+ {
+ var hotkey = inAppConflict.Hotkey;
+ var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
+ inAppConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
+ }
+
+ foreach (var systemConflict in allConflictData.SystemConflicts)
+ {
+ var hotkey = systemConflict.Hotkey;
+ var hotkeySetting = new HotkeySettings(hotkey.Win, hotkey.Ctrl, hotkey.Alt, hotkey.Shift, hotkey.Key);
+ systemConflict.ConflictIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkeySetting);
+ }
+
AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData();
});
}
diff --git a/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs
index 0f0ba98d11..7ce6ec74c6 100644
--- a/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/FancyZonesViewModel.cs
@@ -776,7 +776,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
if (value != _editorHotkey)
{
- if (value == null || value.IsEmpty())
+ if (value == null)
{
_editorHotkey = FZConfigProperties.DefaultEditorHotkeyValue;
}
@@ -822,7 +822,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
if (value != _nextTabHotkey)
{
- if (value == null || value.IsEmpty())
+ if (value == null)
{
_nextTabHotkey = FZConfigProperties.DefaultNextTabHotkeyValue;
}
@@ -848,7 +848,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
if (value != _prevTabHotkey)
{
- if (value == null || value.IsEmpty())
+ if (value == null)
{
_prevTabHotkey = FZConfigProperties.DefaultPrevTabHotkeyValue;
}
diff --git a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs
index 6e38a4b23e..97d31fd9ab 100644
--- a/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs
@@ -100,6 +100,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_mousePointerCrosshairsAutoHide = MousePointerCrosshairsSettingsConfig.Properties.CrosshairsAutoHide.Value;
_mousePointerCrosshairsIsFixedLengthEnabled = MousePointerCrosshairsSettingsConfig.Properties.CrosshairsIsFixedLengthEnabled.Value;
_mousePointerCrosshairsFixedLength = MousePointerCrosshairsSettingsConfig.Properties.CrosshairsFixedLength.Value;
+ _mousePointerCrosshairsOrientation = MousePointerCrosshairsSettingsConfig.Properties.CrosshairsOrientation.Value;
_mousePointerCrosshairsAutoActivate = MousePointerCrosshairsSettingsConfig.Properties.AutoActivate.Value;
int isEnabled = 0;
@@ -869,6 +870,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ public int MousePointerCrosshairsOrientation
+ {
+ get
+ {
+ return _mousePointerCrosshairsOrientation;
+ }
+
+ set
+ {
+ if (value != _mousePointerCrosshairsOrientation)
+ {
+ _mousePointerCrosshairsOrientation = value;
+ MousePointerCrosshairsSettingsConfig.Properties.CrosshairsOrientation.Value = value;
+ NotifyMousePointerCrosshairsPropertyChanged();
+ }
+ }
+ }
+
public bool MousePointerCrosshairsAutoActivate
{
get
@@ -991,6 +1010,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private bool _mousePointerCrosshairsAutoHide;
private bool _mousePointerCrosshairsIsFixedLengthEnabled;
private int _mousePointerCrosshairsFixedLength;
+ private int _mousePointerCrosshairsOrientation;
private bool _mousePointerCrosshairsAutoActivate;
private bool _isAnimationEnabledBySystem;
}
diff --git a/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs
index 2cfcbaf42f..cfd3683080 100644
--- a/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/ShortcutConflictViewModel.cs
@@ -12,12 +12,10 @@ using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
-using System.Windows;
using System.Windows.Threading;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
-using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
@@ -70,6 +68,36 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
protected override string ModuleName => "ShortcutConflictsWindow";
+ ///
+ /// Ignore a specific HotkeySettings
+ ///
+ /// The HotkeySettings to ignore
+ public void IgnoreShortcut(HotkeySettings hotkeySettings)
+ {
+ if (hotkeySettings == null)
+ {
+ return;
+ }
+
+ HotkeyConflictIgnoreHelper.AddToIgnoredList(hotkeySettings);
+ GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
+ }
+
+ ///
+ /// Remove a HotkeySettings from the ignored list
+ ///
+ /// The HotkeySettings to unignore
+ public void UnignoreShortcut(HotkeySettings hotkeySettings)
+ {
+ if (hotkeySettings == null)
+ {
+ return;
+ }
+
+ HotkeyConflictIgnoreHelper.RemoveFromIgnoredList(hotkeySettings);
+ GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
+ }
+
private IHotkeyConfig GetModuleSettings(string moduleKey)
{
try
@@ -120,20 +148,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
foreach (var conflict in conflicts)
{
- ProcessConflictGroup(conflict, isSystemConflict);
+ HotkeySettings hotkey = new(conflict.Hotkey.Win, conflict.Hotkey.Ctrl, conflict.Hotkey.Alt, conflict.Hotkey.Shift, conflict.Hotkey.Key);
+ var isIgnored = HotkeyConflictIgnoreHelper.IsIgnoringConflicts(hotkey);
+ conflict.ConflictIgnored = isIgnored;
+
+ ProcessConflictGroup(conflict, isSystemConflict, isIgnored);
items.Add(conflict);
}
}
- private void ProcessConflictGroup(HotkeyConflictGroupData conflict, bool isSystemConflict)
+ private void ProcessConflictGroup(HotkeyConflictGroupData conflict, bool isSystemConflict, bool isIgnored)
{
foreach (var module in conflict.Modules)
{
- SetupModuleData(module, isSystemConflict);
+ SetupModuleData(module, isSystemConflict, isIgnored);
}
}
- private void SetupModuleData(ModuleHotkeyData module, bool isSystemConflict)
+ private void SetupModuleData(ModuleHotkeyData module, bool isSystemConflict, bool isIgnored)
{
try
{
@@ -220,55 +252,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
- private void SaveModuleSettingsAndNotify(string moduleName)
- {
- try
- {
- var settings = GetModuleSettings(moduleName);
-
- if (settings is ISettingsConfig settingsConfig)
- {
- // No need to save settings here, the runner will call module interface to save it
- // SaveSettingsToFile(settings);
-
- // Send IPC notification using the same format as other ViewModels
- SendConfigMSG(settingsConfig, moduleName);
-
- System.Diagnostics.Debug.WriteLine($"Saved settings and sent IPC notification for module: {moduleName}");
- }
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Error saving settings and notifying for {moduleName}: {ex.Message}");
- }
- }
-
- private void SaveSettingsToFile(IHotkeyConfig settings)
- {
- try
- {
- // Get the repository for this settings type using reflection
- var settingsType = settings.GetType();
- var repositoryMethod = typeof(SettingsFactory).GetMethod("GetRepository");
- if (repositoryMethod != null)
- {
- var genericMethod = repositoryMethod.MakeGenericMethod(settingsType);
- var repository = genericMethod.Invoke(_settingsFactory, null);
-
- if (repository != null)
- {
- var saveMethod = repository.GetType().GetMethod("SaveSettingsToFile");
- saveMethod?.Invoke(repository, null);
- System.Diagnostics.Debug.WriteLine($"Saved settings to file for type: {settingsType.Name}");
- }
- }
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine($"Error saving settings to file: {ex.Message}");
- }
- }
-
///
/// Sends IPC notification using the same format as other ViewModels
///
diff --git a/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs
index 2c05c79358..842c3cf368 100644
--- a/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/WorkspacesViewModel.cs
@@ -127,7 +127,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
if (value != _hotkey)
{
- if (value == null || value.IsEmpty())
+ if (value == null)
{
_hotkey = WorkspacesProperties.DefaultHotkeyValue;
}
diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1
index 2229be63ae..f601146577 100644
--- a/tools/build/build-installer.ps1
+++ b/tools/build/build-installer.ps1
@@ -124,6 +124,20 @@ else {
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
}
+# Generate DSC manifest files
+Write-Host '[DSC] Generating DSC manifest files...'
+$dscScriptPath = Join-Path $repoRoot '.\tools\build\generate-dsc-manifests.ps1'
+if (Test-Path $dscScriptPath) {
+ & $dscScriptPath -BuildPlatform $Platform -BuildConfiguration $Configuration -RepoRoot $repoRoot
+ if ($LASTEXITCODE -ne 0) {
+ Write-Error "DSC manifest generation failed with exit code $LASTEXITCODE"
+ exit 1
+ }
+ Write-Host '[DSC] DSC manifest files generated successfully'
+} else {
+ Write-Warning "[DSC] DSC manifest generator script not found at: $dscScriptPath"
+}
+
RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' $commonArgs $Platform $Configuration
RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' $commonArgs $Platform $Configuration
diff --git a/tools/build/generate-dsc-manifests.ps1 b/tools/build/generate-dsc-manifests.ps1
new file mode 100644
index 0000000000..cb730ddd4a
--- /dev/null
+++ b/tools/build/generate-dsc-manifests.ps1
@@ -0,0 +1,116 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildPlatform,
+
+ [Parameter(Mandatory = $true)]
+ [string]$BuildConfiguration,
+
+ [Parameter()]
+ [string]$RepoRoot = (Get-Location).Path,
+
+ [switch]$ForceRebuildExecutable
+)
+
+$ErrorActionPreference = 'Stop'
+
+function Resolve-PlatformDirectory {
+ param(
+ [string]$Root,
+ [string]$Platform
+ )
+
+ $normalized = $Platform.Trim()
+ $candidates = @()
+ $candidates += Join-Path $Root $normalized
+ $candidates += Join-Path $Root ($normalized.ToUpperInvariant())
+ $candidates += Join-Path $Root ($normalized.ToLowerInvariant())
+ $candidates = $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
+
+ foreach ($candidate in $candidates) {
+ if (Test-Path $candidate) {
+ return $candidate
+ }
+ }
+
+ return $candidates[0]
+}
+
+Write-Host "Repo root: $RepoRoot"
+Write-Host "Requested build platform: $BuildPlatform"
+Write-Host "Requested configuration: $BuildConfiguration"
+
+# Always use x64 PowerToys.DSC.exe since CI/CD machines are x64
+$exePlatform = 'x64'
+$exeRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $exePlatform
+$exeOutputDir = Join-Path $exeRoot $BuildConfiguration
+$exePath = Join-Path $exeOutputDir 'PowerToys.DSC.exe'
+
+Write-Host "Using x64 PowerToys.DSC.exe to generate DSC manifests for $BuildPlatform build"
+
+if ($ForceRebuildExecutable -or -not (Test-Path $exePath)) {
+ Write-Host "PowerToys.DSC.exe not found at '$exePath'. Building x64 binary..."
+
+ $msbuild = Get-Command msbuild.exe -ErrorAction SilentlyContinue
+ if ($null -eq $msbuild) {
+ throw "msbuild.exe was not found on the PATH."
+ }
+
+ $projectPath = Join-Path $RepoRoot 'src\dsc\v3\PowerToys.DSC\PowerToys.DSC.csproj'
+ $msbuildArgs = @(
+ $projectPath,
+ '/t:Build',
+ '/m',
+ "/p:Configuration=$BuildConfiguration",
+ "/p:Platform=x64",
+ '/restore'
+ )
+
+ & $msbuild.Path @msbuildArgs
+ $msbuildExitCode = $LASTEXITCODE
+
+ if ($msbuildExitCode -ne 0) {
+ throw "msbuild build failed with exit code $msbuildExitCode"
+ }
+
+ if (-not (Test-Path $exePath)) {
+ throw "Expected PowerToys.DSC.exe at '$exePath' after build but it was not found."
+ }
+} else {
+ Write-Host "Using existing PowerToys.DSC.exe at '$exePath'."
+}
+
+# Output DSC manifests to the target build platform directory (x64, ARM64, etc.)
+$outputRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $BuildPlatform
+if (-not (Test-Path $outputRoot)) {
+ Write-Host "Creating missing platform output root at '$outputRoot'."
+ New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
+}
+
+$outputDir = Join-Path $outputRoot $BuildConfiguration
+if (-not (Test-Path $outputDir)) {
+ Write-Host "Creating missing configuration output directory at '$outputDir'."
+ New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
+}
+
+Write-Host "DSC manifests will be generated to: '$outputDir'"
+
+Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'."
+Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
+
+$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
+Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
+& $exePath @arguments
+if ($LASTEXITCODE -ne 0) {
+ throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
+}
+
+$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
+if ($generatedFiles.Count -eq 0) {
+ throw "No DSC manifest files were generated in '$outputDir'."
+}
+
+Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"
+foreach ($file in $generatedFiles) {
+ Write-Host " - $($file.FullName)"
+}