diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 2839f08318..3f53a4db6d 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -160,6 +160,7 @@ BUILDARCH
BUILDNUMBER
buildtransitive
builttoroam
+BUNDLEINFO
BVal
BValue
byapp
@@ -863,6 +864,7 @@ LOCKTYPE
LOGFONT
LOGFONTW
logon
+LOGMSG
LOGPIXELSX
LOGPIXELSY
LOn
@@ -1040,6 +1042,7 @@ MWBEx
MYICON
NAMECHANGE
namespaceanddescendants
+Namotion
nao
NCACTIVATE
ncc
@@ -1077,6 +1080,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..4a12d57e81 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",
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/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 20ac18607e..4e5524c757 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -797,6 +797,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}"
@@ -2891,6 +2897,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
@@ -3238,6 +3260,9 @@ Global
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{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/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/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 8c3ad76448..815731c161 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)
{
@@ -1375,6 +1380,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 0cb9118b91..a1b89ec1a9 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)"
@@ -115,6 +117,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
+
diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs
index e343897d5d..30831548dd 100644
--- a/installer/PowerToysSetupVNext/Product.wxs
+++ b/installer/PowerToysSetupVNext/Product.wxs
@@ -62,6 +62,7 @@
+
@@ -69,8 +70,8 @@
-
-
+
+
@@ -117,6 +118,8 @@
+
+
@@ -160,6 +163,9 @@
+
+
+
@@ -244,6 +250,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/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/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)"
+}