diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt
index 475e68045b..ab2446f8ae 100644
--- a/.github/actions/spell-check/allow/names.txt
+++ b/.github/actions/spell-check/allow/names.txt
@@ -29,8 +29,6 @@ shortcutguide
# 8LWXpg is user name but user folder causes a flag
LWXpg
-# 0x6f677548 is user name but user folder causes a flag
-x6f677548
Adoumie
Advaith
alekhyareddy
@@ -210,6 +208,7 @@ capturevideosample
cmdow
Controlz
cortana
+devhints
dlnilsson
fancymouse
firefox
@@ -229,6 +228,7 @@ regedit
roslyn
Skia
Spotify
+tldr
Vanara
wangyi
WEX
diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt
index 7ad88bde19..c6f1225788 100644
--- a/.github/actions/spell-check/excludes.txt
+++ b/.github/actions/spell-check/excludes.txt
@@ -121,6 +121,10 @@
^src/modules/MouseWithoutBorders/App/Helper/.*\.resx$
^src/modules/MouseWithoutBorders/ModuleInterface/generateSecurityDescriptor\.h$
^src/modules/peek/Peek.Common/NativeMethods\.txt$
+^src/modules/peek/Peek.UITests/TestAssets/4\.qoi$
+^src/modules/powerrename/PowerRenameUITest/testItems/folder1/testCase2\.txt$
+^src/modules/powerrename/PowerRenameUITest/testItems/folder2/SpecialCase\.txt$
+^src/modules/powerrename/PowerRenameUITest/testItems/testCase1\.txt$
^src/modules/previewpane/SvgPreviewHandler/SvgHTMLPreviewGenerator\.cs$
^src/modules/previewpane/UnitTests-MarkdownPreviewHandler/HelperFiles/MarkdownWithHTMLImageTag\.txt$
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
@@ -131,3 +135,4 @@
ignore$
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
^src/common/CalculatorEngineCommon/exprtk\.hpp$
+src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage.cs
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index c4c13282e4..c89141a8c5 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -3,6 +3,7 @@ abcdefghjkmnpqrstuvxyz
abgr
ABlocked
ABOUTBOX
+ABORTIFHUNG
Abug
Acceleratorkeys
ACCEPTFILES
@@ -25,8 +26,7 @@ ADMINS
adml
admx
advancedpaste
-advancedpasteui
-advancedpasteuishortcut
+advapi
advfirewall
AFeature
affordances
@@ -43,7 +43,6 @@ ALLINPUT
Allman
Allmodule
ALLOWUNDO
-allpc
ALLVIEW
ALPHATYPE
AModifier
@@ -76,6 +75,7 @@ appwiz
appxpackage
APSTUDIO
AQS
+Aquadrant
ARandom
ARCHITEW
ARemapped
@@ -133,7 +133,6 @@ bla
BLACKFRAME
BLENDFUNCTION
Blockquotes
-blogs
Blt
BLURBEHIND
BLURREGION
@@ -159,6 +158,7 @@ BUILDARCH
BUILDNUMBER
buildtransitive
builttoroam
+BUNDLEINFO
BVal
BValue
byapp
@@ -364,6 +364,7 @@ desktopshorcutinstalled
DESKTOPVERTRES
devblogs
devdocs
+devenv
devmgmt
DEVMODE
DEVMODEW
@@ -392,6 +393,7 @@ DNLEN
DONOTROUND
DONTVALIDATEPATH
dotnet
+downscale
DPICHANGED
DPIs
DPSAPI
@@ -451,6 +453,7 @@ encryptor
ENDSESSION
ENSUREVISIBLE
ENTERSIZEMOVE
+ENTRYW
ENU
environmentvariables
EOAC
@@ -481,6 +484,7 @@ examplehandler
examplepowertoy
EXAND
EXCLUDEFROMCAPTURE
+EXECUTEDEFAULT
executionpolicy
exename
exf
@@ -504,7 +508,6 @@ FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
FARPROC
fesf
-fff
FFFF
FILEEXPLORER
fileexploreraddons
@@ -567,6 +570,7 @@ GETDESKWALLPAPER
GETDLGCODE
GETDPISCALEDSIZE
getfilesiginforedist
+geolocator
GETHOTKEY
GETICON
GETMINMAXINFO
@@ -663,11 +667,7 @@ Hostx
hotfixes
hotkeycontrol
HOTKEYF
-hotkeylockmachine
-hotkeyreconnect
hotkeys
-hotkeyswitch
-hotkeytoggleeasymouse
hotlight
hotspot
HPAINTBUFFER
@@ -726,8 +726,6 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
-imagetotext
-imagetotextshortcut
imagingdevices
ime
imgflip
@@ -818,10 +816,12 @@ killrunner
kmph
kvp
Kybd
+LARGEICON
lastcodeanalysissucceeded
LASTEXITCODE
LAYOUTRTL
LCh
+lbl
lcid
LCIDTo
lcl
@@ -840,10 +840,12 @@ LIBID
LIMITSIZE
LIMITTEXT
lindex
+lightswitch
linkid
LINKOVERLAY
LINQTo
listview
+LIVEDRAW
LIVEZOOM
LLKH
llkhf
@@ -855,17 +857,19 @@ localappdata
localpackage
LOCALSYSTEM
LOCATIONCHANGE
-LOCKMACHINE
LOCKTYPE
LOGFONT
LOGFONTW
logon
+LOGMSG
LOGPIXELSX
LOGPIXELSY
-LOn
+lng
+lon
longdate
LONGNAMES
lowlevel
+lquadrant
LOWORD
lparam
LPBITMAPINFOHEADER
@@ -941,7 +945,6 @@ MDL
mdtext
mdtxt
mdwn
-measuretool
meme
memicmp
MENUITEMINFO
@@ -991,10 +994,12 @@ MOUSEHWHEEL
MOUSEINPUT
mousejump
mousepointer
-mousepointercrosshairs
mouseutils
MOVESIZEEND
MOVESIZESTART
+muxx
+muxxc
+muxxh
MRM
MRT
mru
@@ -1189,23 +1194,13 @@ PACL
PAINTSTRUCT
PALETTEWINDOW
PARENTNOTIFY
+PARENTRELATIVE
PARENTRELATIVEEDITING
PARENTRELATIVEFORADDRESSBAR
+PARENTRELATIVEFORUI
PARENTRELATIVEPARSING
parray
PARTIALCONFIRMATIONDIALOGTITLE
-pasteashtmlfile
-pasteashtmlfileshortcut
-pasteasjson
-pasteasjsonshortcut
-pasteasmarkdown
-pasteasmarkdownshortcut
-pasteasplaintext
-pasteasplaintextshortcut
-pasteaspngfile
-pasteaspngfileshortcut
-pasteastxtfile
-pasteastxtfileshortcut
PATCOPY
PATHMUSTEXIST
PATINVERT
@@ -1213,6 +1208,7 @@ PATPAINT
pbc
pbi
PBlob
+pbrush
pcb
pcch
pcelt
@@ -1246,6 +1242,7 @@ pgp
pguid
phbm
phbmp
+phicon
phwnd
pici
pidl
@@ -1254,6 +1251,7 @@ pinfo
pinvoke
pipename
PKBDLLHOOKSTRUCT
+pkgfamily
plib
ploc
ploca
@@ -1273,7 +1271,6 @@ Pomodoro
Popups
POPUPWINDOW
POSITIONITEM
-powerocr
POWERRENAMECONTEXTMENU
powerrenameinput
POWERRENAMETEST
@@ -1324,7 +1321,6 @@ PRODUCTVERSION
Progman
programdata
projectname
-projitems
PROPERTYKEY
Propset
PROPVARIANT
@@ -1372,8 +1368,9 @@ quickaccent
QUNS
RAII
RAlt
+RAquadrant
randi
-Rasterization
+rasterization
Rasterize
RAWINPUTDEVICE
RAWINPUTHEADER
@@ -1416,7 +1413,6 @@ Removelnk
renamable
RENAMEONCOLLISION
reparented
-reparenthotkey
reparenting
reportfileaccesses
requery
@@ -1442,7 +1438,6 @@ RIDEV
RIGHTSCROLLBAR
riid
RKey
-Rns
RNumber
rop
ROUNDSMALL
@@ -1666,10 +1661,10 @@ STYLECHANGED
STYLECHANGING
subkeys
sublang
-Subdomain
SUBMODULEUPDATE
subresource
Superbar
+suntimes
sut
svchost
SVGIn
@@ -1735,9 +1730,9 @@ tgz
themeresources
THH
THICKFRAME
+THEMECHANGED
THISCOMPONENT
throughs
-thumbnailhotkey
TILEDWINDOW
TILLSON
timedate
@@ -1751,10 +1746,9 @@ tkconverters
tlb
tlbimp
tlc
+tmain
TNP
-TOGGLEEASYMOUSE
Toolhelp
-toolkitconverters
toolwindow
TOPDOWNDIB
TOUCHEVENTF
@@ -1766,11 +1760,9 @@ tracelogging
tracerpt
trackbar
trafficmanager
-transcodetomp
transicc
TRAYMOUSEMESSAGE
triaging
-Tru
trl
trx
tsa
diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt
index 255a7f984c..cb303a10ad 100644
--- a/.github/actions/spell-check/patterns.txt
+++ b/.github/actions/spell-check/patterns.txt
@@ -1,5 +1,10 @@
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
+# marker to ignore all code on line
+^.*/\* #no-spell-check-line \*/.*$
+# marker for ignoring a comment to the end of the line
+// #no-spell-check.*$
+
# Gaelic
Gàidhlig
@@ -264,3 +269,7 @@ St&yle
# This matches a relative clause where the relative pronoun "that" is omitted.
# Example: "Gets or sets the window the TitleBar should configure."
\bthe\s+\w+\s+the\b
+
+# Usernames with numbers
+# 0x6f677548 is user name but user folder causes a flag
+\bx6f677548\b
diff --git a/.github/workflows/automatic-issue-deduplication.yml b/.github/workflows/automatic-issue-deduplication.yml
new file mode 100644
index 0000000000..88ec3e2f23
--- /dev/null
+++ b/.github/workflows/automatic-issue-deduplication.yml
@@ -0,0 +1,19 @@
+name: Automatic New Issue Deduplication
+on:
+ issues:
+ types: [opened, reopened]
+permissions:
+ models: read
+ issues: write
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.issue.number }}
+ cancel-in-progress: true
+jobs:
+ deduplicate:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Run Deduplicate Action
+ uses: pelikhan/action-genai-issue-dedup@v0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ label_as_duplicate: true
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..2a1760d94e 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",
@@ -134,6 +133,9 @@
"PowerToys.ImageResizerContextMenu.dll",
"ImageResizerContextMenuPackage.msix",
+ "PowerToys.LightSwitchModuleInterface.dll",
+ "LightSwitchService\\PowerToys.LightSwitchService.exe",
+
"PowerToys.KeyboardManager.dll",
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
@@ -230,7 +232,10 @@
"PowerToys.CmdPalModuleInterface.dll",
"CmdPalKeyboardService.dll",
- "*Microsoft.CmdPal.UI_*.msix"
+ "*Microsoft.CmdPal.UI_*.msix",
+
+ "PowerToys.DSC.dll",
+ "PowerToys.DSC.exe"
],
"SigningInfo": {
"Operations": [
@@ -297,6 +302,9 @@
"msvcp140_1_app.dll",
"msvcp140_2_app.dll",
"msvcp140_app.dll",
+ "Namotion.Reflection.dll",
+ "NJsonSchema.Annotations.dll",
+ "NJsonSchema.dll",
"vcamp140_app.dll",
"vccorlib140_app.dll",
"vcomp140_app.dll",
@@ -322,6 +330,12 @@
"WinUI3Apps\\ReverseMarkdown.dll",
"WinUI3Apps\\SharpCompress.dll",
"WinUI3Apps\\ZstdSharp.dll",
+ "CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll",
+ "WinUI3Apps\\CommunityToolkit.WinUI.Controls.MarkdownTextBlock.dll",
+ "Markdig.dll",
+ "WinUI3Apps\\Markdig.dll",
+ "RomanNumerals.dll",
+ "WinUI3Apps\\RomanNumerals.dll",
"TestableIO.System.IO.Abstractions.dll",
"WinUI3Apps\\TestableIO.System.IO.Abstractions.dll",
"TestableIO.System.IO.Abstractions.Wrappers.dll",
diff --git a/.pipelines/generateDscManifests.ps1 b/.pipelines/generateDscManifests.ps1
new file mode 100644
index 0000000000..109610e62e
--- /dev/null
+++ b/.pipelines/generateDscManifests.ps1
@@ -0,0 +1,88 @@
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory = $true)]
+ [string]$BuildPlatform,
+
+ [Parameter(Mandatory = $true)]
+ [string]$BuildConfiguration,
+
+ [Parameter()]
+ [string]$RepoRoot = (Get-Location).Path
+)
+
+$ErrorActionPreference = 'Stop'
+
+function Resolve-PlatformDirectory {
+ param(
+ [string]$Root,
+ [string]$Platform
+ )
+
+ $normalized = $Platform.Trim()
+ $candidates = @()
+ $candidates += Join-Path $Root $normalized
+ $candidates += Join-Path $Root ($normalized.ToUpperInvariant())
+ $candidates += Join-Path $Root ($normalized.ToLowerInvariant())
+ $candidates = $candidates | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
+
+ foreach ($candidate in $candidates) {
+ if (Test-Path $candidate) {
+ return $candidate
+ }
+ }
+
+ return $candidates[0]
+}
+
+Write-Host "Repo root: $RepoRoot"
+Write-Host "Requested build platform: $BuildPlatform"
+Write-Host "Requested configuration: $BuildConfiguration"
+
+# Always use x64 PowerToys.DSC.exe since CI/CD machines are x64
+$exePlatform = 'x64'
+$exeRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $exePlatform
+$exeOutputDir = Join-Path $exeRoot $BuildConfiguration
+$exePath = Join-Path $exeOutputDir 'PowerToys.DSC.exe'
+
+Write-Host "Using x64 PowerToys.DSC.exe to generate DSC manifests for $BuildPlatform build"
+
+if (-not (Test-Path $exePath)) {
+ throw "PowerToys.DSC.exe not found at '$exePath'. Make sure it has been built first."
+}
+
+Write-Host "Using PowerToys.DSC.exe at '$exePath'."
+
+# Output DSC manifests to the target build platform directory (x64, ARM64, etc.)
+$outputRoot = Resolve-PlatformDirectory -Root $RepoRoot -Platform $BuildPlatform
+if (-not (Test-Path $outputRoot)) {
+ Write-Host "Creating missing platform output root at '$outputRoot'."
+ New-Item -Path $outputRoot -ItemType Directory -Force | Out-Null
+}
+
+$outputDir = Join-Path $outputRoot $BuildConfiguration
+if (-not (Test-Path $outputDir)) {
+ Write-Host "Creating missing configuration output directory at '$outputDir'."
+ New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
+}
+
+Write-Host "DSC manifests will be generated to: '$outputDir'"
+
+Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'."
+Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
+
+$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
+Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
+& $exePath @arguments
+if ($LASTEXITCODE -ne 0) {
+ throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
+}
+
+$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
+if ($generatedFiles.Count -eq 0) {
+ throw "No DSC manifest files were generated in '$outputDir'."
+}
+
+Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"
+foreach ($file in $generatedFiles) {
+ Write-Host " - $($file.FullName)"
+}
diff --git a/.pipelines/loc/loc.yml b/.pipelines/loc/loc.yml
index cc4512c92e..2abc298652 100644
--- a/.pipelines/loc/loc.yml
+++ b/.pipelines/loc/loc.yml
@@ -29,8 +29,8 @@ steps:
displayName: 'Touchdown Build - 37400, PRODEXT'
inputs:
teamId: 37400
- TDBuildServiceConnection: $(TouchdownServiceConnection)
- authType: SubjectNameIssuer
+ FederatedIdentityTDBuildServiceConnection: $(TouchdownServiceConnection)
+ authType: FederatedIdentityTDBuild
resourceFilePath: |
src\**\Resources.resx
src\**\Resource.resx
diff --git a/.pipelines/v2/ci-nightly.yml b/.pipelines/v2/ci-nightly.yml
new file mode 100644
index 0000000000..1f49359f66
--- /dev/null
+++ b/.pipelines/v2/ci-nightly.yml
@@ -0,0 +1,38 @@
+# .pipelines/v2/nightly-prewarm.yml
+# Nightly pre-warm that reuses your existing ci.yml as-is
+
+trigger: none
+pr: none
+
+# (18:00 UTC) — adjust as you like
+schedules:
+ - cron: "0 18 * * *" # UTC
+ displayName: Nightly pre-warm (main)
+ branches:
+ include:
+ - main
+ always: true
+
+name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
+
+parameters:
+ - name: buildPlatforms
+ type: object
+ default:
+ - x64
+ - arm64
+ - name: enableMsBuildCaching
+ type: boolean
+ displayName: "Enable MSBuild Caching"
+ default: true
+ - name: msBuildCacheIsReadOnly
+ type: boolean
+ displayName: "MSBuild Cache Read Only"
+ default: false
+
+extends:
+ template: templates/pipeline-ci-build.yml
+ parameters:
+ buildPlatforms: ${{ parameters.buildPlatforms }}
+ enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
+ msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
\ No newline at end of file
diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml
index e13792d8d1..45514d4b0f 100644
--- a/.pipelines/v2/release.yml
+++ b/.pipelines/v2/release.yml
@@ -43,11 +43,6 @@ parameters:
displayName: "Build Using Visual Studio Preview"
default: false
- - name: enableAOT
- type: boolean
- displayName: "Enable AOT (Ahead-of-Time) Compilation for CmdPal"
- default: true
-
name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr)
variables:
@@ -109,8 +104,8 @@ extends:
useManagedIdentity: $(SigningUseManagedIdentity)
clientId: $(SigningOriginalClientId)
# Have msbuild use the release nuget config profile
- additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:EnableCmdPalAOT=${{ parameters.enableAOT }} /p:InstallerSuffix=${{ parameters.installerSuffix }}
installerSuffix: ${{ parameters.installerSuffix }}
+ additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:InstallerSuffix=${{ parameters.installerSuffix }} /p:EnableCmdPalAOT=true
beforeBuildSteps:
# Sets versions for all PowerToy created DLLs
- pwsh: |-
diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml
index 5c8fb29a75..6994c7a199 100644
--- a/.pipelines/v2/templates/job-build-project.yml
+++ b/.pipelines/v2/templates/job-build-project.yml
@@ -50,6 +50,9 @@ parameters:
- name: enableMsBuildCaching
type: boolean
default: false
+ - name: msBuildCacheIsReadOnly
+ type: boolean
+ default: true
- name: runTests
type: boolean
default: true
@@ -154,6 +157,11 @@ jobs:
$MSBuildCacheParameters += " -reportfileaccesses"
$MSBuildCacheParameters += " -p:MSBuildCacheEnabled=true"
$MSBuildCacheParameters += " -p:MSBuildCacheLogDirectory=$(LogOutputDirectory)\MSBuildCacheLogs"
+ # Cache read-only policy controlled by parameter
+ $cacheIsReadOnly = "${{ parameters.msBuildCacheIsReadOnly }}"
+ if ($cacheIsReadOnly -eq "True") {
+ $MSBuildCacheParameters += " /p:MSBuildCacheRemoteCacheIsReadOnly=true"
+ }
Write-Host "MSBuildCacheParameters: $MSBuildCacheParameters"
Write-Host "##vso[task.setvariable variable=MSBuildCacheParameters]$MSBuildCacheParameters"
displayName: Prepare MSBuildCache variables
@@ -263,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:
@@ -418,7 +443,7 @@ jobs:
}
if ($Packages.Count -gt 0) {
- # Priority: Look for platform-specific MSIX (x64/arm64) first, then fallback to any
+ # Priority: Look for platform-specific MSIX (x64/arm64) first, then fall back to any
$PlatformPackage = $Packages | Where-Object { $_.Name -match "Microsoft\.CmdPal\.UI_.*_(x64|arm64)\.msix$" } | Select-Object -First 1
if ($PlatformPackage) {
$Package = $PlatformPackage
diff --git a/.pipelines/v2/templates/pipeline-ci-build.yml b/.pipelines/v2/templates/pipeline-ci-build.yml
index 541aff4845..30c1dbc757 100644
--- a/.pipelines/v2/templates/pipeline-ci-build.yml
+++ b/.pipelines/v2/templates/pipeline-ci-build.yml
@@ -13,6 +13,9 @@ parameters:
- name: enableMsBuildCaching
type: boolean
default: false
+ - name: msBuildCacheIsReadOnly
+ type: boolean
+ default: true
- name: runTests
type: boolean
default: true
@@ -52,6 +55,7 @@ stages:
buildConfigurations: [Release]
enablePackageCaching: true
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
+ msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
runTests: ${{ parameters.runTests }}
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
diff --git a/.pipelines/v2/templates/steps-build-installer-vnext.yml b/.pipelines/v2/templates/steps-build-installer-vnext.yml
index 0f7908fceb..6db52ec631 100644
--- a/.pipelines/v2/templates/steps-build-installer-vnext.yml
+++ b/.pipelines/v2/templates/steps-build-installer-vnext.yml
@@ -132,6 +132,39 @@ steps:
ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
#### END MSI
+
+ #### BUILDING AND SIGNING SilentFilesInUseBAFunction DLL
+ - task: VSBuild@1
+ displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build SilentFilesInUseBAFunction
+ inputs:
+ solution: "**/installer/PowerToysSetup.sln"
+ vsVersion: 17.0
+ msbuildArgs: >-
+ /t:SilentFilesInUseBAFunction
+ /p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true
+ /p:InstallerSuffix=${{ parameters.installerSuffix }}
+ -restore -graph
+ /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-SilentFilesInUseBAFunction.binlog
+ ${{ parameters.additionalBuildOptions }}
+ platform: $(BuildPlatform)
+ configuration: $(BuildConfiguration)
+ clean: false # don't undo our hard work above by deleting the msi
+ msbuildArchitecture: x64
+ maximumCpuCount: true
+
+ - ${{ if eq(parameters.codeSign, true) }}:
+ - template: steps-esrp-signing.yml
+ parameters:
+ displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign SilentFilesInUseBAFunction
+ signingIdentity: ${{ parameters.signingIdentity }}
+ inputs:
+ FolderPath: 'installer/$(BuildPlatform)/$(BuildConfiguration)'
+ signType: batchSigning
+ batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
+ ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
+
+ #### END BUILDING AND SIGNING SilentFilesInUseBAFunction DLL
+
#### BOOTSTRAP BUILDING AND SIGNING
- task: VSBuild@1
displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext Bootstrapper
@@ -148,7 +181,7 @@ steps:
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
- clean: false # don't undo our hard work above by deleting the MSI
+ clean: false # don't undo our hard work above by deleting the MSI nor SilentFilesInUseBAFunction
msbuildArchitecture: x64
maximumCpuCount: true
diff --git a/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml b/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml
index 44f8c4b6dc..58f2fe6c47 100644
--- a/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml
+++ b/.pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml
@@ -8,8 +8,8 @@ steps:
displayName: 'Download Localization Files -- PowerToys 37400'
inputs:
teamId: 37400
- TDBuildServiceConnection: $(TouchdownServiceConnection)
- authType: SubjectNameIssuer
+ FederatedIdentityTDBuildServiceConnection: $(TouchdownServiceConnection)
+ authType: FederatedIdentityTDBuild
resourceFilePath: |
**\Resources.resx
**\Resource.resx
diff --git a/.pipelines/verifyCommonProps.ps1 b/.pipelines/verifyCommonProps.ps1
index 028578234c..7ed52f6bf1 100644
--- a/.pipelines/verifyCommonProps.ps1
+++ b/.pipelines/verifyCommonProps.ps1
@@ -39,6 +39,14 @@ foreach ($csprojFile in $csprojFilesArray) {
if ($csprojFile -like '*TemplateCmdPalExtension.csproj') {
continue
}
+
+ # The CmdPal.Core projects use a common shared props file, so skip them
+ if ($csprojFile -like '*Microsoft.CmdPal.Core.*.csproj') {
+ continue
+ }
+ if ($csprojFile -like '*Microsoft.CmdPal.Ext.Shell.csproj') {
+ continue
+ }
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
if (!$importExists) {
diff --git a/Directory.Build.props b/Directory.Build.props
index 4184a8f2a3..e7b415cbca 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -30,7 +30,6 @@
<_PropertySheetDisplayName>PowerToys.Root.Props
$(MsbuildThisFileDirectory)\Cpp.Build.props
-
all
diff --git a/Directory.Build.targets b/Directory.Build.targets
index cba7762d5f..6da66bc8a8 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -3,4 +3,9 @@
+
+
+
+ $(WindowsSdkDir)bin\x64\mt.exe
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index e3688d6614..9ce4168538 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,6 +1,7 @@
true
+ true
@@ -21,11 +22,11 @@
-
-
+
+
@@ -37,6 +38,7 @@
+
@@ -66,6 +68,7 @@
+
@@ -106,7 +109,7 @@
-
+
diff --git a/NOTICE.md b/NOTICE.md
index bedc11379d..1998ea805a 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1509,7 +1509,6 @@ SOFTWARE.
- CommunityToolkit.WinUI.Converters
- CommunityToolkit.WinUI.Extensions
- CommunityToolkit.WinUI.UI.Controls.DataGrid
-- CommunityToolkit.WinUI.UI.Controls.Markdown
- ControlzEx
- HelixToolkit
- HelixToolkit.Core.Wpf
@@ -1522,6 +1521,7 @@ SOFTWARE.
- ModernWpfUI
- Moq
- MSTest
+- NJsonSchema
- NLog
- NLog.Extensions.Logging
- NLog.Schema
diff --git a/PowerToys.sln b/PowerToys.sln
index a72d31b13a..50063816ea 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -5,11 +5,13 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runner", "src\runner\runner.vcxproj", "{9412D5C6-2CF2-4FC2-A601-B55508EA9B27}"
ProjectSection(ProjectDependencies) = postProject
{031AC72E-FA28-4AB7-B690-6F7B9C28AA73} = {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC} = {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}
{0B43679E-EDFA-4DA0-AD30-F4628B308B1B} = {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}
{0B593A6C-4143-4337-860E-DB5710FB87DB} = {0B593A6C-4143-4337-860E-DB5710FB87DB}
{17DA04DF-E393-4397-9CF0-84DABE11032E} = {17DA04DF-E393-4397-9CF0-84DABE11032E}
{217DF501-135C-4E38-BFC8-99D4821032EA} = {217DF501-135C-4E38-BFC8-99D4821032EA}
{2BE46397-4DFA-414C-9BD4-41E4BBF8CB34} = {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}
+ {38177D56-6AD1-4ADF-88C9-2843A7932166} = {38177D56-6AD1-4ADF-88C9-2843A7932166}
{48804216-2A0E-4168-A6D8-9CD068D14227} = {48804216-2A0E-4168-A6D8-9CD068D14227}
{51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2}
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}
@@ -638,7 +640,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.CommandPalette.Ex
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CommandPalette.Extensions.Toolkit", "src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj", "{CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Common", "src\modules\cmdpal\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj", "{14E62033-58D0-4A7D-8990-52F50A08BBBD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Core.Common", "src\modules\cmdpal\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj", "{14E62033-58D0-4A7D-8990-52F50A08BBBD}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI", "src\modules\cmdpal\Microsoft.Terminal.UI\Microsoft.Terminal.UI.vcxproj", "{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}"
EndProject
@@ -728,7 +730,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerRename.UITests", "src\
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Core.ViewModels", "src\modules\cmdpal\Microsoft.CmdPal.Core.ViewModels\Microsoft.CmdPal.Core.ViewModels.csproj", "{24133F7F-C1D1-DE04-EFA8-F5D5467FE027}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Core.ViewModels", "src\modules\cmdpal\Core\Microsoft.CmdPal.Core.ViewModels\Microsoft.CmdPal.Core.ViewModels.csproj", "{24133F7F-C1D1-DE04-EFA8-F5D5467FE027}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0E556541-6A45-42CB-AE49-EE5A9BE05E7C}"
EndProject
@@ -793,10 +795,22 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LightSwitch", "LightSwitch", "{5B201255-53C8-490B-A34F-01F05D48A477}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LightSwitchModuleInterface", "src\modules\LightSwitch\LightSwitchModuleInterface\LightSwitchModuleInterface.vcxproj", "{38177D56-6AD1-4ADF-88C9-2843A7932166}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LightSwitchService", "src\modules\LightSwitch\LightSwitchService\LightSwitchService.vcxproj", "{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1-76DF-42AC-985C-164CC2EE57A1}"
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}"
@@ -805,6 +819,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3DCCD936-D085-4869-A1DE-CA6A64152C94}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\modules\LightSwitch\Tests\LightSwitch.UITests\LightSwitch.UITests.csproj", "{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2883,6 +2903,22 @@ Global
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
+ {38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|ARM64.Build.0 = Debug|ARM64
+ {38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|x64.ActiveCfg = Debug|x64
+ {38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|x64.Build.0 = Debug|x64
+ {38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|ARM64.ActiveCfg = Release|ARM64
+ {38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|ARM64.Build.0 = Release|ARM64
+ {38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|x64.ActiveCfg = Release|x64
+ {38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|x64.Build.0 = Release|x64
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|ARM64.Build.0 = Debug|ARM64
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|x64.ActiveCfg = Debug|x64
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|x64.Build.0 = Debug|x64
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|ARM64.ActiveCfg = Release|ARM64
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|ARM64.Build.0 = Release|ARM64
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|x64.ActiveCfg = Release|x64
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|x64.Build.0 = Release|x64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.ActiveCfg = Debug|ARM64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.Build.0 = Debug|ARM64
{66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.ActiveCfg = Debug|x64
@@ -2891,6 +2927,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
@@ -2923,6 +2975,26 @@ Global
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Build.0 = Debug|ARM64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Deploy.0 = Debug|ARM64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.ActiveCfg = Debug|x64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.Build.0 = Debug|x64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.Deploy.0 = Debug|x64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.ActiveCfg = Release|ARM64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.Build.0 = Release|ARM64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.Deploy.0 = Release|ARM64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.ActiveCfg = Release|x64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.Build.0 = Release|x64
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.Deploy.0 = Release|x64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.Build.0 = Debug|ARM64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.ActiveCfg = Debug|x64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.Build.0 = Debug|x64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.ActiveCfg = Release|ARM64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3160,7 +3232,7 @@ Global
{F3D09629-59A2-4924-A4B9-D6BFAA2C1B49} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{305DD37E-C85D-4B08-AAFE-7381FA890463} = {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49}
{CA4D810F-C8F4-4B61-9DA9-71807E0B9F24} = {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49}
- {14E62033-58D0-4A7D-8990-52F50A08BBBD} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
+ {14E62033-58D0-4A7D-8990-52F50A08BBBD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
{071E18A4-A530-46B8-AB7D-B862EE55E24E} = {3846508C-77EB-4034-A702-F8BB263C4F79}
{C846F7A7-792A-47D9-B0CB-417C900EE03D} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
@@ -3236,12 +3308,21 @@ Global
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
+ {5B201255-53C8-490B-A34F-01F05D48A477} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
+ {38177D56-6AD1-4ADF-88C9-2843A7932166} = {5B201255-53C8-490B-A34F-01F05D48A477}
+ {08E71C67-6A7E-4CA1-B04E-2FB336410BAC} = {5B201255-53C8-490B-A34F-01F05D48A477}
{E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1}
+ {9605B84E-FAC4-477B-B9EC-0753177EE6A8} = {557C4636-D7E1-4838-A504-7D19B725EE95}
+ {94CDC147-6137-45E9-AEDE-17FF809607C0} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8}
+ {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8}
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
+ {3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
+ {F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
+ {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/README.md b/README.md
index cf2dd7beba..631b43d6aa 100644
--- a/README.md
+++ b/README.md
@@ -1,38 +1,56 @@
-# Microsoft PowerToys
+
+
+
+
+
+
+
+ Microsoft PowerToys
+
-
+
+
+Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.
+
-[How to use PowerToys][usingPowerToys-docs-link] | [Downloads & Release notes][github-release-link] | [Contributing to PowerToys](#contributing) | [What's Happening](#whats-happening) | [Roadmap](#powertoys-roadmap)
+| | | |
+|---|---|---|
+| [ Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [ Always on Top](https://aka.ms/PowerToysOverview_AoT) | [ Awake](https://aka.ms/PowerToysOverview_Awake) |
+| [ Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [ Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [ Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
+| [ Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [ Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [ FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
+| [ File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [ File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [ Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
+| [ Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [ Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [ Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
+| [ Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [ New+](https://aka.ms/PowerToysOverview_NewPlus) | [ Peek](https://aka.ms/PowerToysOverview_Peek) |
+| [ PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [ PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [ Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
+| [ Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [ Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [ Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
+| [ Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [ Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [ ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
-## About
-Microsoft PowerToys is a set of utilities for power users to tune and streamline their Windows experience for greater productivity. For more info on [PowerToys overviews and how to use the utilities][usingPowerToys-docs-link], or any other tools and resources for [Windows development environments](https://learn.microsoft.com/windows/dev-environment/overview), head over to [learn.microsoft.com][usingPowerToys-docs-link]!
+## 📋 Installation
-| | Current utilities: | |
-|--------------|--------------------|--------------|
-| [Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) |
-| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
-| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
-| [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
-| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) |
-| [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) |
-| [Peek](https://aka.ms/PowerToysOverview_Peek) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
-| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
-| [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
-| [ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
+For detailed installation instructions, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
-## Installing and running Microsoft PowerToys
+Before you begin, make sure your device meets the system requirements:
-### Requirements
+> [!NOTE]
+> - Windows 11 or Windows 10 version 2004 (20H1 / build 19041) or newer
+> - 64-bit processor: x64 or ARM64
+> - Latest stable version of [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) is installed via the bootstrapper during setup
-- Windows 11 or Windows 10 version 2004 (code name 20H1 / build number 19041) or newer.
-- x64 or ARM64 processor
-- Our installer will install the following items:
- - [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) bootstrapper. This will install the latest version.
+Choose one of the installation methods below:
-### Via GitHub with EXE [Recommended]
+
+Download .exe from GitHub
-Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
+Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
@@ -49,57 +67,49 @@ Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and cl
| Machine wide - x64 | [PowerToysSetup-0.94.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.94.0-arm64.exe][ptMachineArm64] |
-This is our preferred method.
+
-### Via Microsoft Store
+
+Microsoft Store
+You can easily install PowerToys from the Microsoft Store:
+
+
+
+
+
+
+
+
-Install from the [Microsoft Store's PowerToys page][microsoft-store-link]. You must be using the [new Microsoft Store](https://blogs.windows.com/windowsExperience/2021/06/24/building-a-new-open-microsoft-store-on-windows-11/), which is available for both Windows 11 and Windows 10.
-### Via WinGet
+
+WinGet
+
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
-#### User scope installer [default]
+*User scope installer [default]*
```powershell
winget install Microsoft.PowerToys -s winget
```
-#### Machine-wide scope installer
-
+*Machine-wide scope installer*
```powershell
winget install --scope machine Microsoft.PowerToys -s winget
```
+
-### Other install methods
+
+Other methods
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
+
-## Third-Party Run Plugins
+## ✨ What's new
+**Version 0.94 (September 2025)**
-There is a collection of [third-party plugins](./doc/thirdPartyRunPlugins.md) created by the community that aren't distributed with PowerToys.
+For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
-## Contributing
-
-This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
-
-We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
-
-Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
-
-For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
-
-## What's Happening
-
-### PowerToys Roadmap
-
-Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
-
-### 0.94 - Sep 2025 Update
-
-In this release, we focused on new features, stability, optimization improvements, and automation.
-
-For an in-depth look at the latest changes, visit the [release blog](https://aka.ms/powertoys-releaseblog).
-
-**✨Highlights**
+**✨ Highlights**
- PowerToys Settings added a Settings search with fuzzy matching, suggestions, a results page, and UX polish to make finding options faster.
- A comprehensive hotkey conflict detection system was introduced in Settings to surface and help resolve conflicting shortcuts. Note that the default hotkey settings (Win+Ctrl+Shift+T, Win+Ctrl+V, Win+Ctrl+T, Win+Shift+T) may overlap with existing Windows system shortcuts. This is expected. You can resolve the conflict by assigning different hotkeys.
@@ -138,13 +148,13 @@ For an in-depth look at the latest changes, visit the [release blog](https://aka
- Allowed providers to override Dispose with a virtual method.
- Fixed memory leaks by cleaning up removed or cancelled list items.
- Sorted DateTime extension results by relevance for better usability.
- - Reduced search text “jiggling” by avoiding redundant change notifications.
+ - Reduced search text "jiggling" by avoiding redundant change notifications.
- Centralized automation notifications in a UIHelper for better accessibility. Thanks [@chatasweetie](https://github.com/chatasweetie)!
- Preserved Adaptive Card action types during trimming via DynamicDependency.
- Added an acrylic backdrop and refined styling to the context menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Prevented disposed pages and Settings windows from handling stale messages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
- Made the extension API easier to evolve without breaking clients.
- - Added “evil” sample pages to help reproduce tricky bugs.
+ - Added "evil" sample pages to help reproduce tricky bugs.
- Fixed WinGet trim-safety issues by replacing LINQ with manual iteration.
- Cancelled stale list fetches to avoid older results overwriting newer ones in CmdPal.
@@ -220,10 +230,10 @@ For an in-depth look at the latest changes, visit the [release blog](https://aka
- Rewrote system command tests with a new test base and cleaner patterns.
- Added unit tests for WebSearch and Shell extensions with mockable settings.
- Added unit tests and abstractions for Apps and Bookmarks extensions.
- - Cleans up AI‑generated tests; adds meaningful query tests across extensions.
+ - Cleans up AI-generated tests; adds meaningful query tests across extensions.
- Removed the obsolete debug dialog from Settings for a smoother developer loop.
-### What is being planned over the next few releases
+## 🛣️ Roadmap
For [v0.95][github-next-release-work], we'll work on the items below:
@@ -235,9 +245,19 @@ For [v0.95][github-next-release-work], we'll work on the items below:
- New UI automation tests
- Stability, bug fixes
-## PowerToys Community
+## ❤️ PowerToys Community
-The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn’t be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Month by month, you directly help make PowerToys a better piece of software.
+The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn't be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Your contributions and feedback improve PowerToys month after month!
+
+## Contributing
+
+This project welcomes contributions of all types. Besides coding features / bug fixes, other ways to assist include spec writing, design, documentation, and finding bugs. We are excited to work with the power user community to build a set of tools for helping you get the most out of Windows.
+
+We ask that **before you start work on a feature that you would like to contribute**, please read our [Contributor's Guide](CONTRIBUTING.md). We would be happy to work with you to figure out the best approach, provide guidance and mentorship throughout feature development, and help avoid any wasted or duplicate effort.
+
+Most contributions require you to agree to a [Contributor License Agreement (CLA)][oss-CLA] declaring that you grant us the rights to use your contribution and that you have permission to do so.
+
+For guidance on developing for PowerToys, please read the [developer docs](./doc/devdocs) for a detailed breakdown. This includes how to setup your computer to compile.
## Code of Conduct
diff --git a/doc/devdocs/UITests.md b/doc/devdocs/development/ui-tests.md
similarity index 100%
rename from doc/devdocs/UITests.md
rename to doc/devdocs/development/ui-tests.md
diff --git a/doc/devdocs/modules/lightswitch.md b/doc/devdocs/modules/lightswitch.md
new file mode 100644
index 0000000000..1e251dfff1
--- /dev/null
+++ b/doc/devdocs/modules/lightswitch.md
@@ -0,0 +1,107 @@
+# Light Switch
+
+[Public Overview – Microsoft Learn](https://learn.microsoft.com/en-us/windows/powertoys/light-switch)
+
+## Quick Links
+
+* [All Issues](https://github.com/microsoft/PowerToys/issues?q=is%3Aissue%20state%3Aopen%20label%3AProduct-LightSwitch)
+* [Bugs](https://github.com/microsoft/PowerToys/issues?q=is%3Aissue%20state%3Aopen%20label%3AProduct-LightSwitch%20label%3AIssue-Bug)
+* [Pull Requests](https://github.com/microsoft/PowerToys/pulls?q=is%3Apr+is%3Aopen+label%3AProduct-LightSwitch)
+
+## Overview
+
+The **Light Switch** module lets users automatically transition between light and dark mode using a timed schedule or a keyboard shortcut.
+
+## Features
+
+* Set custom times to start and stop dark mode.
+* Use geolocation to determine local sunrise and sunset times.
+* Apply offsets in sunrise mode (e.g., 15 minutes before sunset).
+* Quickly toggle between modes with a keyboard shortcut (`Ctrl+Shift+Win+D` by default).
+* Choose whether theme changes apply to:
+
+ * Apps only
+ * System only
+ * Both apps and system
+
+## Architecture
+
+### Main Components
+
+* **Shortcut/Hotkey**
+ Listens for a hotkey event. Calling `onHotkey()` flips the theme flags.
+
+ > **Note:** Using the shortcut overrides the current schedule until the next transition event.
+
+* **LightSwitchService**
+ Reads settings and applies theming. Runs a check every minute to ensure the state is correct.
+
+* **SettingsXAML/LightSwitch**
+ Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.
+
+* **Settings.UI/ViewModels/LightSwitchViewModel.cs**
+ Handles updates to the settings file and communicates changes to the front end.
+
+* **modules/LightSwitch/Tests**
+ Contains UI tests that verify interactions between the settings UI, system state, and `settings.json`.
+
+### Data Flow
+
+1. User configures settings in the UI (default: manual mode, light mode from 06:00–18:00).
+2. Every minute, the service checks the time.
+
+ * If it’s not a threshold, the service sleeps until the next minute.
+ * If it matches a threshold, the service applies the theme based on settings and returns to sleep.
+3. At **midnight**, when in *Sunrise to Sunset* mode, the service updates daily sunrise and sunset times.
+4. If the machine was asleep during a scheduled event, the service applies the correct settings at the next check.
+
+## User Interface
+
+The module’s settings are exposed in the PowerToys Settings UI. Options include:
+
+* Shortcut customization
+* Mode selection (Manual or Sunrise to Sunset)
+* Manual start/stop times (manual mode only)
+* Automatic sunrise/sunset calculation (location-based)
+* Time offsets (sunrise mode)
+* Target scope (system, apps, or both)
+
+## Development Environment Setup
+
+### Prerequisites
+
+* Visual Studio 2019 or later
+* Windows 10 SDK
+* PowerToys repository cloned from GitHub
+
+### Building and Testing
+
+1. Clone the repo:
+
+ ```sh
+ git clone https://github.com/microsoft/PowerToys.git
+ ```
+2. Initialize submodules:
+
+ ```sh
+ git submodule update --init --recursive
+ ```
+3. Build the solution:
+
+ ```sh
+ msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln
+ ```
+
+ > Note: This may take some time.
+4. Set `runner` as the startup project and press **F5**.
+5. Enable Light Switch in PowerToys Settings.
+6. To debug the service:
+
+ * Press `Ctrl+Alt+P` or go to **Debug > Attach to Process**.
+ * Select `LightSwitchService.exe` and click **Attach**.
+ * You can now set breakpoints in the service files.
+7. To debug the Settings UI:
+
+ * Set the startup project to `PowerToys.Settings` and press **F5**.
+ * Note: Light Switch settings will not persist in this mode (they depend on the service executable).
+ * Alternatively, you can attach `PowerToys.Settings.exe` to the debugger while `runner` is running to test the full flow with breakpoints.
diff --git a/doc/dsc/Settings.md b/doc/dsc/Settings.md
new file mode 100644
index 0000000000..24fab1e2dd
--- /dev/null
+++ b/doc/dsc/Settings.md
@@ -0,0 +1,83 @@
+# Settings resource
+Manage the settings for PowerToys modules
+
+## Commands
+
+### ✨ Modules
+List all the modules supported by the settings resource.
+```shell
+PS C:\> PowerToys.DSC.exe modules --resource 'settings'
+AdvancedPaste
+AlwaysOnTop
+App
+Awake
+ColorPicker
+CropAndLock
+EnvironmentVariables
+FancyZones
+FileLocksmith
+FindMyMouse
+Hosts
+ImageResizer
+KeyboardManager
+MeasureTool
+MouseHighlighter
+MouseJump
+MousePointerCrosshairs
+Peek
+PowerAccent
+PowerOCR
+PowerRename
+RegistryPreview
+ShortcutGuide
+Workspaces
+ZoomIt
+```
+
+### 📄 Get
+Get the settings for a specific module.
+```shell
+PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables
+{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}}
+```
+
+### 🖨️ Export
+Export the settings for a specific module.
+
+ℹ️ Settings resource Get and Export operation output states are identical.
+```shell
+PS C:\> PowerToys.DSC.exe get --resource 'settings' --module EnvironmentVariables
+{"settings":{"properties":{"LaunchAdministrator":{"value":true}},"name":"EnvironmentVariables","version":"1.0"}}
+```
+
+### 📝 Set
+Set the settings for a specific module. This command will update the settings to the specified values.
+```shell
+PS C:\> PowerToys.DSC.exe set --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}'
+{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}
+["settings"]
+```
+
+### 🧪 Test
+Test the settings for a specific module. This command will check if the current settings match the desired state.
+```shell
+PS C:\> PowerToys.DSC.exe test --resource 'settings' --module Awake --input '{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000002-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"}}'
+{"settings":{"properties":{"keepDisplayOn":false,"mode":0,"intervalHours":0,"intervalMinutes":1,"expirationDateTime":"2025-08-13T10:10:00.000001-07:00","customTrayTimes":{}},"name":"Awake","version":"0.0.1"},"_inDesiredState":false}
+["settings"]
+```
+
+### 🛠️ Schema
+Generates the JSON schema for the settings resource of a specific module.
+```shell
+PS C:\> PowerToys.DSC.exe schema --resource 'settings' --module Awake
+{"$schema":"http://json-schema.org/draft-04/schema#","title":"SettingsResourceObjectOfAwakeSettings","type":"object","additionalProperties":false,"required":["settings"],"properties":{"_inDesiredState":{"type":["boolean","null"],"description":"Indicates whether an instance is in the desired state"},"settings":{"description":"The settings content for the module."}}}
+PS E:\src\powertoys> PowerToys.DSC.exe schema --resource 'settings' --module Awake | Format-Json
+```
+
+### 📦 Manifest
+Generates a manifest dsc resource JSON file for the specified module.
+- If the module is not specified, it will generate a manifest for all modules.
+- If the output directory is not specified, it will print the manifest to the console.
+```shell
+PS C:\> PowerToys.DSC.exe manifest --resource settings --module 'Awake' --outputDir "C:\manifests"
+```
\ No newline at end of file
diff --git a/doc/images/icons/Command Palette.png b/doc/images/icons/Command Palette.png
new file mode 100644
index 0000000000..7360fdd113
Binary files /dev/null and b/doc/images/icons/Command Palette.png differ
diff --git a/doc/images/icons/Light Switch.png b/doc/images/icons/Light Switch.png
new file mode 100644
index 0000000000..8a0778ff05
Binary files /dev/null and b/doc/images/icons/Light Switch.png differ
diff --git a/doc/images/icons/ZoomIt.png b/doc/images/icons/ZoomIt.png
new file mode 100644
index 0000000000..777a30bd1f
Binary files /dev/null and b/doc/images/icons/ZoomIt.png differ
diff --git a/doc/images/overview/LightSwitch_large.png b/doc/images/overview/LightSwitch_large.png
new file mode 100644
index 0000000000..3a98b7f3e2
Binary files /dev/null and b/doc/images/overview/LightSwitch_large.png differ
diff --git a/doc/images/overview/LightSwitch_small.png b/doc/images/overview/LightSwitch_small.png
new file mode 100644
index 0000000000..c6e94735a9
Binary files /dev/null and b/doc/images/overview/LightSwitch_small.png differ
diff --git a/doc/images/overview/Original/Light Switch.png b/doc/images/overview/Original/Light Switch.png
new file mode 100644
index 0000000000..04e551a85d
Binary files /dev/null and b/doc/images/overview/Original/Light Switch.png differ
diff --git a/doc/images/overview/PT_hero_image.png b/doc/images/overview/PT_hero_image.png
deleted file mode 100644
index 026a456297..0000000000
Binary files a/doc/images/overview/PT_hero_image.png and /dev/null differ
diff --git a/doc/images/overview/PT_large.png b/doc/images/overview/PT_large.png
deleted file mode 100644
index 340cde5283..0000000000
Binary files a/doc/images/overview/PT_large.png and /dev/null differ
diff --git a/doc/images/overview/PT_small.png b/doc/images/overview/PT_small.png
deleted file mode 100644
index 4c66f43b62..0000000000
Binary files a/doc/images/overview/PT_small.png and /dev/null differ
diff --git a/doc/images/readme/StoreBadge-dark.png b/doc/images/readme/StoreBadge-dark.png
new file mode 100644
index 0000000000..8095159a82
Binary files /dev/null and b/doc/images/readme/StoreBadge-dark.png differ
diff --git a/doc/images/readme/StoreBadge-light.png b/doc/images/readme/StoreBadge-light.png
new file mode 100644
index 0000000000..fc4c9aa8eb
Binary files /dev/null and b/doc/images/readme/StoreBadge-light.png differ
diff --git a/doc/images/readme/pt-hero.dark.png b/doc/images/readme/pt-hero.dark.png
new file mode 100644
index 0000000000..e0ac68155a
Binary files /dev/null and b/doc/images/readme/pt-hero.dark.png differ
diff --git a/doc/images/readme/pt-hero.light.png b/doc/images/readme/pt-hero.light.png
new file mode 100644
index 0000000000..8cdda7b92f
Binary files /dev/null and b/doc/images/readme/pt-hero.light.png differ
diff --git a/doc/thirdPartyRunPlugins.md b/doc/thirdPartyRunPlugins.md
index eccdc3530e..a15cb542a8 100644
--- a/doc/thirdPartyRunPlugins.md
+++ b/doc/thirdPartyRunPlugins.md
@@ -50,6 +50,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. |
| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. |
| [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI |
+| [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. |
## Extending software plugins
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..308b304591 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp
@@ -3,6 +3,7 @@
#include "RcResource.h"
#include
#include
+#include
#include "../../src/common/logger/logger.h"
#include "../../src/common/utils/gpo.h"
@@ -232,7 +233,9 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
auto action = [&commandLine](HANDLE userToken)
{
- STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL};
+ STARTUPINFO startupInfo = { 0 };
+ startupInfo.cb = sizeof(STARTUPINFO);
+ startupInfo.wShowWindow = SW_SHOWNORMAL;
PROCESS_INFORMATION processInformation;
PVOID lpEnvironment = NULL;
@@ -271,7 +274,9 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
}
else
{
- STARTUPINFO startupInfo{.cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL};
+ STARTUPINFO startupInfo = { 0 };
+ startupInfo.cb = sizeof(STARTUPINFO);
+ startupInfo.wShowWindow = SW_SHOWNORMAL;
PROCESS_INFORMATION processInformation;
@@ -424,7 +429,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
const auto modulesPath = baseModulesPath / L"Microsoft.PowerToys.Configure" / (get_product_version(false) + L".0");
std::error_code errorCode;
- fs::create_directories(modulesPath, errorCode);
+ std::filesystem::create_directories(modulesPath, errorCode);
if (errorCode)
{
hr = E_FAIL;
@@ -433,7 +438,7 @@ UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME})
{
- fs::copy_file(fs::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, fs::copy_options::overwrite_existing, errorCode);
+ std::filesystem::copy_file(std::filesystem::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, std::filesystem::copy_options::overwrite_existing, errorCode);
if (errorCode)
{
@@ -481,7 +486,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *filename : {DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME})
{
- fs::remove(versionedModulePath / filename, errorCode);
+ std::filesystem::remove(versionedModulePath / filename, errorCode);
if (errorCode)
{
@@ -492,7 +497,7 @@ UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
for (const auto *modulePath : {&versionedModulePath, &powerToysModulePath})
{
- fs::remove(*modulePath, errorCode);
+ std::filesystem::remove(*modulePath, errorCode);
if (errorCode)
{
@@ -1278,7 +1283,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
}
processes.resize(bytes / sizeof(processes[0]));
- std::array processesToTerminate = {
+ std::array processesToTerminate = {
L"PowerToys.PowerLauncher.exe",
L"PowerToys.Settings.exe",
L"PowerToys.AdvancedPaste.exe",
@@ -1293,6 +1298,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
L"PowerToys.Hosts.exe",
L"PowerToys.PowerRename.exe",
L"PowerToys.ImageResizer.exe",
+ L"PowerToys.LightSwitchService.exe",
L"PowerToys.GcodeThumbnailProvider.exe",
L"PowerToys.BgcodeThumbnailProvider.exe",
L"PowerToys.PdfThumbnailProvider.exe",
@@ -1375,6 +1381,120 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
return WcaFinalize(er);
}
+UINT __stdcall SetBundleInstallLocationCA(MSIHANDLE hInstall)
+{
+ HRESULT hr = S_OK;
+ UINT er = ERROR_SUCCESS;
+
+ // Declare all variables at the beginning to avoid goto issues
+ std::wstring customActionData;
+ std::wstring installationFolder;
+ std::wstring bundleUpgradeCode;
+ std::wstring installScope;
+ bool isPerUser = false;
+ size_t pos1 = std::wstring::npos;
+ size_t pos2 = std::wstring::npos;
+ std::vector keysToTry;
+
+ hr = WcaInitialize(hInstall, "SetBundleInstallLocationCA");
+ ExitOnFailure(hr, "Failed to initialize");
+
+ // Parse CustomActionData: "installFolder;upgradeCode;installScope"
+ hr = getInstallFolder(hInstall, customActionData);
+ ExitOnFailure(hr, "Failed to get CustomActionData.");
+
+ pos1 = customActionData.find(L';');
+ if (pos1 == std::wstring::npos)
+ {
+ hr = E_INVALIDARG;
+ ExitOnFailure(hr, "Invalid CustomActionData format - missing first semicolon");
+ }
+
+ pos2 = customActionData.find(L';', pos1 + 1);
+ if (pos2 == std::wstring::npos)
+ {
+ hr = E_INVALIDARG;
+ ExitOnFailure(hr, "Invalid CustomActionData format - missing second semicolon");
+ }
+
+ installationFolder = customActionData.substr(0, pos1);
+ bundleUpgradeCode = customActionData.substr(pos1 + 1, pos2 - pos1 - 1);
+ installScope = customActionData.substr(pos2 + 1);
+
+ isPerUser = (installScope == L"perUser");
+
+ // Use the appropriate registry based on install scope
+ HKEY targetKey = isPerUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
+ const wchar_t* keyName = isPerUser ? L"HKCU" : L"HKLM";
+
+ WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Searching for Bundle in %ls registry", keyName);
+
+ HKEY uninstallKey;
+ LONG openResult = RegOpenKeyExW(targetKey, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", 0, KEY_READ | KEY_ENUMERATE_SUB_KEYS, &uninstallKey);
+ if (openResult != ERROR_SUCCESS)
+ {
+ WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to open uninstall key, error: %ld", openResult);
+ goto LExit;
+ }
+
+ DWORD index = 0;
+ wchar_t subKeyName[256];
+ DWORD subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t);
+
+ while (RegEnumKeyExW(uninstallKey, index, subKeyName, &subKeyNameSize, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS)
+ {
+ HKEY productKey;
+ if (RegOpenKeyExW(uninstallKey, subKeyName, 0, KEY_READ | KEY_WRITE, &productKey) == ERROR_SUCCESS)
+ {
+ wchar_t upgradeCode[256];
+ DWORD upgradeCodeSize = sizeof(upgradeCode);
+ DWORD valueType;
+
+ if (RegQueryValueExW(productKey, L"BundleUpgradeCode", nullptr, &valueType,
+ reinterpret_cast(upgradeCode), &upgradeCodeSize) == ERROR_SUCCESS)
+ {
+ // Remove brackets from registry upgradeCode for comparison (bundleUpgradeCode doesn't have brackets)
+ std::wstring regUpgradeCode = upgradeCode;
+ if (!regUpgradeCode.empty() && regUpgradeCode.front() == L'{' && regUpgradeCode.back() == L'}')
+ {
+ regUpgradeCode = regUpgradeCode.substr(1, regUpgradeCode.length() - 2);
+ }
+
+ if (_wcsicmp(regUpgradeCode.c_str(), bundleUpgradeCode.c_str()) == 0)
+ {
+ // Found matching Bundle, set InstallLocation
+ LONG setResult = RegSetValueExW(productKey, L"InstallLocation", 0, REG_SZ,
+ reinterpret_cast(installationFolder.c_str()),
+ static_cast((installationFolder.length() + 1) * sizeof(wchar_t)));
+
+ if (setResult == ERROR_SUCCESS)
+ {
+ WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: InstallLocation set successfully");
+ }
+ else
+ {
+ WcaLog(LOGMSG_STANDARD, "SetBundleInstallLocationCA: Failed to set InstallLocation, error: %ld", setResult);
+ }
+
+ RegCloseKey(productKey);
+ RegCloseKey(uninstallKey);
+ goto LExit;
+ }
+ }
+ RegCloseKey(productKey);
+ }
+
+ index++;
+ subKeyNameSize = sizeof(subKeyName) / sizeof(wchar_t);
+ }
+
+ RegCloseKey(uninstallKey);
+
+LExit:
+ er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
+ return WcaFinalize(er);
+}
+
void initSystemLogger()
{
static std::once_flag initLoggerFlag;
diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
index 39efc9ff70..931a555953 100644
--- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
+++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def
@@ -32,4 +32,4 @@ EXPORTS
CleanFileLocksmithRuntimeRegistryCA
CleanPowerRenameRuntimeRegistryCA
CleanNewPlusRuntimeRegistryCA
-
\ No newline at end of file
+ SetBundleInstallLocationCA
diff --git a/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj b/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj
index db6f6e6392..7cd49be6ea 100644
--- a/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj
+++ b/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj
@@ -65,6 +65,7 @@
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Hosts.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Hosts.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\ImageResizer.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\ImageResizer.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\KeyboardManager.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\KeyboardManager.wxs.bk""""
+ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\LightSwitch.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\LightSwitch.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\MouseWithoutBorders.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\MouseWithoutBorders.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\NewPlus.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\NewPlus.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Peek.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Peek.wxs.bk""""
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/LightSwitch.wxs b/installer/PowerToysSetupVNext/LightSwitch.wxs
new file mode 100644
index 0000000000..01f4bc329b
--- /dev/null
+++ b/installer/PowerToysSetupVNext/LightSwitch.wxs
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..5341f66768 100644
--- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
+++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
@@ -14,6 +14,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" x64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
+call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
@@ -24,6 +25,7 @@ SET PTRoot=$(SolutionDir)\..
call "..\..\..\publish.cmd" arm64
)
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
+call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
@@ -39,6 +41,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs
call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs
call move /Y ..\..\..\Hosts.wxs.bk ..\..\..\Hosts.wxs
+ call move /Y ..\..\..\LightSwitch.wxs.bk ..\..\..\LightSwitch.wxs
call move /Y ..\..\..\ImageResizer.wxs.bk ..\..\..\ImageResizer.wxs
call move /Y ..\..\..\KeyboardManager.wxs.bk ..\..\..\KeyboardManager.wxs
call move /Y ..\..\..\MouseWithoutBorders.wxs.bk ..\..\..\MouseWithoutBorders.wxs
@@ -112,9 +115,11 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
+
+
diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs
index e343897d5d..2505557d77 100644
--- a/installer/PowerToysSetupVNext/Product.wxs
+++ b/installer/PowerToysSetupVNext/Product.wxs
@@ -50,6 +50,7 @@
+
@@ -62,6 +63,7 @@
+
@@ -69,8 +71,8 @@
-
-
+
+
@@ -117,6 +119,8 @@
+
+
@@ -160,6 +164,9 @@
+
+
+
@@ -244,6 +251,8 @@
+
+
diff --git a/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj b/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj
index dfa43efebe..3972c1b0f7 100644
--- a/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj
+++ b/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj
@@ -26,6 +26,7 @@
DynamicLibrary
Unicode
SilentFilesInUseBAFunction
+ PowerToysSetupCustomActionsVNext
bafunctions.def
10.0
@@ -91,5 +92,31 @@
+
+
+
+ _DEBUG;%(PreprocessorDefinitions)
+ Disabled
+ MultiThreadedDebug
+
+
+ true
+
+
+
+
+ NDEBUG;%(PreprocessorDefinitions)
+ MaxSpeed
+ MultiThreaded
+ true
+ true
+
+
+ true
+ true
+ true
+
+
+
diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
index fb63868f93..b6f2f88dd0 100644
--- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
+++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
@@ -182,6 +182,10 @@ Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptR
Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer"
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs -regroot $registryroot
+# Light Switch Service
+Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService"
+Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs -regroot $registryroot
+
#New+
Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus"
Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs -regroot $registryroot
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/nuget.config b/nuget.config
index 51f9b3b3f7..6b8d13a023 100644
--- a/nuget.config
+++ b/nuget.config
@@ -9,4 +9,4 @@
-
+
\ No newline at end of file
diff --git a/src/CmdPalVersion.props b/src/CmdPalVersion.props
index 2be9bc69d4..c3c5d7b608 100644
--- a/src/CmdPalVersion.props
+++ b/src/CmdPalVersion.props
@@ -2,7 +2,10 @@
$(XES_APPXMANIFESTVERSION)
+
+
0.0.1.0
+
Local
diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs
index 84945e6939..1891532d16 100644
--- a/src/common/Common.UI/SettingsDeepLink.cs
+++ b/src/common/Common.UI/SettingsDeepLink.cs
@@ -17,6 +17,7 @@ namespace Common.UI
Awake,
ColorPicker,
CmdNotFound,
+ LightSwitch,
FancyZones,
FileLocksmith,
Run,
@@ -60,6 +61,8 @@ namespace Common.UI
return "ColorPicker";
case SettingsWindow.CmdNotFound:
return "CmdNotFound";
+ case SettingsWindow.LightSwitch:
+ return "LightSwitch";
case SettingsWindow.FancyZones:
return "FancyZones";
case SettingsWindow.FileLocksmith:
diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp
index 87ef1721b1..361255f66f 100644
--- a/src/common/GPOWrapper/GPOWrapper.cpp
+++ b/src/common/GPOWrapper/GPOWrapper.cpp
@@ -28,6 +28,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast(powertoys_gpo::getConfiguredCropAndLockEnabledValue());
}
+ GpoRuleConfigured GPOWrapper::GetConfiguredLightSwitchEnabledValue()
+ {
+ return static_cast(powertoys_gpo::getConfiguredLightSwitchEnabledValue());
+ }
GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue()
{
return static_cast(powertoys_gpo::getConfiguredFancyZonesEnabledValue());
diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h
index 33f90e15c9..c0fff9f542 100644
--- a/src/common/GPOWrapper/GPOWrapper.h
+++ b/src/common/GPOWrapper/GPOWrapper.h
@@ -13,6 +13,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
+ static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl
index 252b4d128a..630beab9c9 100644
--- a/src/common/GPOWrapper/GPOWrapper.idl
+++ b/src/common/GPOWrapper/GPOWrapper.idl
@@ -17,6 +17,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredCmdPalEnabledValue();
static GpoRuleConfigured GetConfiguredColorPickerEnabledValue();
static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue();
+ static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue();
static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue();
static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue();
static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue();
diff --git a/src/common/ManagedCommon/Logger.cs b/src/common/ManagedCommon/Logger.cs
index 150d6ea355..11115b1846 100644
--- a/src/common/ManagedCommon/Logger.cs
+++ b/src/common/ManagedCommon/Logger.cs
@@ -19,7 +19,9 @@ namespace ManagedCommon
private static readonly string Error = "Error";
private static readonly string Warning = "Warning";
private static readonly string Info = "Info";
+#if DEBUG
private static readonly string Debug = "Debug";
+#endif
private static readonly string TraceFlag = "Trace";
private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "Unknown";
@@ -151,7 +153,9 @@ namespace ManagedCommon
public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
+#if DEBUG
Log(message, Debug, memberName, sourceFilePath, sourceLineNumber);
+#endif
}
public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs
index 65b00d4b5a..aa741e2f3a 100644
--- a/src/common/ManagedCommon/ModuleType.cs
+++ b/src/common/ManagedCommon/ModuleType.cs
@@ -19,6 +19,7 @@ namespace ManagedCommon
Hosts,
ImageResizer,
KeyboardManager,
+ LightSwitch,
MouseHighlighter,
MouseJump,
MousePointerCrosshairs,
diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs
index 1868a9c34d..6e9efabeac 100644
--- a/src/common/UITestAutomation/Element/Element.cs
+++ b/src/common/UITestAutomation/Element/Element.cs
@@ -81,6 +81,14 @@ namespace Microsoft.PowerToys.UITest
get { return this.windowsElement?.Selected ?? false; }
}
+ ///
+ /// Gets a value indicating whether the UI element is visible to the user.
+ ///
+ public bool Displayed
+ {
+ get { return this.windowsElement?.Displayed ?? false; }
+ }
+
///
/// Gets the Rect of the UI element.
///
@@ -329,7 +337,7 @@ namespace Microsoft.PowerToys.UITest
/// Send Key of the element.
///
/// The Key to Send.
- protected void SendKeys(string key)
+ public void SendKeys(string key)
{
PerformAction((actions, windowElement) =>
{
@@ -369,5 +377,19 @@ namespace Microsoft.PowerToys.UITest
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method SaveToPngFile with parameter: path = {path}");
this.windowsElement.GetScreenshot().SaveAsFile(path);
}
+
+ public void EnsureVisible(Element scrollViewer, int maxScrolls = 10)
+ {
+ int count = 0;
+ if (scrollViewer.WindowsElement != null)
+ {
+ while (!this.windowsElement!.Displayed && count < maxScrolls)
+ {
+ scrollViewer.WindowsElement.SendKeys(OpenQA.Selenium.Keys.PageDown);
+ Task.Delay(250).Wait();
+ count++;
+ }
+ }
+ }
}
}
diff --git a/src/common/UITestAutomation/ModuleConfigData.cs b/src/common/UITestAutomation/ModuleConfigData.cs
index ac3f5ffe26..4dcd168da3 100644
--- a/src/common/UITestAutomation/ModuleConfigData.cs
+++ b/src/common/UITestAutomation/ModuleConfigData.cs
@@ -34,6 +34,7 @@ namespace Microsoft.PowerToys.UITest
PowerRename,
CommandPalette,
ScreenRuler,
+ LightSwitch,
}
///
@@ -106,6 +107,7 @@ namespace Microsoft.PowerToys.UITest
[PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"),
[PowerToysModule.CommandPalette] = new ModuleInfo("Microsoft.CmdPal.UI.exe", "PowerToys Command Palette", "WinUI3Apps\\CmdPal"),
[PowerToysModule.ScreenRuler] = new ModuleInfo("PowerToys.MeasureToolUI.exe", "PowerToys.ScreenRuler", "WinUI3Apps"),
+ [PowerToysModule.LightSwitch] = new ModuleInfo("PowerToys.LightSwitch.exe", "PowerToys.LightSwitch", "LightSwitchService"),
};
}
diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj
index 17841e0a60..add7acfeb9 100644
--- a/src/common/UITestAutomation/UITestAutomation.csproj
+++ b/src/common/UITestAutomation/UITestAutomation.csproj
@@ -17,8 +17,6 @@
-
-
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index 00cde3b485..b2e05fadfe 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -81,6 +81,7 @@ struct LogSettings
inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool";
inline const static std::wstring workspacesSnapshotToolLogPath = L"workspaces-snapshot-tool-log.log";
inline const static std::string zoomItLoggerName = "zoom-it";
+ inline const static std::string lightSwitchLoggerName = "light-switch";
inline const static int retention = 30;
std::wstring logLevel;
LogSettings();
diff --git a/src/common/utils/elevation.h b/src/common/utils/elevation.h
index 7f2ecbf6df..e412ce5aa3 100644
--- a/src/common/utils/elevation.h
+++ b/src/common/utils/elevation.h
@@ -257,7 +257,9 @@ inline HANDLE run_elevated(const std::wstring& file, const std::wstring& params,
exec_info.nShow = SW_HIDE;
}
- return ShellExecuteExW(&exec_info) ? exec_info.hProcess : nullptr;
+ BOOL result = ShellExecuteExW(&exec_info);
+
+ return result ? exec_info.hProcess : nullptr;
}
// Run command as non-elevated user, returns true if succeeded, puts the process id into returnPid if returnPid != NULL
diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h
index ed60bc1a37..471cefe480 100644
--- a/src/common/utils/gpo.h
+++ b/src/common/utils/gpo.h
@@ -30,6 +30,7 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_CMD_NOT_FOUND = L"ConfigureEnabledUtilityCmdNotFound";
const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker";
const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock";
+ const std::wstring POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH = L"ConfigureEnabledUtilityLightSwitch";
const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones";
const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith";
const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview";
@@ -295,6 +296,11 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK);
}
+ inline gpo_rule_configured_t getConfiguredLightSwitchEnabledValue()
+ {
+ return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_LIGHT_SWITCH);
+ }
+
inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_FANCYZONES);
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/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx
index 685eeaf350..07d4f44bde 100644
--- a/src/gpo/assets/PowerToys.admx
+++ b/src/gpo/assets/PowerToys.admx
@@ -137,6 +137,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml
index 7fe996abcc..2703358bb0 100644
--- a/src/gpo/assets/en-US/PowerToys.adml
+++ b/src/gpo/assets/en-US/PowerToys.adml
@@ -245,6 +245,7 @@ If you don't configure this policy, the user will be able to control the setting
Command Not Found: Configure enabled state
CmdPal: Configure enabled state
Crop And Lock: Configure enabled state
+ Light Switch: Configure enabled state
Environment Variables: Configure enabled state
FancyZones: Configure enabled state
File Locksmith: Configure enabled state
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml
index 1ffabf92a0..da79c36a11 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml
@@ -12,156 +12,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesMainPage.xaml b/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesMainPage.xaml
index de06561b50..f2628cf375 100644
--- a/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesMainPage.xaml
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesMainPage.xaml
@@ -21,157 +21,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
12
diff --git a/src/modules/Hosts/HostsUILib/HostsMainPage.xaml b/src/modules/Hosts/HostsUILib/HostsMainPage.xaml
index 762b4264c9..77b71ef5f1 100644
--- a/src/modules/Hosts/HostsUILib/HostsMainPage.xaml
+++ b/src/modules/Hosts/HostsUILib/HostsMainPage.xaml
@@ -27,160 +27,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+#include "resource.h"
+#include "../../../common/version/version.h"
+
+1 VERSIONINFO
+FILEVERSION FILE_VERSION
+PRODUCTVERSION PRODUCT_VERSION
+FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+#ifdef _DEBUG
+FILEFLAGS VS_FF_DEBUG
+#else
+FILEFLAGS 0x0L
+#endif
+FILEOS VOS_NT_WINDOWS32
+FILETYPE VFT_DLL
+FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
+ BEGIN
+ VALUE "CompanyName", COMPANY_NAME
+ VALUE "FileDescription", FILE_DESCRIPTION
+ VALUE "FileVersion", FILE_VERSION_STRING
+ VALUE "InternalName", INTERNAL_NAME
+ VALUE "LegalCopyright", COPYRIGHT_NOTE
+ VALUE "OriginalFilename", ORIGINAL_FILENAME
+ VALUE "ProductName", PRODUCT_NAME
+ VALUE "ProductVersion", PRODUCT_VERSION_STRING
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
+ END
+END
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj
new file mode 100644
index 0000000000..261cfab1e6
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj
@@ -0,0 +1,225 @@
+
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+ Debug
+ ARM64
+
+
+ Release
+ ARM64
+
+
+
+ 15.0
+ {38177d56-6ad1-4adf-88c9-2843a7932166}
+ Win32Proj
+ LightSwitchModuleInterface
+ 10.0
+ LightSwitchModuleInterface
+ PowerToys.LightSwitchModuleInterface
+
+
+
+ DynamicLibrary
+ true
+ v142
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v142
+ true
+ Unicode
+
+
+ DynamicLibrary
+ true
+ v142
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v142
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\$(Platform)\$(Configuration)\
+
+
+ true
+
+
+ false
+
+
+
+ Use
+ Level3
+ Disabled
+ true
+ _DEBUG;EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ pch.h
+ MultiThreadedDebug
+ stdcpplatest
+
+
+ Windows
+ true
+ $(OutDir)$(TargetName)$(TargetExt)
+
+
+
+
+ Use
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ NDEBUG;EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ pch.h
+ MultiThreaded
+ stdcpplatest
+
+
+ Windows
+ true
+ true
+ true
+ $(OutDir)$(TargetName)$(TargetExt)
+
+
+
+
+ Use
+ Level3
+ Disabled
+ true
+ _DEBUG;EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ pch.h
+ MultiThreadedDebug
+ stdcpplatest
+ Use
+ pch.h
+
+
+ Windows
+ true
+ $(OutDir)$(TargetName)$(TargetExt)
+
+
+
+
+ Use
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ NDEBUG;EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ pch.h
+ MultiThreaded
+ stdcpplatest
+
+
+ Windows
+ true
+ true
+ true
+ $(OutDir)$(TargetName)$(TargetExt)
+
+
+
+
+ ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+ $(CoreLibraryDependencies);%(AdditionalDependencies);advapi32.lib
+
+
+
+
+
+
+
+
+
+
+
+ Create
+ Create
+ Create
+ Create
+ pch.h
+ pch.h
+ pch.h
+ pch.h
+
+
+
+
+
+
+
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {4aed67b6-55fd-486f-b917-e543dee2cb3c}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj.filters b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj.filters
new file mode 100644
index 0000000000..45352efe4b
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/LightSwitchModuleInterface.vcxproj.filters
@@ -0,0 +1,50 @@
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ {bbf22ac8-46f8-4206-b44b-9c3897e99ce5}
+
+
+ {530ed784-9a70-46a0-8fb6-20d5dee4f7d3}
+
+
+ {da1cb871-86d3-414c-adf5-a7e9f2077d2f}
+
+
+
+
+ Resource Files
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/ThemeHelper.cpp b/src/modules/LightSwitch/LightSwitchModuleInterface/ThemeHelper.cpp
new file mode 100644
index 0000000000..dff2a67669
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/ThemeHelper.cpp
@@ -0,0 +1,81 @@
+#include "pch.h"
+#include
+#include "ThemeHelper.h"
+
+// Controls changing the themes.
+
+void SetAppsTheme(bool mode)
+{
+ HKEY hKey;
+ if (RegOpenKeyEx(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ 0,
+ KEY_SET_VALUE,
+ &hKey) == ERROR_SUCCESS)
+ {
+ DWORD value = mode;
+ RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast(&value), sizeof(value));
+ RegCloseKey(hKey);
+
+ SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
+
+ SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
+ }
+}
+
+void SetSystemTheme(bool mode)
+{
+ HKEY hKey;
+ if (RegOpenKeyEx(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ 0,
+ KEY_SET_VALUE,
+ &hKey) == ERROR_SUCCESS)
+ {
+ DWORD value = mode;
+ RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast(&value), sizeof(value));
+ RegCloseKey(hKey);
+
+ SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
+
+ SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
+ }
+}
+
+bool GetCurrentSystemTheme()
+{
+ HKEY hKey;
+ DWORD value = 1; // default = light
+ DWORD size = sizeof(value);
+
+ if (RegOpenKeyEx(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ 0,
+ KEY_READ,
+ &hKey) == ERROR_SUCCESS)
+ {
+ RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast(&value), &size);
+ RegCloseKey(hKey);
+ }
+
+ return value == 1; // true = light, false = dark
+}
+
+bool GetCurrentAppsTheme()
+{
+ HKEY hKey;
+ DWORD value = 1;
+ DWORD size = sizeof(value);
+
+ if (RegOpenKeyEx(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ 0,
+ KEY_READ,
+ &hKey) == ERROR_SUCCESS)
+ {
+ RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast(&value), &size);
+ RegCloseKey(hKey);
+ }
+
+ return value == 1; // true = light, false = dark
+}
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/ThemeHelper.h b/src/modules/LightSwitch/LightSwitchModuleInterface/ThemeHelper.h
new file mode 100644
index 0000000000..5985fd95c8
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/ThemeHelper.h
@@ -0,0 +1,5 @@
+#pragma once
+void SetSystemTheme(bool dark);
+void SetAppsTheme(bool dark);
+bool GetCurrentSystemTheme();
+bool GetCurrentAppsTheme();
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp b/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp
new file mode 100644
index 0000000000..be3afb170d
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/dllmain.cpp
@@ -0,0 +1,570 @@
+#include "pch.h"
+#include
+#include "trace.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include "ThemeHelper.h"
+
+extern "C" IMAGE_DOS_HEADER __ImageBase;
+
+namespace
+{
+ const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
+ const wchar_t JSON_KEY_WIN[] = L"win";
+ const wchar_t JSON_KEY_ALT[] = L"alt";
+ const wchar_t JSON_KEY_CTRL[] = L"ctrl";
+ const wchar_t JSON_KEY_SHIFT[] = L"shift";
+ const wchar_t JSON_KEY_CODE[] = L"code";
+ const wchar_t JSON_KEY_TOGGLE_THEME_HOTKEY[] = L"toggle-theme-hotkey";
+ const wchar_t JSON_KEY_VALUE[] = L"value";
+}
+
+BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
+{
+ switch (ul_reason_for_call)
+ {
+ case DLL_PROCESS_ATTACH:
+ Trace::RegisterProvider();
+ break;
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ break;
+ case DLL_PROCESS_DETACH:
+ Trace::UnregisterProvider();
+ break;
+ }
+ return TRUE;
+}
+
+// The PowerToy name that will be shown in the settings.
+const static wchar_t* MODULE_NAME = L"LightSwitch";
+// Add a description that will we shown in the module settings page.
+const static wchar_t* MODULE_DESC = L"This is a module that allows you to control light/dark theming via set times, sun rise, or directly invoking the change.";
+
+enum class ScheduleMode
+{
+ FixedHours,
+ SunsetToSunrise,
+ // add more later
+};
+
+inline std::wstring ToString(ScheduleMode mode)
+{
+ switch (mode)
+ {
+ case ScheduleMode::SunsetToSunrise:
+ return L"SunsetToSunrise";
+ case ScheduleMode::FixedHours:
+ default:
+ return L"FixedHours";
+ }
+}
+
+inline ScheduleMode FromString(const std::wstring& str)
+{
+ if (str == L"SunsetToSunrise")
+ return ScheduleMode::SunsetToSunrise;
+ return ScheduleMode::FixedHours;
+}
+
+// These are the properties shown in the Settings page.
+struct ModuleSettings
+{
+ bool m_changeSystem = true;
+ bool m_changeApps = true;
+ ScheduleMode m_scheduleMode = ScheduleMode::FixedHours;
+ int m_lightTime = 480;
+ int m_darkTime = 1200;
+ int m_sunrise_offset = 0;
+ int m_sunset_offset = 0;
+ std::wstring m_latitude = L"0.0";
+ std::wstring m_longitude = L"0.0";
+} g_settings;
+
+class LightSwitchInterface : public PowertoyModuleIface
+{
+private:
+ bool m_enabled = false;
+
+ HANDLE m_process{ nullptr };
+ HANDLE m_force_light_event_handle;
+ HANDLE m_force_dark_event_handle;
+ HANDLE m_manual_override_event_handle;
+
+ static const constexpr int NUM_DEFAULT_HOTKEYS = 4;
+
+ Hotkey m_toggle_theme_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'D' };
+
+ void init_settings();
+
+public:
+ LightSwitchInterface()
+ {
+ LoggerHelpers::init_logger(L"LightSwitch", L"ModuleInterface", LogSettings::lightSwitchLoggerName);
+
+ m_force_light_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_LIGHT");
+ m_force_dark_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_FORCE_DARK");
+ m_manual_override_event_handle = CreateDefaultEvent(L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
+
+ init_settings();
+ };
+
+ virtual const wchar_t* get_key() override
+ {
+ return L"LightSwitch";
+ }
+
+ // Destroy the powertoy and free memory
+ virtual void destroy() override
+ {
+ delete this;
+ }
+
+ // Return the display name of the powertoy, this will be cached by the runner
+ virtual const wchar_t* get_name() override
+ {
+ return MODULE_NAME;
+ }
+
+ // Return the configured status for the gpo policy for the module
+ virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
+ {
+ return powertoys_gpo::getConfiguredLightSwitchEnabledValue();
+ }
+
+ // Return JSON with the configuration options.
+ virtual bool get_config(wchar_t* buffer, int* buffer_size) override
+ {
+ HINSTANCE hinstance = reinterpret_cast(&__ImageBase);
+
+ // Create a Settings object with your module name
+ PowerToysSettings::Settings settings(hinstance, get_name());
+ settings.set_description(MODULE_DESC);
+ settings.set_overview_link(L"https://aka.ms/powertoys");
+
+ // Boolean toggles
+ settings.add_bool_toggle(
+ L"changeSystem",
+ L"Change System Theme",
+ g_settings.m_changeSystem);
+
+ settings.add_bool_toggle(
+ L"changeApps",
+ L"Change Apps Theme",
+ g_settings.m_changeApps);
+
+ settings.add_choice_group(
+ L"scheduleMode",
+ L"Theme schedule mode",
+ ToString(g_settings.m_scheduleMode),
+ { { L"FixedHours", L"Set hours manually" },
+ { L"SunsetToSunrise", L"Use sunrise/sunset times" } });
+
+ // Integer spinners
+ settings.add_int_spinner(
+ L"lightTime",
+ L"Time to switch to light theme (minutes after midnight).",
+ g_settings.m_lightTime,
+ 0,
+ 1439,
+ 1);
+
+ settings.add_int_spinner(
+ L"darkTime",
+ L"Time to switch to dark theme (minutes after midnight).",
+ g_settings.m_darkTime,
+ 0,
+ 1439,
+ 1);
+
+ settings.add_int_spinner(
+ L"sunrise_offset",
+ L"Time to offset turning on your light theme.",
+ g_settings.m_sunrise_offset,
+ 0,
+ 1439,
+ 1);
+
+ settings.add_int_spinner(
+ L"sunset_offset",
+ L"Time to offset turning on your dark theme.",
+ g_settings.m_sunset_offset,
+ 0,
+ 1439,
+ 1);
+
+ // Strings for latitude and longitude
+ settings.add_string(
+ L"latitude",
+ L"Your latitude in decimal degrees (e.g. 39.95).",
+ g_settings.m_latitude);
+
+ settings.add_string(
+ L"longitude",
+ L"Your longitude in decimal degrees (e.g. -75.16).",
+ g_settings.m_longitude);
+
+ // One-shot actions (buttons)
+ settings.add_custom_action(
+ L"forceLight",
+ L"Switch immediately to light theme",
+ L"Force Light",
+ L"{}");
+
+ settings.add_custom_action(
+ L"forceDark",
+ L"Switch immediately to dark theme",
+ L"Force Dark",
+ L"{}");
+
+ // Hotkeys
+ PowerToysSettings::HotkeyObject dm_hk = PowerToysSettings::HotkeyObject::from_settings(
+ m_toggle_theme_hotkey.win,
+ m_toggle_theme_hotkey.ctrl,
+ m_toggle_theme_hotkey.alt,
+ m_toggle_theme_hotkey.shift,
+ m_toggle_theme_hotkey.key);
+
+ settings.add_hotkey(
+ L"toggle-theme-hotkey",
+ L"Shortcut to toggle theme immediately",
+ dm_hk);
+
+ // Serialize to buffer for the PowerToys runner
+ return settings.serialize_to_buffer(buffer, buffer_size);
+ }
+
+ // Signal from the Settings editor to call a custom action.
+ // This can be used to spawn more complex editors.
+ void call_custom_action(const wchar_t* action) override
+ {
+ try
+ {
+ auto action_object = PowerToysSettings::CustomActionObject::from_json_string(action);
+
+ if (action_object.get_name() == L"forceLight")
+ {
+ Logger::info(L"[Light Switch] Custom action triggered: Force Light");
+ SetSystemTheme(true);
+ SetAppsTheme(true);
+ }
+ else if (action_object.get_name() == L"forceDark")
+ {
+ Logger::info(L"[Light Switch] Custom action triggered: Force Dark");
+ SetSystemTheme(false);
+ SetAppsTheme(false);
+ }
+ }
+ catch (...)
+ {
+ Logger::error(L"[Light Switch] Invalid custom action JSON");
+ }
+ }
+
+ // Called by the runner to pass the updated settings values as a serialized JSON.
+ virtual void set_config(const wchar_t* config) override
+ {
+ try
+ {
+ auto values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
+
+ parse_hotkey(values);
+
+ if (auto v = values.get_bool_value(L"changeSystem"))
+ {
+ g_settings.m_changeSystem = *v;
+ }
+
+ if (auto v = values.get_bool_value(L"changeApps"))
+ {
+ g_settings.m_changeApps = *v;
+ }
+
+ if (auto v = values.get_string_value(L"scheduleMode"))
+ {
+ g_settings.m_scheduleMode = FromString(*v);
+ }
+
+ if (auto v = values.get_int_value(L"lightTime"))
+ {
+ g_settings.m_lightTime = *v;
+ }
+
+ if (auto v = values.get_int_value(L"darkTime"))
+ {
+ g_settings.m_darkTime = *v;
+ }
+
+ if (auto v = values.get_int_value(L"sunrise_offset"))
+ {
+ g_settings.m_sunrise_offset = *v;
+ }
+
+ if (auto v = values.get_int_value(L"m_sunset_offset"))
+ {
+ g_settings.m_sunset_offset = *v;
+ }
+
+ if (auto v = values.get_string_value(L"latitude"))
+ {
+ g_settings.m_latitude = *v;
+ }
+ if (auto v = values.get_string_value(L"longitude"))
+ {
+ g_settings.m_longitude = *v;
+ }
+
+ values.save_to_settings_file();
+ }
+ catch (const std::exception&)
+ {
+ Logger::error("[Light Switch] set_config: Failed to parse or apply config.");
+ }
+ }
+
+ virtual void enable()
+ {
+ m_enabled = true;
+ Logger::info(L"Enabling Light Switch module...");
+
+ unsigned long powertoys_pid = GetCurrentProcessId();
+ std::wstring args = L"--pid " + std::to_wstring(powertoys_pid);
+ std::wstring exe_name = L"LightSwitchService\\PowerToys.LightSwitchService.exe";
+
+ std::wstring resolved_path(MAX_PATH, L'\0');
+ DWORD result = SearchPathW(
+ nullptr,
+ exe_name.c_str(),
+ nullptr,
+ static_cast(resolved_path.size()),
+ resolved_path.data(),
+ nullptr);
+
+ if (result == 0 || result >= resolved_path.size())
+ {
+ Logger::error(
+ L"Failed to locate Light Switch executable named '{}' at location '{}'",
+ exe_name,
+ resolved_path.c_str());
+ return;
+ }
+
+ resolved_path.resize(result);
+ Logger::debug(L"Resolved executable path: {}", resolved_path);
+
+ std::wstring command_line = L"\"" + resolved_path + L"\" " + args;
+
+ STARTUPINFO si = { sizeof(si) };
+ PROCESS_INFORMATION pi;
+
+ if (!CreateProcessW(
+ resolved_path.c_str(),
+ command_line.data(),
+ nullptr,
+ nullptr,
+ TRUE,
+ 0,
+ nullptr,
+ nullptr,
+ &si,
+ &pi))
+ {
+ Logger::error(L"Failed to launch Light Switch process. {}", get_last_error_or_default(GetLastError()));
+ return;
+ }
+
+ Logger::info(L"Light Switch process launched successfully (PID: {}).", pi.dwProcessId);
+ m_process = pi.hProcess;
+ CloseHandle(pi.hThread);
+ }
+
+ // Disable the powertoy
+ virtual void disable()
+ {
+ Logger::info("Light Switch disabling");
+ m_enabled = false;
+
+ if (m_process)
+ {
+ constexpr DWORD timeout_ms = 1500;
+ DWORD result = WaitForSingleObject(m_process, timeout_ms);
+
+ if (result == WAIT_TIMEOUT)
+ {
+ Logger::warn("Light Switch: Process didn't exit in time. Forcing termination.");
+ TerminateProcess(m_process, 0);
+ }
+
+ CloseHandle(m_manual_override_event_handle);
+ m_manual_override_event_handle = nullptr;
+
+ CloseHandle(m_process);
+ m_process = nullptr;
+ }
+ }
+
+ // Returns if the powertoys is enabled
+ virtual bool is_enabled() override
+ {
+ return m_enabled;
+ }
+
+ void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
+ {
+ auto settingsObject = settings.get_raw_json();
+ if (settingsObject.GetView().Size())
+ {
+ try
+ {
+ Hotkey _temp_toggle_theme;
+ auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_TOGGLE_THEME_HOTKEY).GetNamedObject(JSON_KEY_VALUE);
+ _temp_toggle_theme.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
+ _temp_toggle_theme.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
+ _temp_toggle_theme.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
+ _temp_toggle_theme.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
+ _temp_toggle_theme.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
+ m_toggle_theme_hotkey = _temp_toggle_theme;
+ }
+ catch (...)
+ {
+ Logger::error("Failed to initialize Light Switch force dark mode shortcut from settings. Value will keep unchanged.");
+ }
+ }
+ else
+ {
+ Logger::info("Light Switch settings are empty");
+ }
+ }
+
+ virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
+ {
+ if (hotkeys && buffer_size >= 1)
+ {
+ hotkeys[0] = m_toggle_theme_hotkey;
+ }
+ return 1;
+ }
+
+ virtual bool on_hotkey(size_t hotkeyId) override
+ {
+ if (m_enabled)
+ {
+ Logger::trace(L"Light Switch hotkey pressed");
+ if (!is_process_running())
+ {
+ enable();
+ }
+ else if (hotkeyId == 0)
+ {
+ // get current will return true if in light mode; otherwise false
+ Logger::info(L"[Light Switch] Hotkey triggered: Toggle Theme");
+ if (g_settings.m_changeSystem)
+ {
+ SetSystemTheme(!GetCurrentSystemTheme());
+ }
+ if (g_settings.m_changeApps)
+ {
+ SetAppsTheme(!GetCurrentAppsTheme());
+ }
+
+ if (m_manual_override_event_handle)
+ {
+ SetEvent(m_manual_override_event_handle);
+ Logger::debug(L"[Light Switch] Manual override event set");
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ bool is_process_running()
+ {
+ return WaitForSingleObject(m_process, 0) == WAIT_TIMEOUT;
+ }
+};
+
+std::wstring utf8_to_wstring(const std::string& str)
+{
+ if (str.empty())
+ return std::wstring();
+
+ int size_needed = MultiByteToWideChar(
+ CP_UTF8,
+ 0,
+ str.c_str(),
+ static_cast(str.size()),
+ nullptr,
+ 0);
+
+ std::wstring wstr(size_needed, 0);
+
+ MultiByteToWideChar(
+ CP_UTF8,
+ 0,
+ str.c_str(),
+ static_cast(str.size()),
+ &wstr[0],
+ size_needed);
+
+ return wstr;
+}
+
+// Load the settings file.
+void LightSwitchInterface::init_settings()
+{
+ Logger::info(L"[Light Switch] init_settings: starting to load settings for module");
+
+ try
+ {
+ PowerToysSettings::PowerToyValues settings =
+ PowerToysSettings::PowerToyValues::load_from_settings_file(get_name());
+
+ parse_hotkey(settings);
+
+ if (auto v = settings.get_bool_value(L"changeSystem"))
+ g_settings.m_changeSystem = *v;
+ if (auto v = settings.get_bool_value(L"changeApps"))
+ g_settings.m_changeApps = *v;
+ if (auto v = settings.get_string_value(L"scheduleMode"))
+ g_settings.m_scheduleMode = FromString(*v);
+ if (auto v = settings.get_int_value(L"lightTime"))
+ g_settings.m_lightTime = *v;
+ if (auto v = settings.get_int_value(L"darkTime"))
+ g_settings.m_darkTime = *v;
+ if (auto v = settings.get_int_value(L"sunrise_offset"))
+ g_settings.m_sunrise_offset = *v;
+ if (auto v = settings.get_int_value(L"sunset_offset"))
+ g_settings.m_sunset_offset = *v;
+ if (auto v = settings.get_string_value(L"latitude"))
+ g_settings.m_latitude = *v;
+ if (auto v = settings.get_string_value(L"longitude"))
+ g_settings.m_longitude = *v;
+
+ Logger::info(L"[Light Switch] init_settings: loaded successfully");
+ }
+ catch (const winrt::hresult_error& e)
+ {
+ Logger::error(L"[Light Switch] init_settings: hresult_error 0x{:08X} - {}", e.code(), e.message().c_str());
+ }
+ catch (const std::exception& e)
+ {
+ std::wstring whatStr = utf8_to_wstring(e.what());
+ Logger::error(L"[Light Switch] init_settings: std::exception - {}", whatStr);
+ }
+ catch (...)
+ {
+ Logger::error(L"[Light Switch] init_settings: unknown exception while loading settings");
+ }
+}
+
+extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
+{
+ return new LightSwitchInterface();
+}
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/pch.cpp b/src/modules/LightSwitch/LightSwitchModuleInterface/pch.cpp
new file mode 100644
index 0000000000..a83d3bb2cc
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/pch.cpp
@@ -0,0 +1,2 @@
+#include "pch.h"
+#pragma comment(lib, "windowsapp")
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/pch.h b/src/modules/LightSwitch/LightSwitchModuleInterface/pch.h
new file mode 100644
index 0000000000..39f8f4ac84
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/pch.h
@@ -0,0 +1,14 @@
+#pragma once
+#define WIN32_LEAN_AND_MEAN
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/resource.h b/src/modules/LightSwitch/LightSwitchModuleInterface/resource.h
new file mode 100644
index 0000000000..548cde844b
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/resource.h
@@ -0,0 +1,13 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by CalculatorEngineCommon.rc
+
+//////////////////////////////
+// Non-localizable
+
+#define FILE_DESCRIPTION "Light Switch Module"
+#define INTERNAL_NAME "Light Switch"
+#define ORIGINAL_FILENAME "PowerToys.LightSwitchModuleInterface.dll"
+
+// Non-localizable
+//////////////////////////////
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/trace.cpp b/src/modules/LightSwitch/LightSwitchModuleInterface/trace.cpp
new file mode 100644
index 0000000000..57fa1921f7
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/trace.cpp
@@ -0,0 +1,30 @@
+#include "pch.h"
+#include "trace.h"
+#include
+
+TRACELOGGING_DEFINE_PROVIDER(
+ g_hProvider,
+ "Microsoft.PowerToys",
+ // {38e8889b-9731-53f5-e901-e8a7c1753074}
+ (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
+ TraceLoggingOptionProjectTelemetry());
+
+void Trace::RegisterProvider()
+{
+ TraceLoggingRegister(g_hProvider);
+}
+
+void Trace::UnregisterProvider()
+{
+ TraceLoggingUnregister(g_hProvider);
+}
+
+void Trace::MyEvent()
+{
+ TraceLoggingWrite(
+ g_hProvider,
+ "PowerToyName_MyEvent",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
+}
diff --git a/src/modules/LightSwitch/LightSwitchModuleInterface/trace.h b/src/modules/LightSwitch/LightSwitchModuleInterface/trace.h
new file mode 100644
index 0000000000..55cdedb2ee
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchModuleInterface/trace.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include
+#include
+#include
+
+TRACELOGGING_DECLARE_PROVIDER(g_hProvider);
+
+class Trace
+{
+public:
+ static void RegisterProvider();
+ static void UnregisterProvider();
+ static void MyEvent();
+};
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitch.ico b/src/modules/LightSwitch/LightSwitchService/LightSwitch.ico
new file mode 100644
index 0000000000..ee1be50010
Binary files /dev/null and b/src/modules/LightSwitch/LightSwitchService/LightSwitch.ico differ
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp
new file mode 100644
index 0000000000..168ee092e7
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp
@@ -0,0 +1,295 @@
+#include
+#include
+#include "ThemeScheduler.h"
+#include "ThemeHelper.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+SERVICE_STATUS g_ServiceStatus = {};
+SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
+HANDLE g_ServiceStopEvent = nullptr;
+static int g_lastUpdatedDay = -1;
+
+VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
+VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
+DWORD WINAPI ServiceWorkerThread(LPVOID lpParam);
+
+// Entry point for the executable
+int _tmain(int argc, TCHAR* argv[])
+{
+ DWORD parentPid = 0;
+ bool debug = false;
+ for (int i = 1; i < argc; ++i)
+ {
+ if (_tcscmp(argv[i], _T("--debug")) == 0)
+ debug = true;
+ else if (_tcscmp(argv[i], _T("--pid")) == 0 && i + 1 < argc)
+ parentPid = _tstoi(argv[++i]);
+ }
+
+ // Try to connect to SCM
+ wchar_t serviceName[] = L"LightSwitchService";
+ SERVICE_TABLE_ENTRYW table[] = { { serviceName, ServiceMain }, { nullptr, nullptr } };
+
+ if (!StartServiceCtrlDispatcherW(table))
+ {
+ DWORD err = GetLastError();
+ if (err == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) // not launched by SCM
+ {
+ g_ServiceStopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
+ HANDLE hThread = CreateThread(
+ nullptr, 0, ServiceWorkerThread, reinterpret_cast(static_cast(parentPid)), 0, nullptr);
+
+ // Wait so the process stays alive
+ WaitForSingleObject(hThread, INFINITE);
+ CloseHandle(hThread);
+ CloseHandle(g_ServiceStopEvent);
+ return 0;
+ }
+ return static_cast(err);
+ }
+
+ return 0;
+}
+
+// Called when the service is launched by Windows
+VOID WINAPI ServiceMain(DWORD, LPTSTR*)
+{
+ g_StatusHandle = RegisterServiceCtrlHandler(_T("LightSwitchService"), ServiceCtrlHandler);
+ if (!g_StatusHandle)
+ return;
+
+ g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
+ g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
+ g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
+ SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
+
+ g_ServiceStopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
+ if (!g_ServiceStopEvent)
+ {
+ g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
+ g_ServiceStatus.dwWin32ExitCode = GetLastError();
+ SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
+ return;
+ }
+
+ SECURITY_ATTRIBUTES sa{ sizeof(sa) };
+ sa.bInheritHandle = FALSE;
+ sa.lpSecurityDescriptor = nullptr;
+
+ g_ServiceStatus.dwCurrentState = SERVICE_RUNNING;
+ SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
+
+ HANDLE hThread = CreateThread(nullptr, 0, ServiceWorkerThread, nullptr, 0, nullptr);
+ WaitForSingleObject(hThread, INFINITE);
+ CloseHandle(hThread);
+
+ CloseHandle(g_ServiceStopEvent);
+ g_ServiceStatus.dwCurrentState = SERVICE_STOPPED;
+ g_ServiceStatus.dwWin32ExitCode = 0;
+ SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
+}
+
+VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl)
+{
+ switch (dwCtrl)
+ {
+ case SERVICE_CONTROL_STOP:
+ if (g_ServiceStatus.dwCurrentState != SERVICE_RUNNING)
+ break;
+
+ g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING;
+ SetServiceStatus(g_StatusHandle, &g_ServiceStatus);
+
+ // Signal the service to stop
+ SetEvent(g_ServiceStopEvent);
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void update_sun_times(auto& settings)
+{
+ double latitude = std::stod(settings.latitude);
+ double longitude = std::stod(settings.longitude);
+
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+
+ SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay);
+
+ int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
+ int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
+
+ auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
+ values.add_property(L"lightTime", newLightTime);
+ values.add_property(L"darkTime", newDarkTime);
+ values.save_to_settings_file();
+
+ OutputDebugString(L"[LightSwitchService] Updated sun times and saved to config.\n");
+}
+
+DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
+{
+ DWORD parentPid = static_cast(reinterpret_cast(lpParam));
+ HANDLE hParent = nullptr;
+ if (parentPid)
+ hParent = OpenProcess(SYNCHRONIZE, FALSE, parentPid);
+
+ OutputDebugString(L"[LightSwitchService] Worker thread starting...\n");
+
+ // Initialize settings system
+ LightSwitchSettings::instance().InitFileWatcher();
+
+ // Open the manual override event created by the module interface
+ HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
+
+ auto applyTheme = [](int nowMinutes, int lightMinutes, int darkMinutes, const auto& settings) {
+ bool isLightActive = false;
+
+ if (lightMinutes < darkMinutes)
+ {
+ // Normal case: sunrise < sunset
+ isLightActive = (nowMinutes >= lightMinutes && nowMinutes < darkMinutes);
+ }
+ else
+ {
+ // Wraparound case: e.g. light at 21:00, dark at 06:00
+ isLightActive = (nowMinutes >= lightMinutes || nowMinutes < darkMinutes);
+ }
+
+ bool isSystemCurrentlyLight = GetCurrentSystemTheme();
+ bool isAppsCurrentlyLight = GetCurrentAppsTheme();
+
+ if (isLightActive)
+ {
+ if (settings.changeSystem && !isSystemCurrentlyLight)
+ SetSystemTheme(true);
+ if (settings.changeApps && !isAppsCurrentlyLight)
+ SetAppsTheme(true);
+ }
+ else
+ {
+ if (settings.changeSystem && isSystemCurrentlyLight)
+ SetSystemTheme(false);
+ if (settings.changeApps && isAppsCurrentlyLight)
+ SetAppsTheme(false);
+ }
+ };
+
+ // --- At service start: immediately honor the schedule ---
+ {
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ int nowMinutes = st.wHour * 60 + st.wMinute;
+
+ LightSwitchSettings::instance().LoadSettings();
+ const auto& settings = LightSwitchSettings::instance().settings();
+
+ applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
+ }
+
+ // --- Main loop: wakes once per minute or stop/parent death ---
+ for (;;)
+ {
+ HANDLE waits[2] = { g_ServiceStopEvent, hParent };
+ DWORD count = hParent ? 2 : 1;
+
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ int nowMinutes = st.wHour * 60 + st.wMinute;
+
+ LightSwitchSettings::instance().LoadSettings();
+ const auto& settings = LightSwitchSettings::instance().settings();
+
+ // Refresh suntimes at day boundary
+ if (g_lastUpdatedDay != st.wDay)
+ {
+ update_sun_times(settings);
+ g_lastUpdatedDay = st.wDay;
+
+ OutputDebugString(L"[LightSwitchService] Recalculated sun times at new day boundary.\n");
+ }
+
+ wchar_t msg[160];
+ swprintf_s(msg,
+ L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d\n",
+ st.wHour,
+ st.wMinute,
+ settings.lightTime / 60,
+ settings.lightTime % 60,
+ settings.darkTime / 60,
+ settings.darkTime % 60);
+ OutputDebugString(msg);
+
+ // --- Manual override check ---
+ bool manualOverrideActive = false;
+ if (hManualOverride)
+ {
+ manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
+ }
+
+ if (manualOverrideActive)
+ {
+ // Did we hit a scheduled boundary? (reset override at boundary)
+ if (nowMinutes == (settings.lightTime + settings.sunrise_offset) % 1440 ||
+ nowMinutes == (settings.darkTime + settings.sunset_offset) % 1440)
+ {
+ ResetEvent(hManualOverride);
+ OutputDebugString(L"[LightSwitchService] Manual override cleared at boundary\n");
+ }
+ else
+ {
+ OutputDebugString(L"[LightSwitchService] Skipping schedule due to manual override\n");
+ goto sleep_until_next_minute;
+ }
+ }
+
+ // Apply theme logic (only runs if no manual override or override just cleared)
+ applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
+
+ sleep_until_next_minute:
+ GetLocalTime(&st);
+ int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds;
+ if (msToNextMinute < 50)
+ msToNextMinute = 50;
+
+ DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
+ if (wait == WAIT_OBJECT_0) // stop event
+ break;
+ if (hParent && wait == WAIT_OBJECT_0 + 1) // parent exited
+ break;
+ }
+
+ if (hManualOverride)
+ CloseHandle(hManualOverride);
+ if (hParent)
+ CloseHandle(hParent);
+
+ return 0;
+}
+
+int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
+{
+ if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
+ {
+ wchar_t msg[160];
+ swprintf_s(
+ msg,
+ L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.\n");
+ OutputDebugString(msg);
+ return 0;
+ }
+
+ int argc = 0;
+ LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
+ int rc = _tmain(argc, argv); // reuse your existing logic
+ LocalFree(argv);
+ return rc;
+}
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.rc b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.rc
new file mode 100644
index 0000000000..82dbcb263a
Binary files /dev/null and b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.rc differ
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj
new file mode 100644
index 0000000000..2151d0b5b6
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj
@@ -0,0 +1,219 @@
+
+
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+ Debug
+ ARM64
+
+
+ Release
+ ARM64
+
+
+
+ 17.0
+ Win32Proj
+ {08e71c67-6a7e-4ca1-b04e-2fb336410bac}
+ LightSwitchService
+ 10.0.26100.0
+ LightSwitchService
+
+
+
+ Application
+ true
+ v143
+ Unicode
+
+
+ Application
+ false
+ v143
+ true
+ Unicode
+
+
+ Application
+ true
+ v143
+ Unicode
+
+
+ Application
+ false
+ v143
+ true
+ Unicode
+
+
+ Application
+ true
+ v143
+ Unicode
+
+
+ Application
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)\
+ PowerToys.LightSwitchService
+
+
+
+ Level3
+ true
+ _DEBUG;%(PreprocessorDefinitions)
+ true
+
+
+ Windows
+ true
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;%(PreprocessorDefinitions)
+ true
+
+
+ Windows
+ true
+
+
+
+
+ Level3
+ true
+ %(PreprocessorDefinitions)
+ true
+ NotUsing
+
+ ./../;
+ ..\..\..\common\Telemetry;
+ ..\..\..\common;
+ ..\..\..\;
+ ..\..\..\..\deps\spdlog\include;
+ ./;
+ %(AdditionalIncludeDirectories)
+
+
+
+ Windows
+ true
+ Advapi32.lib;%(AdditionalDependencies)
+
+
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+ {1d5be09d-78c0-4fd7-af00-ae7c1af7c525}
+
+
+ {8f021b46-362b-485c-bfba-ccf83e820cbd}
+
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;%(PreprocessorDefinitions)
+ true
+
+
+ Windows
+ true
+
+
+
+
+ NotUsing
+
+
+
+
+ NotUsing
+
+
+ NotUsing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters
new file mode 100644
index 0000000000..a244dfc075
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters
@@ -0,0 +1,72 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
new file mode 100644
index 0000000000..5bd5a1fe92
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
@@ -0,0 +1,167 @@
+#include "LightSwitchSettings.h"
+#include
+#include
+#include "SettingsObserver.h"
+
+#include
+#include
+#include
+
+using namespace std;
+
+LightSwitchSettings& LightSwitchSettings::instance()
+{
+ static LightSwitchSettings inst;
+ return inst;
+}
+
+LightSwitchSettings::LightSwitchSettings()
+{
+ LoadSettings();
+}
+
+std::wstring LightSwitchSettings::GetSettingsFileName()
+{
+ return PTSettingsHelper::get_module_save_file_location(L"LightSwitch");
+}
+
+void LightSwitchSettings::InitFileWatcher()
+{
+ const std::wstring& settingsFileName = GetSettingsFileName();
+ m_settingsFileWatcher = std::make_unique(settingsFileName, [&]() {
+ PostMessageW(HWND_BROADCAST, WM_PRIV_SETTINGS_CHANGED, NULL, NULL);
+ });
+}
+
+void LightSwitchSettings::AddObserver(SettingsObserver& observer)
+{
+ m_observers.insert(&observer);
+}
+
+void LightSwitchSettings::RemoveObserver(SettingsObserver& observer)
+{
+ m_observers.erase(&observer);
+}
+
+void LightSwitchSettings::NotifyObservers(SettingId id) const
+{
+ for (auto observer : m_observers)
+ {
+ if (observer->WantsToBeNotified(id))
+ {
+ observer->SettingsUpdate(id);
+ }
+ }
+}
+
+void LightSwitchSettings::LoadSettings()
+{
+ try
+ {
+ PowerToysSettings::PowerToyValues values =
+ PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
+
+
+ if (const auto jsonVal = values.get_string_value(L"scheduleMode"))
+ {
+ auto val = *jsonVal;
+ auto newMode = FromString(val);
+ if (m_settings.scheduleMode != newMode)
+ {
+ m_settings.scheduleMode = newMode;
+ NotifyObservers(SettingId::ScheduleMode);
+ }
+ }
+
+ // Latitude
+ if (const auto jsonVal = values.get_string_value(L"latitude"))
+ {
+ auto val = *jsonVal;
+ if (m_settings.latitude != val)
+ {
+ m_settings.latitude = val;
+ NotifyObservers(SettingId::Latitude);
+ }
+ }
+
+ // Longitude
+ if (const auto jsonVal = values.get_string_value(L"longitude"))
+ {
+ auto val = *jsonVal;
+ if (m_settings.longitude != val)
+ {
+ m_settings.longitude = val;
+ NotifyObservers(SettingId::Longitude);
+ }
+ }
+
+ // LightTime
+ if (const auto jsonVal = values.get_int_value(L"lightTime"))
+ {
+ auto val = *jsonVal;
+ if (m_settings.lightTime != val)
+ {
+ m_settings.lightTime = val;
+ NotifyObservers(SettingId::LightTime);
+ }
+ }
+
+ // DarkTime
+ if (const auto jsonVal = values.get_int_value(L"darkTime"))
+ {
+ auto val = *jsonVal;
+ if (m_settings.darkTime != val)
+ {
+ m_settings.darkTime = val;
+ NotifyObservers(SettingId::DarkTime);
+ }
+ }
+
+ // Offset
+ if (const auto jsonVal = values.get_int_value(L"sunrise_offset"))
+ {
+ auto val = *jsonVal;
+ if (m_settings.sunrise_offset != val)
+ {
+ m_settings.sunrise_offset = val;
+ NotifyObservers(SettingId::Sunrise_Offset);
+ }
+ }
+
+ if (const auto jsonVal = values.get_int_value(L"sunset_offset"))
+ {
+ auto val = *jsonVal;
+ if (m_settings.sunset_offset != val)
+ {
+ m_settings.sunset_offset = val;
+ NotifyObservers(SettingId::Sunset_Offset);
+ }
+ }
+
+ // ChangeSystem
+ if (const auto jsonVal = values.get_bool_value(L"changeSystem"))
+ {
+ auto val = *jsonVal;
+ if (m_settings.changeSystem != val)
+ {
+ m_settings.changeSystem = val;
+ NotifyObservers(SettingId::ChangeSystem);
+ }
+ }
+
+ // ChangeApps
+ if (const auto jsonVal = values.get_bool_value(L"changeApps"))
+ {
+ auto val = *jsonVal;
+ if (m_settings.changeApps != val)
+ {
+ m_settings.changeApps = val;
+ NotifyObservers(SettingId::ChangeApps);
+ }
+ }
+ }
+ catch (...)
+ {
+ // Keeps defaults if load fails
+ }
+}
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h
new file mode 100644
index 0000000000..51f0988eda
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h
@@ -0,0 +1,88 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+class SettingsObserver;
+
+enum class ScheduleMode
+{
+ FixedHours,
+ SunsetToSunrise
+ // Add more in the future
+};
+
+inline std::wstring ToString(ScheduleMode mode)
+{
+ switch (mode)
+ {
+ case ScheduleMode::FixedHours:
+ return L"FixedHours";
+ case ScheduleMode::SunsetToSunrise:
+ return L"SunsetToSunrise";
+ default:
+ return L"FixedHours";
+ }
+}
+
+inline ScheduleMode FromString(const std::wstring& str)
+{
+ if (str == L"SunsetToSunrise")
+ return ScheduleMode::SunsetToSunrise;
+ else
+ return ScheduleMode::FixedHours;
+}
+
+struct LightSwitchConfig
+{
+ ScheduleMode scheduleMode = ScheduleMode::FixedHours;
+
+ std::wstring latitude = L"0.0";
+ std::wstring longitude = L"0.0";
+
+ // Stored as minutes since midnight
+ int lightTime = 8 * 60; // 08:00 default
+ int darkTime = 20 * 60; // 20:00 default
+
+ int sunrise_offset = 0;
+ int sunset_offset = 0;
+
+ bool changeSystem = false;
+ bool changeApps = false;
+};
+
+class LightSwitchSettings
+{
+public:
+ static LightSwitchSettings& instance();
+
+ static inline const LightSwitchConfig& settings()
+ {
+ return instance().m_settings;
+ }
+
+ void InitFileWatcher();
+ static std::wstring GetSettingsFileName();
+
+ void AddObserver(SettingsObserver& observer);
+ void RemoveObserver(SettingsObserver& observer);
+
+ void LoadSettings();
+
+private:
+ LightSwitchSettings();
+ ~LightSwitchSettings() = default;
+
+ LightSwitchConfig m_settings;
+ std::unique_ptr m_settingsFileWatcher;
+ std::unordered_set m_observers;
+
+ void NotifyObservers(SettingId id) const;
+};
diff --git a/src/modules/LightSwitch/LightSwitchService/SettingsConstants.cpp b/src/modules/LightSwitch/LightSwitchService/SettingsConstants.cpp
new file mode 100644
index 0000000000..534e55f5e3
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/SettingsConstants.cpp
@@ -0,0 +1 @@
+#include "SettingsConstants.h"
diff --git a/src/modules/LightSwitch/LightSwitchService/SettingsConstants.h b/src/modules/LightSwitch/LightSwitchService/SettingsConstants.h
new file mode 100644
index 0000000000..4872864eff
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/SettingsConstants.h
@@ -0,0 +1,14 @@
+#pragma once
+
+enum class SettingId
+{
+ ScheduleMode = 0,
+ Latitude,
+ Longitude,
+ LightTime,
+ DarkTime,
+ Sunrise_Offset,
+ Sunset_Offset,
+ ChangeSystem,
+ ChangeApps
+};
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h b/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h
new file mode 100644
index 0000000000..88d0194eef
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include
+#include "SettingsConstants.h"
+
+class LightSwitchSettings;
+
+class SettingsObserver
+{
+public:
+ SettingsObserver(std::unordered_set observedSettings) :
+ m_observedSettings(std::move(observedSettings))
+ {
+ LightSwitchSettings::instance().AddObserver(*this);
+ }
+
+ virtual ~SettingsObserver()
+ {
+ LightSwitchSettings::instance().RemoveObserver(*this);
+ }
+
+ // Override this in your class to respond to updates
+ virtual void SettingsUpdate(SettingId type) {}
+
+ bool WantsToBeNotified(SettingId type) const noexcept
+ {
+ return m_observedSettings.contains(type);
+ }
+
+protected:
+ std::unordered_set m_observedSettings;
+};
diff --git a/src/modules/LightSwitch/LightSwitchService/ThemeHelper.cpp b/src/modules/LightSwitch/LightSwitchService/ThemeHelper.cpp
new file mode 100644
index 0000000000..b0a57cf468
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/ThemeHelper.cpp
@@ -0,0 +1,81 @@
+#include
+#include "ThemeHelper.h"
+
+// Controls changing the themes.
+
+void SetAppsTheme(bool mode)
+{
+ HKEY hKey;
+ if (RegOpenKeyEx(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ 0,
+ KEY_SET_VALUE,
+ &hKey) == ERROR_SUCCESS)
+ {
+ DWORD value = mode;
+ RegSetValueEx(hKey, L"AppsUseLightTheme", 0, REG_DWORD, reinterpret_cast(&value), sizeof(value));
+ RegCloseKey(hKey);
+
+ SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
+
+ SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
+ }
+}
+
+void SetSystemTheme(bool mode)
+{
+ HKEY hKey;
+ if (RegOpenKeyEx(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ 0,
+ KEY_SET_VALUE,
+ &hKey) == ERROR_SUCCESS)
+ {
+ DWORD value = mode;
+ RegSetValueEx(hKey, L"SystemUsesLightTheme", 0, REG_DWORD, reinterpret_cast(&value), sizeof(value));
+ RegCloseKey(hKey);
+
+ SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, reinterpret_cast(L"ImmersiveColorSet"), SMTO_ABORTIFHUNG, 5000, nullptr);
+
+ SendMessageTimeout(HWND_BROADCAST, WM_THEMECHANGED, 0, 0, SMTO_ABORTIFHUNG, 5000, nullptr);
+ }
+}
+
+// Can think of this as "is the current theme light?"
+bool GetCurrentSystemTheme()
+{
+ HKEY hKey;
+ DWORD value = 1; // default = light
+ DWORD size = sizeof(value);
+
+ if (RegOpenKeyEx(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ 0,
+ KEY_READ,
+ &hKey) == ERROR_SUCCESS)
+ {
+ RegQueryValueEx(hKey, L"SystemUsesLightTheme", nullptr, nullptr, reinterpret_cast(&value), &size);
+ RegCloseKey(hKey);
+ }
+
+ return value == 1; // true = light, false = dark
+}
+
+bool GetCurrentAppsTheme()
+{
+ HKEY hKey;
+ DWORD value = 1;
+ DWORD size = sizeof(value);
+
+ if (RegOpenKeyEx(HKEY_CURRENT_USER,
+ L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ 0,
+ KEY_READ,
+ &hKey) == ERROR_SUCCESS)
+ {
+ RegQueryValueEx(hKey, L"AppsUseLightTheme", nullptr, nullptr, reinterpret_cast(&value), &size);
+ RegCloseKey(hKey);
+ }
+
+ return value == 1; // true = light, false = dark
+}
diff --git a/src/modules/LightSwitch/LightSwitchService/ThemeHelper.h b/src/modules/LightSwitch/LightSwitchService/ThemeHelper.h
new file mode 100644
index 0000000000..5985fd95c8
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/ThemeHelper.h
@@ -0,0 +1,5 @@
+#pragma once
+void SetSystemTheme(bool dark);
+void SetAppsTheme(bool dark);
+bool GetCurrentSystemTheme();
+bool GetCurrentAppsTheme();
diff --git a/src/modules/LightSwitch/LightSwitchService/ThemeScheduler.cpp b/src/modules/LightSwitch/LightSwitchService/ThemeScheduler.cpp
new file mode 100644
index 0000000000..7b07dd0ef7
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/ThemeScheduler.cpp
@@ -0,0 +1,89 @@
+#include "ThemeScheduler.h"
+#include
+
+SunTimes CalculateSunriseSunset(double latitude, double longitude, int year, int month, int day)
+{
+ double zenith = 90.833;
+ int N1 = static_cast(floor(275.0 * month / 9.0));
+ int N2 = static_cast(floor((static_cast(month) + 9) / 12.0));
+ int N3 = static_cast(floor((1.0 + floor((year - 4.0 * floor(year / 4.0) + 2.0) / 3.0))));
+ int N = N1 - (N2 * N3) + day - 30;
+
+ auto calcTime = [&](bool sunrise) -> double {
+ double lngHour = longitude / 15.0;
+ double t = sunrise ? N + ((6 - lngHour) / 24) : N + ((18 - lngHour) / 24);
+
+ double M = (0.9856 * t) - 3.289;
+ double L = M + (1.916 * sin(deg2rad(M))) + (0.020 * sin(2 * deg2rad(M))) + 282.634;
+ if (L < 0)
+ L += 360;
+ if (L > 360)
+ L -= 360;
+
+ double RA = rad2deg(atan(0.91764 * tan(deg2rad(L))));
+ if (RA < 0)
+ RA += 360;
+ if (RA > 360)
+ RA -= 360;
+
+ double Lquadrant = floor(L / 90) * 90;
+ double RAquadrant = floor(RA / 90) * 90;
+ RA = RA + (Lquadrant - RAquadrant);
+ RA /= 15;
+
+ double sinDec = 0.39782 * sin(deg2rad(L));
+ double cosDec = cos(asin(sinDec));
+
+ double cosH = (cos(deg2rad(zenith)) - (sinDec * sin(deg2rad(latitude)))) / (cosDec * cos(deg2rad(latitude)));
+ if (cosH > 1 || cosH < -1)
+ return -1;
+
+ double H = sunrise ? 360 - rad2deg(acos(cosH)) : rad2deg(acos(cosH));
+ H /= 15;
+
+ double T = H + RA - (0.06571 * t) - 6.622;
+ double UT = T - lngHour;
+ while (UT < 0)
+ UT += 24;
+ while (UT >= 24)
+ UT -= 24;
+
+ return UT;
+ };
+
+ double riseUT = calcTime(true);
+ double setUT = calcTime(false);
+
+ auto toLocal = [](double UT) {
+ TIME_ZONE_INFORMATION tz;
+ DWORD state = GetTimeZoneInformation(&tz);
+ double totalBias = tz.Bias;
+
+ if (state == TIME_ZONE_ID_DAYLIGHT)
+ totalBias += tz.DaylightBias;
+ else if (state == TIME_ZONE_ID_STANDARD)
+ totalBias += tz.StandardBias;
+
+ double biasHours = -(totalBias / 60.0);
+ double localTime = UT + biasHours;
+
+ while (localTime < 0)
+ localTime += 24;
+ while (localTime >= 24)
+ localTime -= 24;
+
+ int hour = static_cast(localTime);
+ int minute = static_cast((localTime - hour) * 60);
+ return std::pair{ hour, minute };
+ };
+
+ auto [riseHour, riseMinute] = toLocal(riseUT);
+ auto [setHour, setMinute] = toLocal(setUT);
+
+ SunTimes result;
+ result.sunriseHour = riseHour;
+ result.sunriseMinute = riseMinute;
+ result.sunsetHour = setHour;
+ result.sunsetMinute = setMinute;
+ return result;
+}
diff --git a/src/modules/LightSwitch/LightSwitchService/ThemeScheduler.h b/src/modules/LightSwitch/LightSwitchService/ThemeScheduler.h
new file mode 100644
index 0000000000..4e6869830a
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/ThemeScheduler.h
@@ -0,0 +1,25 @@
+#pragma once
+#include
+#include
+#include
+
+// Struct to hold calculated sunrise/sunset times
+struct SunTimes
+{
+ int sunriseHour;
+ int sunriseMinute;
+ int sunsetHour;
+ int sunsetMinute;
+};
+
+constexpr double PI = 3.14159265358979323846;
+constexpr double deg2rad(double deg)
+{
+ return deg * PI / 180.0;
+}
+constexpr double rad2deg(double rad)
+{
+ return rad * 180.0 / PI;
+}
+
+SunTimes CalculateSunriseSunset(double latitude, double longitude, int year, int month, int day);
diff --git a/src/modules/LightSwitch/LightSwitchService/WinHookEventIDs.cpp b/src/modules/LightSwitch/LightSwitchService/WinHookEventIDs.cpp
new file mode 100644
index 0000000000..5e271fc8d0
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/WinHookEventIDs.cpp
@@ -0,0 +1,15 @@
+
+#include "WinHookEventIDs.h"
+#include
+#include
+
+UINT WM_PRIV_SETTINGS_CHANGED = 0;
+
+std::once_flag init_flag;
+
+void InitializeWinhookEventIds()
+{
+ std::call_once(init_flag, [&] {
+ WM_PRIV_SETTINGS_CHANGED = RegisterWindowMessage(L"{11978F7B-221A-4E65-B9A9-693F7D6E4B25}");
+ });
+}
diff --git a/src/modules/LightSwitch/LightSwitchService/WinHookEventIDs.h b/src/modules/LightSwitch/LightSwitchService/WinHookEventIDs.h
new file mode 100644
index 0000000000..177fd139cd
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/WinHookEventIDs.h
@@ -0,0 +1,6 @@
+#pragma once
+#include
+
+extern UINT WM_PRIV_SETTINGS_CHANGED; // Scheduled when a watched settings file is updated
+
+void InitializeWinhookEventIds();
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchService/packages.config b/src/modules/LightSwitch/LightSwitchService/packages.config
new file mode 100644
index 0000000000..ff4b059648
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/packages.config
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchService/resource.h b/src/modules/LightSwitch/LightSwitchService/resource.h
new file mode 100644
index 0000000000..e8ed3b4747
--- /dev/null
+++ b/src/modules/LightSwitch/LightSwitchService/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by LightSwitchService.rc
+//
+#define IDI_ICON1 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/LockScreenLogo.scale-200.png b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/LockScreenLogo.scale-200.png
new file mode 100644
index 0000000000..7440f0d4bf
Binary files /dev/null and b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/LockScreenLogo.scale-200.png differ
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/SplashScreen.scale-200.png b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/SplashScreen.scale-200.png
new file mode 100644
index 0000000000..32f486a867
Binary files /dev/null and b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/SplashScreen.scale-200.png differ
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square150x150Logo.scale-200.png b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square150x150Logo.scale-200.png
new file mode 100644
index 0000000000..53ee3777ea
Binary files /dev/null and b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square150x150Logo.scale-200.png differ
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square44x44Logo.scale-200.png b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square44x44Logo.scale-200.png
new file mode 100644
index 0000000000..f713bba67f
Binary files /dev/null and b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square44x44Logo.scale-200.png differ
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
new file mode 100644
index 0000000000..dc9f5bea0c
Binary files /dev/null and b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/StoreLogo.png b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/StoreLogo.png
new file mode 100644
index 0000000000..a4586f26bd
Binary files /dev/null and b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/StoreLogo.png differ
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Wide310x150Logo.scale-200.png b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Wide310x150Logo.scale-200.png
new file mode 100644
index 0000000000..8b4a5d0dd5
Binary files /dev/null and b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/LightSwitch.UITests.csproj b/src/modules/LightSwitch/Tests/LightSwitch.UITests/LightSwitch.UITests.csproj
new file mode 100644
index 0000000000..9770255af6
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/LightSwitch.UITests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ PowerToys.LightSwitch.UITests
+ LightSwitch.UITests
+ false
+ true
+ enable
+ Library
+
+
+ false
+
+
+ $(SolutionDir)$(Platform)\$(Configuration)\tests\LightSwitch.UITests\
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/Package.appxmanifest b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Package.appxmanifest
new file mode 100644
index 0000000000..a38ad92615
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/Package.appxmanifest
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+ LightSwitch.UITests
+ Microsoft
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestGeolocation.cs b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestGeolocation.cs
new file mode 100644
index 0000000000..aaa5124995
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestGeolocation.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.UITest;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace LightSwitch.UITests
+{
+ [TestClass]
+ public class TestGeolocation : UITestBase
+ {
+ public TestGeolocation()
+ : base(PowerToysModule.PowerToysSettings, WindowSize.Large)
+ {
+ }
+
+ [TestMethod("LightSwitch.Geolocation")]
+ [TestCategory("Location")]
+ public void TestGeolocationUpdate()
+ {
+ TestHelper.InitializeTest(this, "geolocation test");
+ TestHelper.PerformGeolocationTest(this);
+ TestHelper.CleanupTest(this);
+ }
+ }
+}
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs
new file mode 100644
index 0000000000..7b586301f6
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs
@@ -0,0 +1,439 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using Microsoft.PowerToys.UITest;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.Win32;
+
+namespace LightSwitch.UITests
+{
+ internal sealed class TestHelper
+ {
+ private static readonly string[] ShortcutSeparators = { " + ", "+", " " };
+
+ ///
+ /// Performs common test initialization: navigate to settings, enable toggle, verify shortcut
+ ///
+ /// The test base instance
+ /// Name of the test for assertions
+ /// The activation keys for the test
+ public static Key[] InitializeTest(UITestBase testBase, string testName)
+ {
+ LaunchFromSetting(testBase);
+
+ var toggleSwitch = SetLightSwitchToggle(testBase, enable: true);
+ Assert.IsTrue(
+ toggleSwitch.IsOn,
+ $"Light Switch toggle switch should be ON for {testName}");
+
+ var activationKeys = ReadActivationShortcut(testBase);
+ Assert.IsNotNull(activationKeys, "Should be able to read activation shortcut");
+ Assert.IsTrue(activationKeys.Length > 0, "Activation shortcut should contain at least one key");
+
+ return activationKeys;
+ }
+
+ ///
+ /// Navigate to the Light Switch settings page
+ ///
+ public static void LaunchFromSetting(UITestBase testBase)
+ {
+ var lightSwitch = testBase.Session.FindAll(By.AccessibilityId("LightSwitchNavItem"));
+
+ if (lightSwitch.Count == 0)
+ {
+ testBase.Session.Find(By.AccessibilityId("SystemToolsNavItem"), 5000).Click(msPostAction: 500);
+ }
+
+ testBase.Session.Find(By.AccessibilityId("LightSwitchNavItem"), 5000).Click(msPostAction: 500);
+ }
+
+ ///
+ /// Set the Light Switch enable toggle switch to the specified state
+ ///
+ public static ToggleSwitch SetLightSwitchToggle(UITestBase testBase, bool enable)
+ {
+ var toggleSwitch = testBase.Session.Find(By.AccessibilityId("Toggle_LightSwitch"), 5000);
+
+ if (toggleSwitch.IsOn != enable)
+ {
+ toggleSwitch.Click(msPreAction: 1000, msPostAction: 2000);
+ }
+
+ if (toggleSwitch.IsOn != enable)
+ {
+ testBase.Session.SendKey(Key.Space, msPreAction: 0, msPostAction: 2000);
+ }
+
+ return toggleSwitch;
+ }
+
+ ///
+ /// Read the current activation shortcut from the ShortcutControl
+ ///
+ public static Key[] ReadActivationShortcut(UITestBase testBase)
+ {
+ var shortcutCard = testBase.Session.Find(By.AccessibilityId("Shortcut_LightSwitch"), 5000);
+ var shortcutButton = shortcutCard.Find(By.AccessibilityId("EditButton"), 5000);
+ return ParseShortcutText(shortcutButton.HelpText);
+ }
+
+ ///
+ /// Parse shortcut text like "Win + Ctrl + Shift + M" into Key array
+ ///
+ private static Key[] ParseShortcutText(string shortcutText)
+ {
+ if (string.IsNullOrEmpty(shortcutText))
+ {
+ return new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.D };
+ }
+
+ var keys = new List();
+ var parts = shortcutText.Split(ShortcutSeparators, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (var part in parts)
+ {
+ var cleanPart = part.Trim().ToLowerInvariant();
+ var key = cleanPart switch
+ {
+ "win" or "windows" => Key.Win,
+ "ctrl" or "control" => Key.Ctrl,
+ "shift" => Key.Shift,
+ "alt" => Key.Alt,
+ _ when cleanPart.Length == 1 && char.IsLetter(cleanPart[0]) &&
+ cleanPart[0] >= 'a' && cleanPart[0] <= 'z' =>
+ (Key)Enum.Parse(typeof(Key), cleanPart.ToUpperInvariant()),
+ _ => (Key?)null,
+ };
+
+ if (key.HasValue)
+ {
+ keys.Add(key.Value);
+ }
+ }
+
+ return keys.Count > 0 ? keys.ToArray() : new Key[] { Key.Win, Key.Ctrl, Key.Shift, Key.D };
+ }
+
+ ///
+ /// Performs common test cleanup: close LightSwitch task
+ ///
+ /// The test base instance
+ public static void CleanupTest(UITestBase testBase)
+ {
+ // TODO: Make sure the task kills?
+ // CloseLightSwitch(testBase);
+
+ // Ensure we're attached to settings after cleanup
+ try
+ {
+ testBase.Session.Attach(PowerToysModule.PowerToysSettings);
+ }
+ catch
+ {
+ // Ignore attachment errors - this is just cleanup
+ }
+ }
+
+ ///
+ /// Perform a update time test operation
+ ///
+ public static void PerformUpdateTimeTest(UITestBase testBase)
+ {
+ // Make sure in manual mode
+ var modeCombobox = testBase.Session.Find(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
+ Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
+
+ var neededTabs = 6;
+
+ if (modeCombobox.Text != "Manual")
+ {
+ modeCombobox.Click();
+ var manualListItem = testBase.Session.Find(By.AccessibilityId("ManualCBItem_LightSwitch"), 5000);
+ Assert.IsNotNull(manualListItem, "Manual combobox item not found.");
+ manualListItem.Click();
+ neededTabs = 1;
+ }
+
+ Assert.AreEqual("Manual", modeCombobox.Text, "Mode combobox should be set to Manual.");
+
+ var timeline = testBase.Session.Find(By.AccessibilityId("Timeline_LightSwitch"), 5000);
+ Assert.IsNotNull(timeline, "Timeline not found.");
+
+ var helpText = timeline.GetAttribute("HelpText");
+ string originalEndValue = GetHelpTextValue(helpText, "End");
+
+ for (int i = 0; i < neededTabs; i++)
+ {
+ testBase.Session.SendKeys(Key.Tab);
+ }
+
+ testBase.Session.SendKeys(Key.Enter);
+ testBase.Session.SendKeys(Key.Up);
+ testBase.Session.SendKeys(Key.Enter);
+
+ helpText = timeline.GetAttribute("HelpText");
+ string updatedEndValue = GetHelpTextValue(helpText, "End");
+
+ Assert.AreNotEqual(originalEndValue, updatedEndValue, "Timeline end time should have been updated.");
+
+ helpText = timeline.GetAttribute("HelpText");
+ string originalStartValue = GetHelpTextValue(helpText, "Start");
+
+ testBase.Session.SendKeys(Key.Tab);
+ testBase.Session.SendKeys(Key.Enter);
+ testBase.Session.SendKeys(Key.Up);
+ testBase.Session.SendKeys(Key.Enter);
+
+ helpText = timeline.GetAttribute("HelpText");
+ string updatedStartValue = GetHelpTextValue(helpText, "Start");
+
+ Assert.AreNotEqual(originalStartValue, updatedStartValue, "Timeline start time should have been updated.");
+ }
+
+ ///
+ /// Perform a update geolocation test operation
+ ///
+ public static void PerformUserSelectedLocationTest(UITestBase testBase)
+ {
+ // Make sure in sun time mode
+ var modeCombobox = testBase.Session.Find(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
+ Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
+
+ if (modeCombobox.Text != "Sunset to sunrise")
+ {
+ modeCombobox.Click();
+ var sunriseListItem = testBase.Session.Find(By.AccessibilityId("SunCBItem_LightSwitch"), 5000);
+ Assert.IsNotNull(sunriseListItem, "Sunrise combobox item not found.");
+ sunriseListItem.Click();
+ }
+
+ Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
+
+ var setLocationButton = testBase.Session.Find(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
+ Assert.IsNotNull(setLocationButton, "Set location button not found.");
+ setLocationButton.Click();
+
+ var autoSuggestTextbox = testBase.Session.Find(By.AccessibilityId("CitySearchBox_LightSwitch"), 5000);
+ Assert.IsNotNull(autoSuggestTextbox, "City search box not found.");
+ autoSuggestTextbox.Click();
+ autoSuggestTextbox.SendKeys("Seattle");
+ autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Down);
+ autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Enter);
+
+ var latLong = testBase.Session.Find(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
+ Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
+
+ var sunrise = testBase.Session.Find(By.AccessibilityId("SunriseText_LightSwitch"), 5000);
+ Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text));
+
+ var sunset = testBase.Session.Find(By.AccessibilityId("SunsetText_LightSwitch"), 5000);
+ Assert.IsFalse(string.IsNullOrWhiteSpace(sunset.Text));
+ }
+
+ ///
+ /// Perform a update geolocation test operation
+ ///
+ public static void PerformGeolocationTest(UITestBase testBase)
+ {
+ // Make sure in sun time mode
+ var modeCombobox = testBase.Session.Find(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
+ Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
+
+ if (modeCombobox.Text != "Sunset to sunrise")
+ {
+ modeCombobox.Click();
+ var sunriseListItem = testBase.Session.Find(By.AccessibilityId("SunCBItem_LightSwitch"), 5000);
+ Assert.IsNotNull(sunriseListItem, "Sunrise combobox item not found.");
+ sunriseListItem.Click();
+ }
+
+ Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
+
+ // Click the select city button
+ var setLocationButton = testBase.Session.Find(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
+ Assert.IsNotNull(setLocationButton, "Set location button not found.");
+ setLocationButton.Click(msPostAction: 8000);
+
+ var latLong = testBase.Session.Find(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
+ Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
+
+ var sunrise = testBase.Session.Find(By.AccessibilityId("SunriseText_LightSwitch"), 5000);
+ Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text));
+
+ var sunset = testBase.Session.Find(By.AccessibilityId("SunsetText_LightSwitch"), 5000);
+ Assert.IsFalse(string.IsNullOrWhiteSpace(sunset.Text));
+ }
+
+ ///
+ /// Perform a update time test operation
+ ///
+ public static void PerformOffsetTest(UITestBase testBase)
+ {
+ // Make sure in sun time mode
+ var modeCombobox = testBase.Session.Find(By.AccessibilityId("ModeSelection_LightSwitch"), 5000);
+ Assert.IsNotNull(modeCombobox, "Mode combobox not found.");
+
+ if (modeCombobox.Text != "Sunset to sunrise")
+ {
+ modeCombobox.Click();
+ var sunriseListItem = testBase.Session.Find(By.AccessibilityId("SunCBItem_LightSwitch"), 5000);
+ Assert.IsNotNull(sunriseListItem, "Sunrise combobox item not found.");
+ sunriseListItem.Click();
+ }
+
+ Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
+
+ // Testing sunrise offset
+ var sunriseOffset = testBase.Session.Find(By.AccessibilityId("SunriseOffset_LightSwitch"), 5000);
+ Assert.IsNotNull(sunriseOffset, "Sunrise offset number box not found.");
+
+ var timeline = testBase.Session.Find(By.AccessibilityId("Timeline_LightSwitch"), 5000);
+ Assert.IsNotNull(timeline, "Timeline not found.");
+
+ var helpText = timeline.GetAttribute("HelpText");
+ string originalStartValue = GetHelpTextValue(helpText, "Start");
+
+ sunriseOffset.Click();
+ testBase.Session.SendKeys(Key.Up);
+
+ helpText = timeline.GetAttribute("HelpText");
+ string updatedStartValue = GetHelpTextValue(helpText, "Start");
+
+ Assert.AreNotEqual(originalStartValue, updatedStartValue, "Timeline start time should have been updated.");
+
+ // Testing sunset offset
+ var sunsetOffset = testBase.Session.Find(By.AccessibilityId("SunsetOffset_LightSwitch"), 5000);
+ Assert.IsNotNull(sunsetOffset, "Sunrise offset number box not found.");
+
+ helpText = timeline.GetAttribute("HelpText");
+ string originalEndValue = GetHelpTextValue(helpText, "End");
+
+ sunsetOffset.Click();
+ testBase.Session.SendKeys(Key.Up);
+
+ helpText = timeline.GetAttribute("HelpText");
+ string updatedEndValue = GetHelpTextValue(helpText, "End");
+
+ Assert.AreNotEqual(originalEndValue, updatedEndValue, "Timeline end time should have been updated.");
+ }
+
+ ///
+ /// Perform a test for shortcut changing themes
+ ///
+ public static void PerformShortcutTest(UITestBase testBase, Key[] activationKeys)
+ {
+ // Test when both are checked
+ var systemCheckbox = testBase.Session.Find(By.AccessibilityId("ChangeSystemCheckbox_LightSwitch"), 5000);
+ Assert.IsNotNull(systemCheckbox, "System checkbox not found.");
+
+ var scrollViewer = testBase.Session.Find(By.AccessibilityId("PageScrollViewer"));
+ systemCheckbox.EnsureVisible(scrollViewer);
+
+ int neededTabs = 10;
+
+ if (!systemCheckbox.Selected)
+ {
+ for (int i = 0; i < neededTabs; i++)
+ {
+ testBase.Session.SendKeys(Key.Tab);
+ }
+
+ systemCheckbox.Click();
+ }
+
+ Assert.IsTrue(systemCheckbox.Selected, "System checkbox should be checked.");
+
+ var appsCheckbox = testBase.Session.Find(By.AccessibilityId("ChangeAppsCheckbox_LightSwitch"), 5000);
+ Assert.IsNotNull(appsCheckbox, "Apps checkbox not found.");
+
+ if (!appsCheckbox.Selected)
+ {
+ appsCheckbox.Click();
+ }
+
+ Assert.IsTrue(appsCheckbox.Selected, "Apps checkbox should be checked.");
+
+ var systemBeforeValue = GetSystemTheme();
+ var appsBeforeValue = GetAppsTheme();
+
+ testBase.Session.SendKeys(activationKeys);
+ Task.Delay(5000).Wait();
+
+ var systemAfterValue = GetSystemTheme();
+ var appsAfterValue = GetAppsTheme();
+
+ Assert.AreNotEqual(systemBeforeValue, systemAfterValue, "System theme should have changed.");
+ Assert.AreNotEqual(appsBeforeValue, appsAfterValue, "Apps theme should have changed.");
+
+ // Test with nothing checked
+ if (systemCheckbox.Selected)
+ {
+ systemCheckbox.Click();
+ }
+
+ if (appsCheckbox.Selected)
+ {
+ appsCheckbox.Click();
+ }
+
+ Assert.IsFalse(systemCheckbox.Selected, "System checkbox should be unchecked.");
+ Assert.IsFalse(appsCheckbox.Selected, "Apps checkbox should be unchecked.");
+
+ var noneSystemBeforeValue = GetSystemTheme();
+ var noneAppsBeforeValue = GetAppsTheme();
+
+ testBase.Session.SendKeys(activationKeys);
+ Task.Delay(5000).Wait();
+
+ var noneSystemAfterValue = GetSystemTheme();
+ var noneAppsAfterValue = GetAppsTheme();
+
+ Assert.AreEqual(noneSystemBeforeValue, noneSystemAfterValue, "System theme should not have changed.");
+ Assert.AreEqual(noneAppsBeforeValue, noneAppsAfterValue, "Apps theme should not have changed.");
+ }
+
+ /* Helpers */
+ private static int GetSystemTheme()
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
+ if (key is null)
+ {
+ return 1;
+ }
+
+ return (int)key.GetValue("SystemUsesLightTheme", 1);
+ }
+
+ private static int GetAppsTheme()
+ {
+ using var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
+ if (key is null)
+ {
+ return 1;
+ }
+
+ return (int)key.GetValue("AppsUseLightTheme", 1);
+ }
+
+ private static string GetHelpTextValue(string helpText, string key)
+ {
+ foreach (var part in helpText.Split(';'))
+ {
+ var kv = part.Split('=');
+ if (kv.Length == 2 && kv[0] == key)
+ {
+ return kv[1];
+ }
+ }
+
+ return string.Empty;
+ }
+ }
+}
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestOffset.cs b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestOffset.cs
new file mode 100644
index 0000000000..e8ed9debf6
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestOffset.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.UITest;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace LightSwitch.UITests
+{
+ [TestClass]
+ public class TestOffset : UITestBase
+ {
+ public TestOffset()
+ : base(PowerToysModule.PowerToysSettings, WindowSize.Large)
+ {
+ }
+
+ [TestMethod("LightSwitch.Offset")]
+ [TestCategory("Time")]
+ public void TestTimeOffset()
+ {
+ TestHelper.InitializeTest(this, "offset test");
+ TestHelper.PerformOffsetTest(this);
+ TestHelper.CleanupTest(this);
+ }
+ }
+}
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestShortcut.cs b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestShortcut.cs
new file mode 100644
index 0000000000..26e17c4612
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestShortcut.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.UITest;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace LightSwitch.UITests
+{
+ [TestClass]
+ public class TestShortcut : UITestBase
+ {
+ public TestShortcut()
+ : base(PowerToysModule.PowerToysSettings, WindowSize.Large)
+ {
+ }
+
+ [TestMethod("LightSwitch.TestShortcut")]
+ [TestCategory("Shortcut")]
+ public void TestLightSwitchShortcut()
+ {
+ var activationKeys = TestHelper.InitializeTest(this, "light switch shortcut test");
+ TestHelper.PerformShortcutTest(this, activationKeys);
+ TestHelper.CleanupTest(this);
+ }
+ }
+}
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestUpdateManualTime.cs b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestUpdateManualTime.cs
new file mode 100644
index 0000000000..f92909657f
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestUpdateManualTime.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.UITest;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace LightSwitch.UITests
+{
+ [TestClass]
+ public class TestUpdateManualTime : UITestBase
+ {
+ public TestUpdateManualTime()
+ : base(PowerToysModule.PowerToysSettings, WindowSize.Large)
+ {
+ }
+
+ [TestMethod("LightSwitch.UpdateManualTime")]
+ [TestCategory("Time")]
+ public void TestUpdateTime()
+ {
+ TestHelper.InitializeTest(this, "update manual time test");
+ TestHelper.PerformUpdateTimeTest(this);
+ TestHelper.CleanupTest(this);
+ }
+ }
+}
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestUserSelectedLocation.cs b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestUserSelectedLocation.cs
new file mode 100644
index 0000000000..924a04d9d9
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestUserSelectedLocation.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.UITest;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace LightSwitch.UITests
+{
+ [TestClass]
+ public class TestUserSelectedLocation : UITestBase
+ {
+ public TestUserSelectedLocation()
+ : base(PowerToysModule.PowerToysSettings, WindowSize.Large)
+ {
+ }
+
+ [TestMethod("LightSwitch.UserSelectedLocation")]
+ [TestCategory("Location")]
+ public void TestUserSelectedLocationUpdate()
+ {
+ TestHelper.InitializeTest(this, "user selected location test");
+ TestHelper.PerformUserSelectedLocationTest(this);
+ TestHelper.CleanupTest(this);
+ }
+ }
+}
diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/app.manifest b/src/modules/LightSwitch/Tests/LightSwitch.UITests/app.manifest
new file mode 100644
index 0000000000..0cec0ecb5e
--- /dev/null
+++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/app.manifest
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PerMonitorV2
+
+
+
\ No newline at end of file
diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp
index 3049d3740c..adf5075837 100644
--- a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp
+++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp
@@ -8,21 +8,28 @@
#include "common/utils/process_path.h"
#include "common/utils/excluded_apps.h"
#include "common/utils/MsWindowsSettings.h"
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
#include
-#ifdef COMPOSITION
namespace winrt
{
using namespace winrt::Windows::System;
- using namespace winrt::Windows::UI::Composition;
}
-namespace ABI
-{
- using namespace ABI::Windows::System;
- using namespace ABI::Windows::UI::Composition::Desktop;
-}
-#endif
+namespace muxc = winrt::Microsoft::UI::Composition;
+namespace muxx = winrt::Microsoft::UI::Xaml;
+namespace muxxc = winrt::Microsoft::UI::Xaml::Controls;
+namespace muxxh = winrt::Microsoft::UI::Xaml::Hosting;
#pragma region Super_Sonar_Base_Code
@@ -70,11 +77,11 @@ protected:
int m_sonarRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS;
int m_sonarZoomFactor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM;
DWORD m_fadeDuration = FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS;
- int m_finalAlphaNumerator = FIND_MY_MOUSE_DEFAULT_OVERLAY_OPACITY;
+ int m_finalAlphaNumerator = 100; // legacy (root now always animates to 1.0; kept for GDI fallback compatibility)
std::vector m_excludedApps;
int m_shakeMinimumDistance = FIND_MY_MOUSE_DEFAULT_SHAKE_MINIMUM_DISTANCE;
static constexpr int FinalAlphaDenominator = 100;
- winrt::DispatcherQueueController m_dispatcherQueueController{ nullptr };
+ winrt::Microsoft::UI::Dispatching::DispatcherQueueController m_dispatcherQueueController{ nullptr };
// Don't consider movements started past these milliseconds to detect shaking.
int m_shakeIntervalMs = FIND_MY_MOUSE_DEFAULT_SHAKE_INTERVAL_MS;
@@ -82,7 +89,6 @@ protected:
int m_shakeFactor = FIND_MY_MOUSE_DEFAULT_SHAKE_FACTOR;
private:
-
// Save the mouse movement that occurred in any direction.
struct PointerRecentMovement
{
@@ -159,7 +165,6 @@ bool SuperSonar::Initialize(HINSTANCE hinst)
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
WNDCLASS wc{};
-
if (!GetClassInfoW(hinst, className, &wc))
{
wc.lpfnWndProc = s_WndProc;
@@ -171,14 +176,28 @@ bool SuperSonar::Initialize(HINSTANCE hinst)
if (!RegisterClassW(&wc))
{
+ Logger::error("RegisterClassW failed. GetLastError={}", GetLastError());
return false;
}
}
+ // else: class already registered
m_hwndOwner = CreateWindow(L"static", nullptr, WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, hinst, nullptr);
+ if (!m_hwndOwner)
+ {
+ Logger::error("Failed to create owner window. GetLastError={}", GetLastError());
+ return false;
+ }
- DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle();
- return CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this) != nullptr;
+ DWORD exStyle = WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle();
+ HWND created = CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this);
+ if (!created)
+ {
+ Logger::error("CreateWindowExW failed. GetLastError={}", GetLastError());
+ return false;
+ }
+
+ return true;
}
template
@@ -226,7 +245,8 @@ LRESULT SuperSonar::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) n
switch (message)
{
case WM_CREATE:
- if(!OnSonarCreate()) return -1;
+ if (!OnSonarCreate())
+ return -1;
UpdateMouseSnooping();
return 0;
@@ -314,8 +334,7 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input)
return;
}
- if ((m_activationMethod != FindMyMouseActivationMethod::DoubleRightControlKey && m_activationMethod != FindMyMouseActivationMethod::DoubleLeftControlKey)
- || input.data.keyboard.VKey != VK_CONTROL)
+ if ((m_activationMethod != FindMyMouseActivationMethod::DoubleRightControlKey && m_activationMethod != FindMyMouseActivationMethod::DoubleLeftControlKey) || input.data.keyboard.VKey != VK_CONTROL)
{
StopSonar();
return;
@@ -326,8 +345,7 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input)
bool leftCtrlPressed = (input.data.keyboard.Flags & RI_KEY_E0) == 0;
bool rightCtrlPressed = (input.data.keyboard.Flags & RI_KEY_E0) != 0;
- if ((m_activationMethod == FindMyMouseActivationMethod::DoubleRightControlKey && !rightCtrlPressed)
- || (m_activationMethod == FindMyMouseActivationMethod::DoubleLeftControlKey && !leftCtrlPressed))
+ if ((m_activationMethod == FindMyMouseActivationMethod::DoubleRightControlKey && !rightCtrlPressed) || (m_activationMethod == FindMyMouseActivationMethod::DoubleLeftControlKey && !leftCtrlPressed))
{
StopSonar();
return;
@@ -376,7 +394,6 @@ void SuperSonar::OnSonarKeyboardInput(RAWINPUT const& input)
GetCursorPos(&m_lastKeyPos);
UpdateMouseSnooping();
}
- Logger::info("Detecting double left control click with {} ms interval.", doubleClickInterval);
m_lastKeyTime = now;
m_lastKeyPos = ptCursor;
}
@@ -402,14 +419,13 @@ template
void SuperSonar::DetectShake()
{
ULONGLONG shakeStartTick = GetTickCount64() - m_shakeIntervalMs;
-
+
// Prune the story of movements for those movements that started too long ago.
std::erase_if(m_movementHistory, [shakeStartTick](const PointerRecentMovement& movement) { return movement.tick < shakeStartTick; });
-
-
+
double distanceTravelled = 0;
- LONGLONG currentX=0, minX=0, maxX=0;
- LONGLONG currentY=0, minY=0, maxY=0;
+ LONGLONG currentX = 0, minX = 0, maxX = 0;
+ LONGLONG currentY = 0, minY = 0, maxY = 0;
for (const PointerRecentMovement& movement : m_movementHistory)
{
@@ -421,23 +437,22 @@ void SuperSonar::DetectShake()
minY = min(currentY, minY);
maxY = max(currentY, maxY);
}
-
+
if (distanceTravelled < m_shakeMinimumDistance)
{
return;
}
// Size of the rectangle that the pointer moved in.
- double rectangleWidth = static_cast(maxX) - minX;
- double rectangleHeight = static_cast(maxY) - minY;
+ double rectangleWidth = static_cast(maxX) - minX;
+ double rectangleHeight = static_cast(maxY) - minY;
double diagonal = sqrt(rectangleWidth * rectangleWidth + rectangleHeight * rectangleHeight);
- if (diagonal > 0 && distanceTravelled / diagonal > (m_shakeFactor/100.f))
+ if (diagonal > 0 && distanceTravelled / diagonal > (m_shakeFactor / 100.f))
{
m_movementHistory.clear();
StartSonar();
}
-
}
template
@@ -453,7 +468,7 @@ void SuperSonar::OnSonarMouseInput(RAWINPUT const& input)
{
LONG relativeX = 0;
LONG relativeY = 0;
- if ((input.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) == MOUSE_MOVE_ABSOLUTE && (input.data.mouse.lLastX!=0 || input.data.mouse.lLastY!=0))
+ if ((input.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE) == MOUSE_MOVE_ABSOLUTE && (input.data.mouse.lLastX != 0 || input.data.mouse.lLastY != 0))
{
// Getting absolute mouse coordinates. Likely inside a VM / RDP session.
if (m_seenAnAbsoluteMousePosition)
@@ -482,7 +497,7 @@ void SuperSonar::OnSonarMouseInput(RAWINPUT const& input)
}
else
{
- m_movementHistory.push_back({ .diff = { .x=relativeX, .y=relativeY }, .tick = GetTickCount64() });
+ m_movementHistory.push_back({ .diff = { .x = relativeX, .y = relativeY }, .tick = GetTickCount64() });
// Mouse movement changed directions. Take the opportunity do detect shake.
DetectShake();
}
@@ -491,7 +506,6 @@ void SuperSonar