diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index f9389b8d91..1a85de1e06 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -7,6 +7,13 @@ body:
- type: markdown
attributes:
value: Please make sure to [search for existing issues](https://github.com/microsoft/PowerToys/issues) before filing a new one!
+- type: markdown
+ attributes:
+ value: |
+ We are aware of the following high-volume issues and are actively working on them. Please check if your issue is one of these before filing a new bug report:
+ * **PowerToys Run crash related to "Desktop composition is disabled"**: This may appear as `COMException: 0x80263001`. For more details, see issue [#31226](https://github.com/microsoft/PowerToys/issues/31226).
+ * **PowerToys Run crash with `COMException (0xD0000701)`**: For more details, see issue [#30769](https://github.com/microsoft/PowerToys/issues/30769).
+ * **PowerToys Run crash with a "Cyclic reference" error**: This `System.InvalidOperationException` is detailed in issue [#36451](https://github.com/microsoft/PowerToys/issues/36451).
- id: version
type: input
attributes:
diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt
index 756c450534..41a53d33ed 100644
--- a/.github/actions/spell-check/allow/code.txt
+++ b/.github/actions/spell-check/allow/code.txt
@@ -321,3 +321,10 @@ REGSTR
# Misc Win32 APIs and PInvokes
INVOKEIDLIST
+
+# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
+DDDD
+FFF
+HHH
+riday
+YYY
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 37ce368394..b3966292e9 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -22,6 +22,7 @@ ADate
ADDSTRING
ADDUNDORECORD
ADifferent
+adjacents
ADMINS
adml
admx
@@ -34,6 +35,7 @@ AFX
AGGREGATABLE
AHK
AHybrid
+AIUI
akv
ALarger
ALIGNRIGHT
@@ -45,6 +47,7 @@ Allmodule
ALLOWUNDO
ALLVIEW
ALPHATYPE
+amazonbedrock
AModifier
amr
ANDSCANS
@@ -97,6 +100,7 @@ atl
ATX
ATRIOX
aumid
+authenticode
Authenticode
AUTOBUDDY
AUTOCHECKBOX
@@ -111,6 +115,9 @@ AValid
AWAYMODE
azcliversion
azman
+azureaiinference
+azureinference
+azureopenai
bbwe
BCIE
bck
@@ -142,6 +149,7 @@ bmi
BNumber
BODGY
BOklab
+Bootstrappers
BOOTSTRAPPERINSTALLFOLDER
BOTTOMALIGN
boxmodel
@@ -169,9 +177,12 @@ BYPOSITION
CALCRECT
CALG
callbackptr
+cabstr
calpwstr
+caub
Cangjie
CANRENAME
+Carlseibert
Canvascustomlayout
CAPTUREBLT
CAPTURECHANGED
@@ -199,6 +210,7 @@ CIBUILD
cidl
CIELCh
cim
+claude
CImage
cla
CLASSDC
@@ -231,6 +243,7 @@ CODENAME
codereview
Codespaces
Coen
+cognitiveservices
COINIT
colid
colorconv
@@ -245,6 +258,7 @@ cominterop
commandnotfound
commandpalette
compmgmt
+COMPOSITIONDISABLED
COMPOSITIONFULL
CONFIGW
CONFLICTINGMODIFIERKEY
@@ -277,6 +291,7 @@ cpptools
cppvsdbg
cppwinrt
createdump
+creativecommons
CREATEPROCESS
CREATESCHEDULEDTASK
CREATESTRUCT
@@ -300,6 +315,8 @@ CURRENTDIR
CURSORINFO
cursorpos
CURSORSHOWING
+CURSORWRAP
+CursorWrap
customaction
CUSTOMACTIONTEST
CUSTOMFORMATPLACEHOLDER
@@ -339,6 +356,7 @@ Deact
debugbreak
decryptor
Dedup
+dfx
Deduplicator
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER
@@ -413,6 +431,7 @@ DROPFILES
DSTINVERT
DString
DSVG
+dto
DTo
DUMMYUNIONNAME
dutil
@@ -512,10 +531,12 @@ EXTRINSICPROPERTIES
eyetracker
FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR
+FNumber
FARPROC
fdx
fesf
FFFF
+Figma
FILEEXPLORER
fileexploreraddons
fileexplorerpreview
@@ -542,6 +563,7 @@ FIXEDSYS
flac
flyouts
FMask
+foundrylocal
fmtid
FOF
FOFX
@@ -606,6 +628,8 @@ GValue
gwl
GWLP
GWLSTYLE
+googleai
+googlegemini
hangeul
Hanzi
Hardlines
@@ -668,6 +692,7 @@ hmonitor
homies
homljgmgpmcbpjbnjpfijnhipfkiclkd
HOOKPROC
+huggingface
HORZRES
HORZSIZE
Hostbackdropbrush
@@ -694,6 +719,7 @@ HTCLIENT
hthumbnail
HTOUCHINPUT
HTTRANSPARENT
+hutchinsoniana
HVal
HValue
Hvci
@@ -715,7 +741,9 @@ IDCANCEL
IDD
idk
idl
+IIM
idlist
+ifd
IDOK
IDOn
IDR
@@ -732,6 +760,7 @@ Ijwhost
ILD
IMAGEHLP
IMAGERESIZERCONTEXTMENU
+IPTC
IMAGERESIZEREXT
imageresizerinput
imageresizersettings
@@ -750,7 +779,7 @@ INITDIALOG
INITGUID
INITTOLOGFONTSTRUCT
INLINEPREFIX
-Inlines
+inlines
INPC
inproc
INPUTHARDWARE
@@ -871,6 +900,7 @@ LOCKTYPE
LOGFONT
LOGFONTW
logon
+lon
LOGMSG
LOGPIXELSX
LOGPIXELSY
@@ -962,6 +992,7 @@ MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metadatas
+metadatamatters
metafile
mfc
Mgmt
@@ -1029,6 +1060,7 @@ msiexec
MSIFASTINSTALL
MSIHANDLE
MSIRESTARTMANAGERCONTROL
+MSIs
msixbundle
MSIXCA
MSLLHOOKSTRUCT
@@ -1120,9 +1152,11 @@ NONCLIENTMETRICSW
NONELEVATED
nonspace
nonstd
+nullrefs
NOOWNERZORDER
NOPARENTNOTIFY
NOPREFIX
+NPU
NOREDIRECTIONBITMAP
NOREDRAW
NOREMOVE
@@ -1182,6 +1216,9 @@ opencode
OPENFILENAME
opensource
openxmlformats
+ollama
+Olllama
+onnx
OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE
ORSCANS
@@ -1276,6 +1313,7 @@ pnid
PNMLINK
Poc
Podcasts
+Photoshop
POINTERID
POINTERUPDATE
Pokedex
@@ -1370,6 +1408,8 @@ pwsz
pwtd
QDC
qit
+QNN
+Qualcomm
QITAB
QITABENT
qoi
@@ -1478,6 +1518,7 @@ sacl
safeprojectname
SAMEKEYPREVIOUSLYMAPPED
SAMESHORTCUTPREVIOUSLYMAPPED
+samsung
sancov
SAVEFAILED
scanled
@@ -1840,6 +1881,7 @@ USEINSTALLERFORTEST
USESHOWWINDOW
USESTDHANDLES
USRDLL
+utm
UType
uuidv
uwp
@@ -1916,6 +1958,7 @@ wcsicmp
wcsncpy
wcsnicmp
WCT
+WCRAPI
WDA
wdm
wdp
@@ -1929,6 +1972,7 @@ wgpocpl
WHEREID
wic
wifi
+wikimedia
wikipedia
WIL
winapi
@@ -1958,6 +2002,8 @@ WINL
winlogon
winmd
WINNT
+windowsml
+winml
winres
winrt
winsdk
@@ -2023,7 +2069,9 @@ XAxis
XButton
xclip
xcopy
+xap
XDeployment
+XDimension
xdf
XDocument
XElement
@@ -2041,6 +2089,7 @@ xsi
XSpeed
XStr
xstyler
+xmp
XTimer
XUP
XVIRTUALSCREEN
@@ -2048,6 +2097,7 @@ xxxxxx
YAxis
ycombinator
YIncrement
+YDimension
yinle
yinyue
YPels
diff --git a/.gitignore b/.gitignore
index ed3f80a4ec..1318abc22c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -349,10 +349,7 @@ src/common/Telemetry/*.etl
/src/modules/powerrename/ui/RCb24464
# 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 d99fabf0eb..c419d1b588 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -5,7 +5,6 @@
{
"MatchedPath": [
"*.resources.dll",
-
"WinUI3Apps\\Assets\\Settings\\Scripts\\*.ps1",
"PowerToys.ActionRunner.exe",
@@ -27,6 +26,7 @@
"PowerToys.GPOWrapper.dll",
"PowerToys.GPOWrapperProjection.dll",
"PowerToys.AllExperiments.dll",
+ "LanguageModelProvider.dll",
"Common.Search.dll",
@@ -181,6 +181,7 @@
"PowerToys.MousePointerCrosshairs.dll",
"PowerToys.MouseJumpUI.dll",
"PowerToys.MouseJumpUI.exe",
+ "PowerToys.CursorWrap.dll",
"PowerToys.MouseWithoutBorders.dll",
"PowerToys.MouseWithoutBorders.exe",
@@ -346,6 +347,8 @@
"Testably.Abstractions.FileSystem.Interface.dll",
"WinUI3Apps\\Testably.Abstractions.FileSystem.Interface.dll",
"ColorCode.Core.dll",
+ "Microsoft.SemanticKernel.Connectors.Ollama.dll",
+ "OllamaSharp.dll",
"UnitsNet.dll",
"UtfUnknown.dll",
diff --git a/.pipelines/ESRPSigning_installer.json b/.pipelines/ESRPSigning_installer.json
deleted file mode 100644
index c9e505d3a2..0000000000
--- a/.pipelines/ESRPSigning_installer.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "Version": "1.0.0",
- "UseMinimatch": false,
- "SignBatches": [
- {
- "MatchedPath": [
- "PowerToysSetupCustomActionsVNext.dll",
- "SilentFilesInUseBAFunction.dll",
- "PowerToys*Setup-*.exe",
- "PowerToys*Setup-*.msi"
- ],
- "SigningInfo": {
- "Operations": [
- {
- "KeyCode": "CP-230012",
- "OperationSetCode": "SigntoolSign",
- "Parameters": [
- {
- "parameterName": "OpusName",
- "parameterValue": "Microsoft"
- },
- {
- "parameterName": "OpusInfo",
- "parameterValue": "http://www.microsoft.com"
- },
- {
- "parameterName": "FileDigest",
- "parameterValue": "/fd \"SHA256\""
- },
- {
- "parameterName": "PageHash",
- "parameterValue": "/NPH"
- },
- {
- "parameterName": "TimeStamp",
- "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
- }
- ],
- "ToolName": "sign",
- "ToolVersion": "1.0"
- },
- {
- "KeyCode": "CP-230012",
- "OperationSetCode": "SigntoolVerify",
- "Parameters": [],
- "ToolName": "sign",
- "ToolVersion": "1.0"
- }
- ]
- }
- }
- ]
-}
diff --git a/.pipelines/generateDscManifests.ps1 b/.pipelines/generateDscManifests.ps1
index 109610e62e..e0a2f463af 100644
--- a/.pipelines/generateDscManifests.ps1
+++ b/.pipelines/generateDscManifests.ps1
@@ -65,21 +65,28 @@ if (-not (Test-Path $outputDir)) {
New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
}
-Write-Host "DSC manifests will be generated to: '$outputDir'"
+# DSC v3 manifests go to DSCModules subfolder
+$dscOutputDir = Join-Path $outputDir 'DSCModules'
+if (-not (Test-Path $dscOutputDir)) {
+ Write-Host "Creating DSCModules subfolder at '$dscOutputDir'."
+ New-Item -Path $dscOutputDir -ItemType Directory -Force | Out-Null
+}
-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
+Write-Host "DSC manifests will be generated to: '$dscOutputDir'"
-$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
+Write-Host "Cleaning previously generated DSC manifest files from '$dscOutputDir'."
+Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force
+
+$arguments = @('manifest', '--resource', 'settings', '--outputDir', $dscOutputDir)
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
+$generatedFiles = Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
if ($generatedFiles.Count -eq 0) {
- throw "No DSC manifest files were generated in '$outputDir'."
+ throw "No DSC manifest files were generated in '$dscOutputDir'."
}
Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"
diff --git a/.pipelines/v2/ci.yml b/.pipelines/v2/ci.yml
index 6b0105a38a..297c268757 100644
--- a/.pipelines/v2/ci.yml
+++ b/.pipelines/v2/ci.yml
@@ -32,7 +32,7 @@ parameters:
- name: enableMsBuildCaching
type: boolean
displayName: "Enable MSBuild Caching"
- default: true
+ default: false
- name: runTests
type: boolean
displayName: "Run Tests"
diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml
index 2ffd0c7f62..34e4ce4340 100644
--- a/.pipelines/v2/templates/job-build-project.yml
+++ b/.pipelines/v2/templates/job-build-project.yml
@@ -266,6 +266,26 @@ jobs:
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
+ - task: VSBuild@1
+ displayName: Generate DSC artifacts for ARM64
+ condition: and(succeeded(), eq(variables['BuildPlatform'], 'arm64'))
+ inputs:
+ solution: PowerToys.sln
+ vsVersion: 17.0
+ msbuildArgs: >-
+ -restore
+ /p:Configuration=$(BuildConfiguration)
+ /p:Platform=x64
+ /t:DSC\PowerToys_Settings_DSC_Schema_Generator
+ /bl:$(LogOutputDirectory)\build-dsc-generator.binlog
+ ${{ parameters.additionalBuildOptions }}
+ $(MSBuildCacheParameters)
+ $(RestoreAdditionalProjectSourcesArg)
+ platform: x64
+ configuration: $(BuildConfiguration)
+ msbuildArchitecture: x64
+ maximumCpuCount: true
+
# 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)
@@ -512,14 +532,6 @@ jobs:
versionNumber: ${{ parameters.versionNumber }}
additionalBuildOptions: ${{ parameters.additionalBuildOptions }}
- - template: steps-build-installer-vnext.yml
- parameters:
- codeSign: ${{ parameters.codeSign }}
- signingIdentity: ${{ parameters.signingIdentity }}
- versionNumber: ${{ parameters.versionNumber }}
- additionalBuildOptions: ${{ parameters.additionalBuildOptions }}
- buildUserInstaller: true # NOTE: This is the distinction between the above and below rules
-
# This saves ~1GiB per architecture. We won't need these later.
# Removes:
# - All .pdb files from any static libs .libs (which were only used during linking)
diff --git a/.pipelines/v2/templates/steps-build-installer-vnext.yml b/.pipelines/v2/templates/steps-build-installer-vnext.yml
index 882df8696a..71a698b219 100644
--- a/.pipelines/v2/templates/steps-build-installer-vnext.yml
+++ b/.pipelines/v2/templates/steps-build-installer-vnext.yml
@@ -2,9 +2,6 @@ parameters:
- name: versionNumber
type: string
default: "0.0.1"
- - name: buildUserInstaller
- type: boolean
- default: false
- name: codeSign
type: boolean
default: false
@@ -25,43 +22,26 @@ steps:
arguments: 'install --global wix --version 5.0.2'
- pwsh: |-
- & git clean -xfd -e *exe -- .\installer\
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Clean installer to reduce cross-contamination
-
- - pwsh: |-
- # Determine whether this is a per-user build
- $IsPerUser = $${{ parameters.buildUserInstaller }}
-
- # Build slug used to locate the artifacts
- $InstallerBuildSlug = if ($IsPerUser) { 'UserSetup' } else { 'MachineSetup' }
-
- # VNext bundle folder; base name intentionally omits the VNext suffix
- $InstallerFolder = 'PowerToysSetupVNext'
- if ($IsPerUser) {
- $InstallerBasename = "PowerToysUserSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
- }
- else {
- $InstallerBasename = "PowerToysSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
- }
-
- # Export variables for downstream steps
- Write-Host "##vso[task.setvariable variable=InstallerBuildSlug]$InstallerBuildSlug"
- Write-Host "##vso[task.setvariable variable=InstallerRelativePath]$(BuildPlatform)\$(BuildConfiguration)\$InstallerBuildSlug"
- Write-Host "##vso[task.setvariable variable=InstallerBasename]$InstallerBasename"
- Write-Host "##vso[task.setvariable variable=InstallerFolder]$InstallerFolder"
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Prepare Installer variables
+ Write-Host "##vso[task.setvariable variable=InstallerMachineRoot]installer\PowerToysSetupVNext\$(BuildPlatform)\$(BuildConfiguration)\MachineSetup"
+ Write-Host "##vso[task.setvariable variable=InstallerUserRoot]installer\PowerToysSetupVNext\$(BuildPlatform)\$(BuildConfiguration)\UserSetup"
+ Write-Host "##vso[task.setvariable variable=InstallerMachineBasename]PowerToysSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
+ Write-Host "##vso[task.setvariable variable=InstallerUserBasename]PowerToysUserSetup-${{ parameters.versionNumber }}-$(BuildPlatform)"
+ displayName: Prepare Installer variables
# This dll needs to be built and signed before building the MSI.
+ # The Custom Actions project contains a pre-build event that prepares the .wxs files
+ # by filling them out with all our components. We pass RunBuildEvents=true to force
+ # that logic to run.
- task: VSBuild@1
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build PowerToysSetupCustomActionsVNext
+ displayName: Build Shared Support DLLs
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
- /t:PowerToysSetupCustomActionsVNext
- /p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true
+ /t:PowerToysSetupCustomActionsVNext;SilentFilesInUseBAFunction
+ /p:RunBuildEvents=true;RestorePackagesConfig=true;CIBuild=true
-restore -graph
- /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-actions.binlog
+ /bl:$(LogOutputDirectory)\installer-actions.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
@@ -70,28 +50,53 @@ steps:
maximumCpuCount: true
- ${{ if eq(parameters.codeSign, true) }}:
- - template: steps-esrp-signing.yml
+ - template: steps-esrp-sign-files-authenticode.yml
parameters:
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign PowerToysSetupCustomActionsVNext
+ displayName: Sign Shared Support DLLs
signingIdentity: ${{ parameters.signingIdentity }}
- inputs:
- FolderPath: 'installer/PowerToysSetupCustomActionsVNext/$(InstallerRelativePath)'
- signType: batchSigning
- batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
- ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
+ folder: 'installer'
+ pattern: |-
+ **/PowerToysSetupCustomActionsVNext.dll
+ **/SilentFilesInUseBAFunction.dll
## INSTALLER START
#### MSI BUILDING AND SIGNING
+ #
+ # The MSI build contains code that reverts the .wxs files to their in-tree versions.
+ # This is only supposed to happen during local builds. Since this build system is
+ # supposed to run side by side--machine and then user--we do NOT want to destroy
+ # the .wxs files. Therefore, we pass RunBuildEvents=false to suppress all of that
+ # logic.
+ #
+ # We pass BuildProjectReferences=false so that it does not recompile the DLLs we just built.
+ # We only pass -restore on the first one because the second run should already have all
+ # of the dependencies.
- task: VSBuild@1
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext MSI
+ displayName: 💻 Build VNext MSI
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
-restore
/t:PowerToysInstallerVNext
- /p:RunBuildEvents=false;PerUser=${{parameters.buildUserInstaller}};BuildProjectReferences=false;CIBuild=true
- /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-msi.binlog
+ /p:RunBuildEvents=false;PerUser=false;BuildProjectReferences=false;CIBuild=true
+ /bl:$(LogOutputDirectory)\installer-machine-msi.binlog
+ ${{ parameters.additionalBuildOptions }}
+ platform: $(BuildPlatform)
+ configuration: $(BuildConfiguration)
+ clean: false # don't undo our hard work above by deleting the CustomActions dll
+ msbuildArchitecture: x64
+ maximumCpuCount: true
+
+ - task: VSBuild@1
+ displayName: 👤 Build VNext MSI
+ inputs:
+ solution: "**/installer/PowerToysSetup.sln"
+ vsVersion: 17.0
+ msbuildArgs: >-
+ /t:PowerToysInstallerVNext
+ /p:RunBuildEvents=false;PerUser=true;BuildProjectReferences=false;CIBuild=true
+ /bl:$(LogOutputDirectory)\installer-user-msi.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
@@ -100,77 +105,66 @@ steps:
maximumCpuCount: true
- script: |-
- wix msi decompile installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).msi -x $(build.sourcesdirectory)\extractedMsi
- dir $(build.sourcesdirectory)\extractedMsi
- displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Extract and verify MSI"
+ wix msi decompile $(InstallerMachineRoot)\$(InstallerMachineBasename).msi -x $(build.sourcesdirectory)\extractedMachineMsi
+ wix msi decompile $(InstallerUserRoot)\$(InstallerUserBasename).msi -x $(build.sourcesdirectory)\extractedUserMsi
+ dir $(build.sourcesdirectory)\extractedMachineMsi
+ dir $(build.sourcesdirectory)\extractedUserMsi
+ displayName: "WiX5: Extract and verify MSIs"
# Check if deps.json files don't reference different dll versions.
- pwsh: |-
- & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMsi\File'
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Audit deps.json in MSI extracted files
+ & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\File'
+ & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedUserMsi\File'
+ displayName: Audit deps.json in MSI extracted files
- ${{ if eq(parameters.codeSign, true) }}:
- pwsh: |-
- & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\File'
- & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\Binary'
- git clean -xfd ./extractedMsi
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Verify all binaries are signed and versioned
+ & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\File'
+ & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\Binary'
+ & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedUserMsi\File'
+ & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedUserMsi\Binary'
+ git clean -xfd ./extractedMachineMsi ./extractedUserMsi
+ displayName: Verify all binaries are signed and versioned
- - template: steps-esrp-signing.yml
+ - template: steps-esrp-sign-files-authenticode.yml
parameters:
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign VNext MSI
+ displayName: Sign VNext MSIs
signingIdentity: ${{ parameters.signingIdentity }}
- inputs:
- FolderPath: 'installer/$(InstallerFolder)/$(InstallerRelativePath)'
- signType: batchSigning
- batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
- ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
+ folder: 'installer'
+ pattern: '**/PowerToys*Setup-*.msi'
#### 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
- -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
+ # We pass BuildProjectReferences=false so that it does not recompile the DLLs we just built.
+ # We only pass -restore on the first one because the second run should already have all
+ # of the dependencies.
- task: VSBuild@1
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext Bootstrapper
+ displayName: 💻 Build VNext Bootstrapper
inputs:
solution: "**/installer/PowerToysSetup.sln"
vsVersion: 17.0
msbuildArgs: >-
-restore
/t:PowerToysBootstrapperVNext
- /p:PerUser=${{parameters.buildUserInstaller}};CIBuild=true
- /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-bootstrapper.binlog
- -restore -graph
+ /p:PerUser=false;BuildProjectReferences=false;CIBuild=true
+ /bl:$(LogOutputDirectory)\installer-machine-bootstrapper.binlog
+ ${{ parameters.additionalBuildOptions }}
+ platform: $(BuildPlatform)
+ configuration: $(BuildConfiguration)
+ clean: false # don't undo our hard work above by deleting the MSI nor SilentFilesInUseBAFunction
+ msbuildArchitecture: x64
+ maximumCpuCount: true
+
+ - task: VSBuild@1
+ displayName: 👤 Build VNext Bootstrapper
+ inputs:
+ solution: "**/installer/PowerToysSetup.sln"
+ vsVersion: 17.0
+ msbuildArgs: >-
+ /t:PowerToysBootstrapperVNext
+ /p:PerUser=true;BuildProjectReferences=false;CIBuild=true
+ /bl:$(LogOutputDirectory)\installer-user-bootstrapper.binlog
${{ parameters.additionalBuildOptions }}
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
@@ -181,54 +175,41 @@ steps:
# The entirety of bundle unpacking/re-packing is unnecessary if we are not code signing it.
- ${{ if eq(parameters.codeSign, true) }}:
- script: |-
- wix burn detach installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe -engine installer\engine.exe
- displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Extract Engine from Bundle"
+ wix burn detach $(InstallerMachineRoot)\$(InstallerMachineBasename).exe -engine installer\machine-engine.exe
+ wix burn detach $(InstallerUserRoot)\$(InstallerUserBasename).exe -engine installer\user-engine.exe
+ displayName: "WiX5: Extract Engines from Bundles"
- - template: steps-esrp-signing.yml
+ - template: steps-esrp-sign-files-authenticode.yml
parameters:
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign WiX Engine
+ displayName: Sign WiX Engines
signingIdentity: ${{ parameters.signingIdentity }}
- inputs:
- FolderPath: "installer"
- Pattern: engine.exe
- signConfigType: inlineSignParams
- inlineOperation: |
- [
- {
- "KeyCode": "CP-230012",
- "OperationCode": "SigntoolSign",
- "Parameters": {
- "OpusName": "Microsoft",
- "OpusInfo": "http://www.microsoft.com",
- "FileDigest": "/fd \"SHA256\"",
- "PageHash": "/NPH",
- "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
- },
- "ToolName": "sign",
- "ToolVersion": "1.0"
- },
- {
- "KeyCode": "CP-230012",
- "OperationCode": "SigntoolVerify",
- "Parameters": {},
- "ToolName": "sign",
- "ToolVersion": "1.0"
- }
- ]
+ folder: "installer"
+ pattern: '*-engine.exe'
- script: |-
- wix burn reattach installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe -engine installer\engine.exe -o installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe
- displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Reattach Engine to Bundle"
+ wix burn reattach $(InstallerMachineRoot)\$(InstallerMachineBasename).exe -engine installer\machine-engine.exe -o $(InstallerMachineRoot)\$(InstallerMachineBasename).exe
+ wix burn reattach $(InstallerUserRoot)\$(InstallerUserBasename).exe -engine installer\user-engine.exe -o $(InstallerUserRoot)\$(InstallerUserBasename).exe
+ displayName: "WiX5: Reattach Engines to Bundles"
- - template: steps-esrp-signing.yml
+ - pwsh: |-
+ & wix burn extract -oba installer\ba\m "$(InstallerMachineRoot)\$(InstallerMachineBasename).exe"
+ & wix burn extract -oba installer\ba\u "$(InstallerUserRoot)\$(InstallerUserBasename).exe"
+ Get-ChildItem installer\ba -Recurse -Include *.exe,*.dll | Get-AuthenticodeSignature | ForEach-Object {
+ If ($_.Status -Ne "Valid") {
+ Write-Error $_.StatusMessage
+ } Else {
+ Write-Host $_.StatusMessage
+ }
+ }
+ & git clean -fdx installer\ba
+ displayName: "WiX5: Verify Bootstrapper content is signed"
+
+ - template: steps-esrp-sign-files-authenticode.yml
parameters:
- displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign Final Bootstrapper
+ displayName: Sign Final Bootstrappers
signingIdentity: ${{ parameters.signingIdentity }}
- inputs:
- FolderPath: 'installer/$(InstallerFolder)/$(InstallerRelativePath)'
- signType: batchSigning
- batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json'
- ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml'
+ folder: 'installer'
+ pattern: '**/PowerToys*Setup-*.exe'
#### END BOOTSTRAP
## END INSTALLER
diff --git a/.pipelines/v2/templates/steps-esrp-sign-files-authenticode.yml b/.pipelines/v2/templates/steps-esrp-sign-files-authenticode.yml
new file mode 100644
index 0000000000..5b9bbd2fce
--- /dev/null
+++ b/.pipelines/v2/templates/steps-esrp-sign-files-authenticode.yml
@@ -0,0 +1,45 @@
+parameters:
+ - name: displayName
+ type: string
+ default: Sign Specific Files
+ - name: folder
+ type: string
+ - name: pattern
+ type: string
+ - name: signingIdentity
+ type: object
+ default: {}
+
+steps:
+ - template: steps-esrp-signing.yml
+ parameters:
+ displayName: ${{ parameters.displayName }}
+ signingIdentity: ${{ parameters.signingIdentity }}
+ inputs:
+ FolderPath: ${{ parameters.folder }}
+ Pattern: ${{ parameters.pattern }}
+ UseMinimatch: true
+ signConfigType: inlineSignParams
+ inlineOperation: |-
+ [
+ {
+ "KeyCode": "CP-230012",
+ "OperationCode": "SigntoolSign",
+ "Parameters": {
+ "OpusName": "Microsoft",
+ "OpusInfo": "http://www.microsoft.com",
+ "FileDigest": "/fd \"SHA256\"",
+ "PageHash": "/NPH",
+ "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
+ },
+ "ToolName": "sign",
+ "ToolVersion": "1.0"
+ },
+ {
+ "KeyCode": "CP-230012",
+ "OperationCode": "SigntoolVerify",
+ "Parameters": {},
+ "ToolName": "sign",
+ "ToolVersion": "1.0"
+ }
+ ]
diff --git a/.vscode/launch.json b/.vscode/launch.json
index b2d2bca9ac..940ff302de 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -38,6 +38,17 @@
"env": {},
"console": "internalConsole",
"stopAtEntry": false
- }
+ },
+ {
+ "name": "Run AdvancedPaste (managed, no build, ARCH configurable)",
+ "type": "coreclr",
+ "request": "launch",
+ "program": "${workspaceFolder}\\${input:arch}\\Debug\\WinUI3Apps\\PowerToys.AdvancedPaste.exe",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ "env": {},
+ "console": "internalConsole",
+ "stopAtEntry": false
+ },
]
}
\ No newline at end of file
diff --git a/Cpp.Build.props b/Cpp.Build.props
index 5a4538f940..99738fd0dc 100644
--- a/Cpp.Build.props
+++ b/Cpp.Build.props
@@ -1,6 +1,56 @@
+
+
+ $(Configuration)
+
+
+
+
+
+
+
+
+
+
+ MultiThreadedDebug
+
+
+
+ %(IgnoreSpecificDefaultLibraries);libucrtd.lib
+ %(AdditionalOptions) /defaultlib:ucrtd.lib
+
+
+
+
+
+ MultiThreaded
+
+
+
+ %(IgnoreSpecificDefaultLibraries);libucrt.lib
+ %(AdditionalOptions) /defaultlib:ucrt.lib
+
+
+
+
+
+
+ MultiThreadedDebugDLL
+
+
+
+
+ MultiThreadedDLL
+
+
@@ -73,7 +123,6 @@
_DEBUG;%(PreprocessorDefinitions)
Disabled
- MultiThreadedDebug
true
@@ -83,7 +132,6 @@
NDEBUG;%(PreprocessorDefinitions)
MaxSpeed
- MultiThreaded
true
true
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 129abadb00..ec93d6d829 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,4 +1,4 @@
-
+
true
true
@@ -9,7 +9,6 @@
-
@@ -35,23 +34,35 @@
-
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
-
-
+
+
+
-
+
-
-
+
+
+
-
+
-
-
-
-
+
+
+
+
@@ -127,4 +139,4 @@
-
+
\ No newline at end of file
diff --git a/NOTICE.md b/NOTICE.md
index 1998ea805a..a0d87d429c 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1495,7 +1495,6 @@ SOFTWARE.
- AdaptiveCards.Rendering.WinUI3
- AdaptiveCards.Templating
- Appium.WebDriver
-- Azure.AI.OpenAI
- CoenM.ImageSharp.ImageHash
- CommunityToolkit.Common
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
diff --git a/PowerToys.sln b/PowerToys.sln
index 4b49f397e1..e34289ef53 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -822,12 +822,16 @@ 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("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CursorWrap", "src\modules\MouseUtils\CursorWrap\CursorWrap.vcxproj", "{48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}"
+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
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageModelProvider", "src\common\LanguageModelProvider\LanguageModelProvider.csproj", "{45354F4F-1414-45CE-B600-51CD1209FD19}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}"
EndProject
Global
@@ -2988,6 +2992,14 @@ 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
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Debug|ARM64.Build.0 = Debug|ARM64
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Debug|x64.ActiveCfg = Debug|x64
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Debug|x64.Build.0 = Debug|x64
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Release|ARM64.ActiveCfg = Release|ARM64
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Release|ARM64.Build.0 = Release|ARM64
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.Release|x64.ActiveCfg = Release|x64
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5}.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
@@ -3008,6 +3020,14 @@ Global
{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
+ {45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|ARM64.Build.0 = Debug|ARM64
+ {45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|x64.ActiveCfg = Debug|x64
+ {45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|x64.Build.0 = Debug|x64
+ {45354F4F-1414-45CE-B600-51CD1209FD19}.Release|ARM64.ActiveCfg = Release|ARM64
+ {45354F4F-1414-45CE-B600-51CD1209FD19}.Release|ARM64.Build.0 = Release|ARM64
+ {45354F4F-1414-45CE-B600-51CD1209FD19}.Release|x64.ActiveCfg = Release|x64
+ {45354F4F-1414-45CE-B600-51CD1209FD19}.Release|x64.Build.0 = Release|x64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.Build.0 = Debug|ARM64
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.ActiveCfg = Debug|x64
@@ -3341,9 +3361,11 @@ Global
{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}
+ {48A1DB8C-5DF8-4FB3-9E14-2B67F3F2D8B5} = {322566EF-20DC-43A6-B9F8-616AF942579A}
{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}
+ {45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
diff --git a/README.md b/README.md
index 85fad26e1f..e737281523 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ Before you begin, make sure your device meets the system requirements:
Choose one of the installation methods below:
-
+
Download .exe from GitHub
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.
@@ -56,17 +56,17 @@ Go to the [PowerToys GitHub releases][github-release-link], click Assets to reve
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
-[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-x64.exe
-[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-arm64.exe
-[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-x64.exe
-[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-arm64.exe
+[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-x64.exe
+[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-arm64.exe
+[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-x64.exe
+[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-arm64.exe
| Description | Filename |
|----------------|----------|
-| Per user - x64 | [PowerToysUserSetup-0.95.0-x64.exe][ptUserX64] |
-| Per user - ARM64 | [PowerToysUserSetup-0.95.0-arm64.exe][ptUserArm64] |
-| Machine wide - x64 | [PowerToysSetup-0.95.0-x64.exe][ptMachineX64] |
-| Machine wide - ARM64 | [PowerToysSetup-0.95.0-arm64.exe][ptMachineArm64] |
+| Per user - x64 | [PowerToysUserSetup-0.95.1-x64.exe][ptUserX64] |
+| Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] |
+| Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] |
+| Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] |
@@ -281,4 +281,4 @@ The application logs basic diagnostic data (telemetry). For more privacy informa
[roadmap]: https://github.com/microsoft/PowerToys/wiki/Roadmap
[privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839
[loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title=
-[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
\ No newline at end of file
+[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs
diff --git a/installer/PowerToysSetup/generateDscResourcesWxs.ps1 b/installer/PowerToysSetup/generateDscResourcesWxs.ps1
deleted file mode 100644
index f76fd71953..0000000000
--- a/installer/PowerToysSetup/generateDscResourcesWxs.ps1
+++ /dev/null
@@ -1,87 +0,0 @@
-[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
-$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration"
-
-if (-not (Test-Path $buildOutputDir)) {
- Write-Error "Build output directory not found: '$buildOutputDir'"
- exit 1
-}
-
-$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'"
- $wxsContent = @"
-
-
-
-
-
-
-
-"@
- 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/PowerToysSetupCustomActionsVNext.vcxproj b/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj
index 7cd49be6ea..21b0e75837 100644
--- a/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj
+++ b/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj
@@ -34,13 +34,8 @@
- $(Platform)\$(Configuration)\MachineSetup\
- $(Platform)\$(Configuration)\UserSetup\
- $(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\MachineSetup\obj\
- $(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\UserSetup\obj\
-
- false
- true
+ $(Platform)\$(Configuration)\SetupShared\
+ $(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\SetupShared\obj\
true
@@ -59,6 +54,7 @@
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\CmdPal.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\CmdPal.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\ColorPicker.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\ColorPicker.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Core.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Core.wxs.bk""""
+ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\DscResources.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\DscResources.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\EnvironmentVariables.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\EnvironmentVariables.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\FileExplorerPreview.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\FileExplorerPreview.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\FileLocksmith.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\FileLocksmith.wxs.bk""""
@@ -80,8 +76,7 @@
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\WinAppSDK.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\WinAppSDK.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\WinUI3Applications.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\WinUI3Applications.wxs.bk""""
call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Workspaces.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Workspaces.wxs.bk""""
- if not "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform)
- if "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform) -installscopeperuser $(NormalizedPerUserValue)
+ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform)
Backing up original files and populating .NET and WPF Runtime dependencies for WiX3 based installer
@@ -115,7 +110,6 @@
Disabled
_DEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)
EnableFastChecks
- MultiThreadedDebug
true
@@ -128,7 +122,6 @@
MaxSpeed
true
NDEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)
- MultiThreaded
true
@@ -181,4 +174,4 @@
-
\ No newline at end of file
+
diff --git a/installer/PowerToysSetupVNext/CmdPal.wxs b/installer/PowerToysSetupVNext/CmdPal.wxs
index 8304551c12..f05a1f2f35 100644
--- a/installer/PowerToysSetupVNext/CmdPal.wxs
+++ b/installer/PowerToysSetupVNext/CmdPal.wxs
@@ -4,6 +4,13 @@
+
+
+
+
+
+
+
@@ -18,14 +25,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/Core.wxs b/installer/PowerToysSetupVNext/Core.wxs
index f7da6162f9..a9cf083512 100644
--- a/installer/PowerToysSetupVNext/Core.wxs
+++ b/installer/PowerToysSetupVNext/Core.wxs
@@ -15,8 +15,8 @@
-
-
+
+
@@ -24,8 +24,8 @@
-
-
+
+
@@ -63,16 +63,6 @@
-
-
-
-
-
-
-
-
-
-
@@ -120,7 +110,6 @@
-
@@ -128,16 +117,15 @@
-
-
-
-
-
-
+
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/Directory.Build.props b/installer/PowerToysSetupVNext/Directory.Build.props
index 505e3cf844..69a63832d1 100644
--- a/installer/PowerToysSetupVNext/Directory.Build.props
+++ b/installer/PowerToysSetupVNext/Directory.Build.props
@@ -1,4 +1,5 @@
+
@@ -8,4 +9,4 @@
$(BaseIntermediateOutputPath)
-
+
\ No newline at end of file
diff --git a/installer/PowerToysSetupVNext/DscResources.wxs b/installer/PowerToysSetupVNext/DscResources.wxs
new file mode 100644
index 0000000000..2c08253229
--- /dev/null
+++ b/installer/PowerToysSetupVNext/DscResources.wxs
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
index afce3d396d..18d6232140 100644
--- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
+++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj
@@ -14,7 +14,6 @@ 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)"
@@ -25,7 +24,6 @@ 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)"
@@ -37,6 +35,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\CmdPal.wxs.bk ..\..\..\CmdPal.wxs
call move /Y ..\..\..\ColorPicker.wxs.bk ..\..\..\ColorPicker.wxs
call move /Y ..\..\..\Core.wxs.bk ..\..\..\Core.wxs
+ call move /Y ..\..\..\DscResources.wxs.bk ..\..\..\DscResources.wxs
call move /Y ..\..\..\EnvironmentVariables.wxs.bk ..\..\..\EnvironmentVariables.wxs
call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs
call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs
@@ -60,6 +59,12 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil
call move /Y ..\..\..\Workspaces.wxs.bk ..\..\..\Workspaces.wxs
+
+
+
+ false
+ false
+
$(DefineConstants);PerUser=true
diff --git a/installer/PowerToysSetupVNext/Settings.wxs b/installer/PowerToysSetupVNext/Settings.wxs
index f9e5312ea7..cf7cf7f727 100644
--- a/installer/PowerToysSetupVNext/Settings.wxs
+++ b/installer/PowerToysSetupVNext/Settings.wxs
@@ -14,11 +14,16 @@
+
+
+
-
+
+
+
@@ -45,6 +50,11 @@
+
+
+
+
+
@@ -67,6 +77,7 @@
+
diff --git a/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj b/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj
index 3972c1b0f7..d45e32f87c 100644
--- a/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj
+++ b/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunction.vcxproj
@@ -1,30 +1,7 @@
-
-
-
-
- Debug
- ARM64
-
-
- Release
- ARM64
-
-
- Debug
- x64
-
-
- Release
- x64
-
-
-
{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}
- DynamicLibrary
- Unicode
SilentFilesInUseBAFunction
PowerToysSetupCustomActionsVNext
bafunctions.def
@@ -33,7 +10,6 @@
-
DynamicLibrary
true
@@ -65,7 +41,10 @@
-
+
+ Use
+ precomp.h
+
Create
precomp.h
@@ -92,31 +71,5 @@
-
-
-
- _DEBUG;%(PreprocessorDefinitions)
- Disabled
- MultiThreadedDebug
-
-
- true
-
-
-
-
- NDEBUG;%(PreprocessorDefinitions)
- MaxSpeed
- MultiThreaded
- true
- true
-
-
- true
- true
- true
-
-
-
diff --git a/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunctions.cpp b/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunctions.cpp
index 9b9e5d570f..ceccde5f0d 100644
--- a/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunctions.cpp
+++ b/installer/PowerToysSetupVNext/SilentFilesInUseBA/SilentFilesInUseBAFunctions.cpp
@@ -18,7 +18,6 @@ public: // IBootstrapperApplication
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running detect begin BA function. fCached=%d, registrationType=%d, cPackages=%u, fCancel=%d", fCached, registrationType, cPackages, *pfCancel);
- LExit:
return hr;
}
@@ -32,12 +31,6 @@ public: // IBAFunctions
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION SYSTEM ACTIVE *** Running plan begin BA function. cPackages=%u, fCancel=%d", cPackages, *pfCancel);
- //-------------------------------------------------------------------------------------------------
- // YOUR CODE GOES HERE
- // BalExitOnFailure(hr, "Change this message to represent real error handling.");
- //-------------------------------------------------------------------------------------------------
-
- LExit:
return hr;
}
@@ -63,6 +56,7 @@ public: // IBAFunctions
)
{
HRESULT hr = S_OK;
+ UNREFERENCED_PARAMETER(source);
BalLog(BOOTSTRAPPER_LOG_LEVEL_STANDARD, "*** CUSTOM BA FUNCTION CALLED *** Running OnExecuteFilesInUse BA function. packageId=%ls, cFiles=%u, recommendation=%d", wzPackageId, cFiles, nRecommendation);
diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
index b6f2f88dd0..6724d95170 100644
--- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
+++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1
@@ -1,9 +1,7 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True, Position = 1)]
- [string]$platform,
- [Parameter(Mandatory = $False, Position = 2)]
- [string]$installscopeperuser = "false"
+ [string]$platform
)
Function Generate-FileList() {
@@ -77,9 +75,7 @@ Function Generate-FileComponents() {
[Parameter(Mandatory = $True, Position = 1)]
[string]$fileListName,
[Parameter(Mandatory = $True, Position = 2)]
- [string]$wxsFilePath,
- [Parameter(Mandatory = $True, Position = 3)]
- [string]$regroot
+ [string]$wxsFilePath
)
$wxsFile = Get-Content $wxsFilePath;
@@ -100,7 +96,7 @@ Function Generate-FileComponents() {
$componentDefs +=
@"
-
+
`r`n
"@
@@ -134,194 +130,194 @@ if ($platform -ceq "arm64") {
$platform = "ARM64"
}
-if ($installscopeperuser -eq "true") {
- $registryroot = "HKCU"
-} else {
- $registryroot = "HKLM"
-}
-
#BaseApplications
Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release"
-Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs
#WinUI3Applications
Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
-Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs
#AdvancedPaste
Generate-FileList -fileDepsJson "" -fileListName AdvancedPasteAssetsFiles -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\AdvancedPaste"
-Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs
#AwakeFiles
Generate-FileList -fileDepsJson "" -fileListName AwakeImagesFiles -wxsFilePath $PSScriptRoot\Awake.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Awake"
-Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs
#ColorPicker
Generate-FileList -fileDepsJson "" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker"
-Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs
#Environment Variables
Generate-FileList -fileDepsJson "" -fileListName EnvironmentVariablesAssetsFiles -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\EnvironmentVariables"
-Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs
#FileExplorerAdd-ons
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerMonacoAssetsFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco"
Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerCustomLanguagesFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco\customLanguages"
-Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs
+Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs
#FileLocksmith
Generate-FileList -fileDepsJson "" -fileListName FileLocksmithAssetsFiles -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\FileLocksmith"
-Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs
#Hosts
Generate-FileList -fileDepsJson "" -fileListName HostsAssetsFiles -wxsFilePath $PSScriptRoot\Hosts.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Hosts"
-Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs
#ImageResizer
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
+Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs
# 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
+Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs
#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
+Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs
#Peek
Generate-FileList -fileDepsJson "" -fileListName PeekAssetsFiles -wxsFilePath $PSScriptRoot\Peek.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Peek\"
-Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs
#PowerRename
Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\"
-Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs
#RegistryPreview
Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\"
-Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs
#Run
Generate-FileList -fileDepsJson "" -fileListName launcherImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\PowerLauncher"
-Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
## Plugins
###Calculator
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.deps.json" -fileListName calcComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName calcImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Images"
-Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Folder
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Microsoft.Plugin.Folder.deps.json" -fileListName FolderComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName FolderImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Images"
-Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Program
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Microsoft.Plugin.Program.deps.json" -fileListName ProgramComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ProgramImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Images"
-Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Shell
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Microsoft.Plugin.Shell.deps.json" -fileListName ShellComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ShellImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Images"
-Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Indexer
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Microsoft.Plugin.Indexer.deps.json" -fileListName IndexerComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName IndexerImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Images"
-Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###UnitConverter
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.deps.json" -fileListName UnitConvCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName UnitConvImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Images"
-Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###WebSearch
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Community.PowerToys.Run.Plugin.WebSearch.deps.json" -fileListName WebSrchCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WebSrchImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Images"
-Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###History
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Microsoft.PowerToys.Run.Plugin.History.deps.json" -fileListName HistoryPluginComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName HistoryPluginImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Images"
-Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Uri
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Microsoft.Plugin.Uri.deps.json" -fileListName UriComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName UriImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Images"
-Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###VSCodeWorkspaces
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Community.PowerToys.Run.Plugin.VSCodeWorkspaces.deps.json" -fileListName VSCWrkCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName VSCWrkImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Images"
-Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###WindowWalker
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Microsoft.Plugin.WindowWalker.deps.json" -fileListName WindowWlkrCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WindowWlkrImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Images"
-Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###OneNote
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.deps.json" -fileListName OneNoteComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName OneNoteImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Images"
-Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Registry
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Microsoft.PowerToys.Run.Plugin.Registry.deps.json" -fileListName RegistryComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName RegistryImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Images"
-Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###Service
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Microsoft.PowerToys.Run.Plugin.Service.deps.json" -fileListName ServiceComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ServiceImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Images"
-Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###System
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Microsoft.PowerToys.Run.Plugin.System.deps.json" -fileListName SystemComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName SystemImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Images"
-Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###TimeDate
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Microsoft.PowerToys.Run.Plugin.TimeDate.deps.json" -fileListName TimeDateComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName TimeDateImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Images"
-Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###WindowsSettings
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.deps.json" -fileListName WinSetCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WinSetImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Images"
-Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###WindowsTerminal
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.deps.json" -fileListName WinTermCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName WinTermImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Images"
-Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###PowerToys
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.deps.json" -fileListName PowerToysCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName PowerToysImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Images"
-Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
###ValueGenerator
Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Community.PowerToys.Run.Plugin.ValueGenerator.deps.json" -fileListName ValueGeneratorCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1
Generate-FileList -fileDepsJson "" -fileListName ValueGeneratorImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Images"
-Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
+Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs
## Plugins
#ShortcutGuide
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideSvgFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ShortcutGuide\"
-Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs
#Settings
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\OOBE\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsFluentIconsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\"
-Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
-Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot
+Generate-FileList -fileDepsJson "" -fileListName SettingsV2IconsModelsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\Models\"
+Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
+Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
+Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
+Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
+Generate-FileComponents -fileListName "SettingsV2IconsModelsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
#Workspaces
Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\"
-Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs -regroot $registryroot
+Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs
+
+#DSC Resources - JSON manifest files in DSCModules subfolder
+Generate-FileList -fileDepsJson "" -fileListName DscJsonFiles -wxsFilePath $PSScriptRoot\DscResources.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\DSCModules\"
+Generate-FileComponents -fileListName "DscJsonFiles" -wxsFilePath $PSScriptRoot\DscResources.wxs
diff --git a/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1 b/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1
deleted file mode 100644
index 14172db0bc..0000000000
--- a/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1
+++ /dev/null
@@ -1,102 +0,0 @@
-[CmdletBinding()]
-Param(
- [Parameter(Mandatory = $True)]
- [string]$dscWxsFile,
- [Parameter(Mandatory = $True)]
- [string]$Platform,
- [Parameter(Mandatory = $True)]
- [string]$Configuration
-)
-
-$ErrorActionPreference = 'Stop'
-
-$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
-
-# Find build output directory
-$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration"
-
-if (-not (Test-Path $buildOutputDir)) {
- Write-Error "Build output directory not found: '$buildOutputDir'"
- exit 1
-}
-
-# Find all DSC manifest JSON files
-$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File
-
-if (-not $dscFiles) {
- Write-Warning "No DSC manifest files found in '$buildOutputDir'"
- # Create empty component group
- $wxsContent = @"
-
-
-
-
-
-
-
-
-
-"@
- Set-Content -Path $dscWxsFile -Value $wxsContent
- exit 0
-}
-
-Write-Host "Found $($dscFiles.Count) DSC manifest file(s)"
-
-# Generate WiX fragment
-$wxsContent = @"
-
-
-
-
-
-
-"@
-
-$componentRefs = @()
-
-foreach ($file in $dscFiles) {
- $componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_')
- $fileId = $componentId + "_File"
- $guid = [System.Guid]::NewGuid().ToString().ToUpper()
-
- $componentRefs += $componentId
-
- $wxsContent += @"
-
-
-
-
-
-
-
-"@
-}
-
-$wxsContent += @"
-
-
-
-
-
-
-"@
-
-foreach ($componentId in $componentRefs) {
- $wxsContent += @"
-
-
-"@
-}
-
-$wxsContent += @"
-
-
-
-
-"@
-
-# Write the WiX file
-Set-Content -Path $dscWxsFile -Value $wxsContent
-
-Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'"
\ No newline at end of file
diff --git a/src/PackageIdentity/AppxManifest.xml b/src/PackageIdentity/AppxManifest.xml
index af637229ca..2e9d52a2fa 100644
--- a/src/PackageIdentity/AppxManifest.xml
+++ b/src/PackageIdentity/AppxManifest.xml
@@ -36,7 +36,7 @@
-
+
-
+
-
+
-
diff --git a/src/common/CalculatorEngineCommon/CalculatorEngineCommon.vcxproj b/src/common/CalculatorEngineCommon/CalculatorEngineCommon.vcxproj
index 43f4749892..ff9332cfc0 100644
--- a/src/common/CalculatorEngineCommon/CalculatorEngineCommon.vcxproj
+++ b/src/common/CalculatorEngineCommon/CalculatorEngineCommon.vcxproj
@@ -9,12 +9,6 @@
CalculatorEngineCommon
false
-
-
- true
- false
-
-
true
@@ -25,11 +19,9 @@
true
- true
+ false
true
Windows Store
- false
-
@@ -148,43 +140,5 @@
-
-
-
-
-
-
- MultiThreadedDebug
- stdcpp17
-
-
-
- %(IgnoreSpecificDefaultLibraries);libucrtd.lib
- %(AdditionalOptions) /defaultlib:ucrtd.lib
-
-
-
-
-
- MultiThreaded
-
-
-
- %(IgnoreSpecificDefaultLibraries);libucrt.lib
- %(AdditionalOptions) /defaultlib:ucrt.lib
-
-
-
+
\ No newline at end of file
diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp
index 361255f66f..52c91a0795 100644
--- a/src/common/GPOWrapper/GPOWrapper.cpp
+++ b/src/common/GPOWrapper/GPOWrapper.cpp
@@ -112,6 +112,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast(powertoys_gpo::getConfiguredMousePointerCrosshairsEnabledValue());
}
+ GpoRuleConfigured GPOWrapper::GetConfiguredCursorWrapEnabledValue()
+ {
+ return static_cast(powertoys_gpo::getConfiguredCursorWrapEnabledValue());
+ }
GpoRuleConfigured GPOWrapper::GetConfiguredPowerRenameEnabledValue()
{
return static_cast(powertoys_gpo::getConfiguredPowerRenameEnabledValue());
@@ -192,6 +196,38 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast(powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue());
}
+ GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteOpenAIValue()
+ {
+ return static_cast(powertoys_gpo::getAllowedAdvancedPasteOpenAIValue());
+ }
+ GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteAzureOpenAIValue()
+ {
+ return static_cast(powertoys_gpo::getAllowedAdvancedPasteAzureOpenAIValue());
+ }
+ GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteAzureAIInferenceValue()
+ {
+ return static_cast(powertoys_gpo::getAllowedAdvancedPasteAzureAIInferenceValue());
+ }
+ GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteMistralValue()
+ {
+ return static_cast(powertoys_gpo::getAllowedAdvancedPasteMistralValue());
+ }
+ GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteGoogleValue()
+ {
+ return static_cast(powertoys_gpo::getAllowedAdvancedPasteGoogleValue());
+ }
+ GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteAnthropicValue()
+ {
+ return static_cast(powertoys_gpo::getAllowedAdvancedPasteAnthropicValue());
+ }
+ GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteOllamaValue()
+ {
+ return static_cast(powertoys_gpo::getAllowedAdvancedPasteOllamaValue());
+ }
+ GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteFoundryLocalValue()
+ {
+ return static_cast(powertoys_gpo::getAllowedAdvancedPasteFoundryLocalValue());
+ }
GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusEnabledValue()
{
return static_cast(powertoys_gpo::getConfiguredNewPlusEnabledValue());
diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h
index c0fff9f542..846fba0a61 100644
--- a/src/common/GPOWrapper/GPOWrapper.h
+++ b/src/common/GPOWrapper/GPOWrapper.h
@@ -35,6 +35,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue();
static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue();
static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue();
+ static GpoRuleConfigured GetConfiguredCursorWrapEnabledValue();
static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue();
static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue();
static GpoRuleConfigured GetConfiguredQuickAccentEnabledValue();
@@ -54,6 +55,14 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteOpenAIValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteAzureOpenAIValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteAzureAIInferenceValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteMistralValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteGoogleValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteAnthropicValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteOllamaValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteFoundryLocalValue();
static GpoRuleConfigured GetConfiguredNewPlusEnabledValue();
static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue();
static GpoRuleConfigured GetConfiguredMwbClipboardSharingEnabledValue();
diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl
index 630beab9c9..ab6dbf0c29 100644
--- a/src/common/GPOWrapper/GPOWrapper.idl
+++ b/src/common/GPOWrapper/GPOWrapper.idl
@@ -38,6 +38,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue();
static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue();
static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue();
+ static GpoRuleConfigured GetConfiguredCursorWrapEnabledValue();
static GpoRuleConfigured GetConfiguredMouseWithoutBordersEnabledValue();
static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue();
static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue();
@@ -58,6 +59,14 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteOpenAIValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteAzureOpenAIValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteAzureAIInferenceValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteMistralValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteGoogleValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteAnthropicValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteOllamaValue();
+ static GpoRuleConfigured GetAllowedAdvancedPasteFoundryLocalValue();
static GpoRuleConfigured GetConfiguredNewPlusEnabledValue();
static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue();
static GpoRuleConfigured GetConfiguredMwbClipboardSharingEnabledValue();
diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryCachedModel.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryCachedModel.cs
new file mode 100644
index 0000000000..489a779179
--- /dev/null
+++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryCachedModel.cs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace LanguageModelProvider.FoundryLocal;
+
+internal sealed record FoundryCachedModel(string Name, string? Id);
diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryCatalogModel.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryCatalogModel.cs
new file mode 100644
index 0000000000..413bb47316
--- /dev/null
+++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryCatalogModel.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+
+namespace LanguageModelProvider.FoundryLocal;
+
+internal sealed record FoundryCatalogModel
+{
+ [JsonPropertyName("name")]
+ public string Name { get; init; } = string.Empty;
+
+ [JsonPropertyName("displayName")]
+ public string DisplayName { get; init; } = string.Empty;
+
+ [JsonPropertyName("providerType")]
+ public string ProviderType { get; init; } = string.Empty;
+
+ [JsonPropertyName("uri")]
+ public string Uri { get; init; } = string.Empty;
+
+ [JsonPropertyName("version")]
+ public string Version { get; init; } = string.Empty;
+
+ [JsonPropertyName("modelType")]
+ public string ModelType { get; init; } = string.Empty;
+
+ [JsonPropertyName("promptTemplate")]
+ public PromptTemplate PromptTemplate { get; init; } = default!;
+
+ [JsonPropertyName("publisher")]
+ public string Publisher { get; init; } = string.Empty;
+
+ [JsonPropertyName("task")]
+ public string Task { get; init; } = string.Empty;
+
+ [JsonPropertyName("runtime")]
+ public Runtime Runtime { get; init; } = default!;
+
+ [JsonPropertyName("fileSizeMb")]
+ public long FileSizeMb { get; init; }
+
+ [JsonPropertyName("modelSettings")]
+ public ModelSettings ModelSettings { get; init; } = default!;
+
+ [JsonPropertyName("alias")]
+ public string Alias { get; init; } = string.Empty;
+
+ [JsonPropertyName("supportsToolCalling")]
+ public bool SupportsToolCalling { get; init; }
+
+ [JsonPropertyName("license")]
+ public string License { get; init; } = string.Empty;
+
+ [JsonPropertyName("licenseDescription")]
+ public string LicenseDescription { get; init; } = string.Empty;
+
+ [JsonPropertyName("parentModelUri")]
+ public string ParentModelUri { get; init; } = string.Empty;
+}
diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs
new file mode 100644
index 0000000000..84c38c49f2
--- /dev/null
+++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs
@@ -0,0 +1,208 @@
+// 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 ManagedCommon;
+using Microsoft.AI.Foundry.Local;
+
+namespace LanguageModelProvider.FoundryLocal;
+
+internal sealed class FoundryClient
+{
+ public static async Task CreateAsync()
+ {
+ try
+ {
+ Logger.LogInfo("[FoundryClient] Creating Foundry Local client");
+
+ var manager = new FoundryLocalManager();
+
+ // Check if service is already running
+ if (manager.IsServiceRunning)
+ {
+ Logger.LogInfo("[FoundryClient] Foundry service is already running");
+ return new FoundryClient(manager);
+ }
+
+ // Start the service using SDK's method
+ Logger.LogInfo("[FoundryClient] Starting Foundry service using manager.StartServiceAsync()");
+ await manager.StartServiceAsync().ConfigureAwait(false);
+
+ Logger.LogInfo("[FoundryClient] Foundry service started successfully");
+ return new FoundryClient(manager);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[FoundryClient] Error creating client: {ex.Message}");
+ if (ex.InnerException != null)
+ {
+ Logger.LogError($"[FoundryClient] Inner exception: {ex.InnerException.Message}");
+ }
+
+ return null;
+ }
+ }
+
+ private readonly FoundryLocalManager _foundryManager;
+ private readonly List _catalogModels = [];
+
+ private FoundryClient(FoundryLocalManager foundryManager)
+ {
+ _foundryManager = foundryManager;
+ }
+
+ public Task GetServiceUrl()
+ {
+ try
+ {
+ return Task.FromResult(_foundryManager.Endpoint?.ToString());
+ }
+ catch
+ {
+ return Task.FromResult(null);
+ }
+ }
+
+ public Uri? GetServiceUri()
+ {
+ try
+ {
+ return _foundryManager.ServiceUri;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public async Task> ListCatalogModels()
+ {
+ if (_catalogModels.Count > 0)
+ {
+ return _catalogModels;
+ }
+
+ try
+ {
+ Logger.LogInfo("[FoundryClient] Listing catalog models");
+ var models = await _foundryManager.ListCatalogModelsAsync().ConfigureAwait(false);
+
+ if (models != null)
+ {
+ foreach (var model in models)
+ {
+ _catalogModels.Add(new FoundryCatalogModel
+ {
+ Name = model.ModelId ?? string.Empty,
+ DisplayName = model.DisplayName ?? string.Empty,
+ ProviderType = model.ProviderType ?? string.Empty,
+ Uri = model.Uri ?? string.Empty,
+ Version = model.Version ?? string.Empty,
+ ModelType = model.ModelType ?? string.Empty,
+ Publisher = model.Publisher ?? string.Empty,
+ Task = model.Task ?? string.Empty,
+ FileSizeMb = model.FileSizeMb,
+ Alias = model.Alias ?? string.Empty,
+ License = model.License ?? string.Empty,
+ LicenseDescription = model.LicenseDescription ?? string.Empty,
+ ParentModelUri = model.ParentModelUri ?? string.Empty,
+ SupportsToolCalling = model.SupportsToolCalling,
+ });
+ }
+
+ Logger.LogInfo($"[FoundryClient] Found {_catalogModels.Count} catalog models");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[FoundryClient] Error listing catalog models: {ex.Message}");
+
+ // Surfacing errors here prevents listing other providers; swallow and return cached list instead.
+ }
+
+ return _catalogModels;
+ }
+
+ public async Task> ListCachedModels()
+ {
+ try
+ {
+ Logger.LogInfo("[FoundryClient] Listing cached models");
+ var cachedModels = await _foundryManager.ListCachedModelsAsync().ConfigureAwait(false);
+ var catalogModels = await ListCatalogModels().ConfigureAwait(false);
+
+ List models = [];
+
+ foreach (var model in cachedModels)
+ {
+ var catalogModel = catalogModels.FirstOrDefault(m => m.Name == model.ModelId);
+ var alias = catalogModel?.Alias ?? model.Alias;
+ models.Add(new FoundryCachedModel(model.ModelId ?? string.Empty, alias));
+ }
+
+ Logger.LogInfo($"[FoundryClient] Found {models.Count} cached models");
+ return models;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[FoundryClient] Error listing cached models: {ex.Message}");
+ return [];
+ }
+ }
+
+ public async Task IsModelLoaded(string modelId)
+ {
+ try
+ {
+ var loadedModels = await _foundryManager.ListLoadedModelsAsync().ConfigureAwait(false);
+ var isLoaded = loadedModels.Any(m => m.ModelId == modelId);
+ Logger.LogInfo($"[FoundryClient] IsModelLoaded({modelId}): {isLoaded}");
+ Logger.LogInfo($"[FoundryClient] Loaded models: {string.Join(", ", loadedModels.Select(m => m.ModelId))}");
+ return isLoaded;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[FoundryClient] IsModelLoaded exception: {ex.Message}");
+ return false;
+ }
+ }
+
+ public async Task EnsureModelLoaded(string modelId)
+ {
+ try
+ {
+ Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}");
+
+ // Check if already loaded
+ if (await IsModelLoaded(modelId).ConfigureAwait(false))
+ {
+ Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}");
+ return true;
+ }
+
+ // Check if model exists in cache
+ var cachedModels = await ListCachedModels().ConfigureAwait(false);
+ Logger.LogInfo($"[FoundryClient] Cached models: {string.Join(", ", cachedModels.Select(m => m.Name))}");
+
+ if (!cachedModels.Any(m => m.Name == modelId))
+ {
+ Logger.LogWarning($"[FoundryClient] Model not found in cache: {modelId}");
+ return false;
+ }
+
+ // Load the model
+ Logger.LogInfo($"[FoundryClient] Loading model: {modelId}");
+ await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false);
+
+ // Verify it's loaded
+ var loaded = await IsModelLoaded(modelId).ConfigureAwait(false);
+ Logger.LogInfo($"[FoundryClient] Model load result: {loaded}");
+ return loaded;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[FoundryClient] EnsureModelLoaded exception: {ex.Message}");
+ return false;
+ }
+ }
+}
diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryJsonContext.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryJsonContext.cs
new file mode 100644
index 0000000000..5dcb4076ed
--- /dev/null
+++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryJsonContext.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace LanguageModelProvider.FoundryLocal;
+
+[JsonSourceGenerationOptions(
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ WriteIndented = false)]
+[JsonSerializable(typeof(FoundryCatalogModel))]
+[JsonSerializable(typeof(List))]
+internal sealed partial class FoundryJsonContext : JsonSerializerContext
+{
+}
diff --git a/src/common/LanguageModelProvider/FoundryLocal/ModelSettings.cs b/src/common/LanguageModelProvider/FoundryLocal/ModelSettings.cs
new file mode 100644
index 0000000000..fda91217eb
--- /dev/null
+++ b/src/common/LanguageModelProvider/FoundryLocal/ModelSettings.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace LanguageModelProvider.FoundryLocal;
+
+internal sealed record ModelSettings
+{
+ // The sample shows an empty array; keep it open-ended.
+ [JsonPropertyName("parameters")]
+ public List Parameters { get; init; } = [];
+}
diff --git a/src/common/LanguageModelProvider/FoundryLocal/PromptTemplate.cs b/src/common/LanguageModelProvider/FoundryLocal/PromptTemplate.cs
new file mode 100644
index 0000000000..a2cbb9fe45
--- /dev/null
+++ b/src/common/LanguageModelProvider/FoundryLocal/PromptTemplate.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+
+namespace LanguageModelProvider.FoundryLocal;
+
+internal sealed record PromptTemplate
+{
+ [JsonPropertyName("assistant")]
+ public string Assistant { get; init; } = string.Empty;
+
+ [JsonPropertyName("prompt")]
+ public string Prompt { get; init; } = string.Empty;
+}
diff --git a/src/common/LanguageModelProvider/FoundryLocal/Runtime.cs b/src/common/LanguageModelProvider/FoundryLocal/Runtime.cs
new file mode 100644
index 0000000000..e2019c8f87
--- /dev/null
+++ b/src/common/LanguageModelProvider/FoundryLocal/Runtime.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Text.Json.Serialization;
+
+namespace LanguageModelProvider.FoundryLocal;
+
+internal sealed record Runtime
+{
+ [JsonPropertyName("deviceType")]
+ public string DeviceType { get; init; } = string.Empty;
+
+ [JsonPropertyName("executionProvider")]
+ public string ExecutionProvider { get; init; } = string.Empty;
+}
diff --git a/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs b/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs
new file mode 100644
index 0000000000..b866ab05d9
--- /dev/null
+++ b/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs
@@ -0,0 +1,185 @@
+// 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.ClientModel;
+using LanguageModelProvider.FoundryLocal;
+using ManagedCommon;
+using Microsoft.Extensions.AI;
+using OpenAI;
+
+namespace LanguageModelProvider;
+
+public sealed class FoundryLocalModelProvider : ILanguageModelProvider
+{
+ private IEnumerable? _downloadedModels;
+ private FoundryClient? _foundryManager;
+ private string? _serviceUrl;
+
+ public static FoundryLocalModelProvider Instance { get; } = new();
+
+ public string Name => "FoundryLocal";
+
+ public string ProviderDescription => "The model will run locally via Foundry Local";
+
+ public string UrlPrefix => "fl://";
+
+ public IChatClient? GetIChatClient(string url)
+ {
+ try
+ {
+ Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {url}");
+ InitializeAsync().GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[FoundryLocal] Failed to initialize: {ex.Message}");
+ return null;
+ }
+
+ if (string.IsNullOrWhiteSpace(_serviceUrl) || _foundryManager == null)
+ {
+ Logger.LogError("[FoundryLocal] Service URL or manager is null");
+ return null;
+ }
+
+ // Extract model ID from URL (format: fl://modelname)
+ var modelId = url.Replace(UrlPrefix, string.Empty).Trim('/');
+ if (string.IsNullOrWhiteSpace(modelId))
+ {
+ Logger.LogError("[FoundryLocal] Model ID is empty after extraction");
+ return null;
+ }
+
+ Logger.LogInfo($"[FoundryLocal] Extracted model ID: {modelId}");
+
+ // Ensure the model is loaded before returning chat client
+ try
+ {
+ var isLoaded = _foundryManager.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
+ if (!isLoaded)
+ {
+ Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
+ return null;
+ }
+
+ Logger.LogInfo($"[FoundryLocal] Model is loaded: {modelId}");
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"[FoundryLocal] Exception ensuring model loaded: {ex.Message}");
+ return null;
+ }
+
+ // Use ServiceUri instead of Endpoint since Endpoint already includes /v1
+ var baseUri = _foundryManager.GetServiceUri();
+ if (baseUri == null)
+ {
+ Logger.LogError("[FoundryLocal] Service URI is null");
+ return null;
+ }
+
+ var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1");
+ Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}");
+ Logger.LogInfo($"[FoundryLocal] Model ID for chat client: {modelId}");
+
+ return new OpenAIClient(
+ new ApiKeyCredential("none"),
+ new OpenAIClientOptions { Endpoint = endpointUri })
+ .GetChatClient(modelId)
+ .AsIChatClient();
+ }
+
+ public string GetIChatClientString(string url)
+ {
+ try
+ {
+ InitializeAsync().GetAwaiter().GetResult();
+ }
+ catch
+ {
+ return string.Empty;
+ }
+
+ var modelId = url.Split('/').LastOrDefault();
+
+ if (string.IsNullOrWhiteSpace(_serviceUrl) || string.IsNullOrWhiteSpace(modelId))
+ {
+ return string.Empty;
+ }
+
+ return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
+ }
+
+ public async Task> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
+ {
+ if (ignoreCached)
+ {
+ Logger.LogInfo("[FoundryLocal] Ignoring cached models, resetting");
+ Reset();
+ }
+
+ await InitializeAsync(cancelationToken);
+
+ Logger.LogInfo($"[FoundryLocal] Returning {_downloadedModels?.Count() ?? 0} downloaded models");
+ return _downloadedModels ?? [];
+ }
+
+ private void Reset()
+ {
+ _downloadedModels = null;
+ _ = InitializeAsync();
+ }
+
+ private async Task InitializeAsync(CancellationToken cancelationToken = default)
+ {
+ if (_foundryManager != null && _downloadedModels != null && _downloadedModels.Any())
+ {
+ return;
+ }
+
+ Logger.LogInfo("[FoundryLocal] Initializing provider");
+ _foundryManager ??= await FoundryClient.CreateAsync();
+
+ if (_foundryManager == null)
+ {
+ Logger.LogError("[FoundryLocal] Failed to create Foundry client");
+ return;
+ }
+
+ _serviceUrl ??= await _foundryManager.GetServiceUrl();
+ Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
+
+ var cachedModels = await _foundryManager.ListCachedModels();
+ Logger.LogInfo($"[FoundryLocal] Found {cachedModels.Count} cached models");
+
+ List downloadedModels = [];
+
+ foreach (var model in cachedModels)
+ {
+ Logger.LogInfo($"[FoundryLocal] Adding unmatched cached model: {model.Name}");
+ downloadedModels.Add(new ModelDetails
+ {
+ Id = $"fl-{model.Name}",
+ Name = model.Name,
+ Url = $"{UrlPrefix}{model.Name}",
+ Description = $"{model.Name} running locally with Foundry Local",
+ HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
+ SupportedOnQualcomm = true,
+ ProviderModelDetails = model,
+ });
+ }
+
+ _downloadedModels = downloadedModels;
+ Logger.LogInfo($"[FoundryLocal] Initialization complete. Total downloaded models: {downloadedModels.Count}");
+ }
+
+ public async Task IsAvailable()
+ {
+ Logger.LogInfo("[FoundryLocal] Checking availability");
+ await InitializeAsync();
+ var available = _foundryManager != null;
+ Logger.LogInfo($"[FoundryLocal] Available: {available}");
+ return available;
+ }
+}
diff --git a/src/common/LanguageModelProvider/HardwareAccelerator.cs b/src/common/LanguageModelProvider/HardwareAccelerator.cs
new file mode 100644
index 0000000000..d2c94b8155
--- /dev/null
+++ b/src/common/LanguageModelProvider/HardwareAccelerator.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace LanguageModelProvider;
+
+public enum HardwareAccelerator
+{
+ CPU,
+ DML,
+ QNN,
+ WCRAPI,
+ OLLAMA,
+ OPENAI,
+ FOUNDRYLOCAL,
+ LEMONADE,
+ NPU,
+ GPU,
+ VitisAI,
+ OpenVINO,
+ NvTensorRT,
+}
diff --git a/src/common/LanguageModelProvider/ILanguageModelProvider.cs b/src/common/LanguageModelProvider/ILanguageModelProvider.cs
new file mode 100644
index 0000000000..60b3d99386
--- /dev/null
+++ b/src/common/LanguageModelProvider/ILanguageModelProvider.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.AI;
+
+namespace LanguageModelProvider;
+
+public interface ILanguageModelProvider
+{
+ string Name { get; }
+
+ string UrlPrefix { get; }
+
+ string ProviderDescription { get; }
+
+ Task> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
+
+ IChatClient? GetIChatClient(string url);
+
+ string GetIChatClientString(string url);
+}
diff --git a/src/common/LanguageModelProvider/LanguageModelProvider.csproj b/src/common/LanguageModelProvider/LanguageModelProvider.csproj
new file mode 100644
index 0000000000..4dba9247a3
--- /dev/null
+++ b/src/common/LanguageModelProvider/LanguageModelProvider.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/common/LanguageModelProvider/LanguageModelService.cs b/src/common/LanguageModelProvider/LanguageModelService.cs
new file mode 100644
index 0000000000..1cfb3b3c49
--- /dev/null
+++ b/src/common/LanguageModelProvider/LanguageModelService.cs
@@ -0,0 +1,106 @@
+// 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.Concurrent;
+using Microsoft.Extensions.AI;
+
+namespace LanguageModelProvider;
+
+public sealed class LanguageModelService
+{
+ private readonly ConcurrentDictionary _providersByPrefix;
+
+ public LanguageModelService(IEnumerable providers)
+ {
+ ArgumentNullException.ThrowIfNull(providers);
+
+ _providersByPrefix = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var provider in providers)
+ {
+ if (!string.IsNullOrWhiteSpace(provider.UrlPrefix))
+ {
+ _providersByPrefix[provider.UrlPrefix] = provider;
+ }
+ }
+ }
+
+ public static LanguageModelService CreateDefault()
+ {
+ return new LanguageModelService(new[]
+ {
+ FoundryLocalModelProvider.Instance,
+ });
+ }
+
+ public IReadOnlyCollection Providers => _providersByPrefix.Values.ToArray();
+
+ public bool RegisterProvider(ILanguageModelProvider provider)
+ {
+ ArgumentNullException.ThrowIfNull(provider);
+
+ if (string.IsNullOrWhiteSpace(provider.UrlPrefix))
+ {
+ throw new ArgumentException("Provider must supply a URL prefix.", nameof(provider));
+ }
+
+ _providersByPrefix[provider.UrlPrefix] = provider;
+ return true;
+ }
+
+ public ILanguageModelProvider? GetProviderFor(string? modelReference)
+ {
+ if (string.IsNullOrWhiteSpace(modelReference))
+ {
+ return null;
+ }
+
+ foreach (var provider in _providersByPrefix.Values)
+ {
+ if (modelReference.StartsWith(provider.UrlPrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return provider;
+ }
+ }
+
+ return null;
+ }
+
+ public async Task> GetModelsAsync(bool refresh = false, CancellationToken cancellationToken = default)
+ {
+ List models = [];
+
+ foreach (var provider in _providersByPrefix.Values)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var providerModels = await provider.GetModelsAsync(refresh, cancellationToken).ConfigureAwait(false);
+ models.AddRange(providerModels);
+ }
+
+ return models;
+ }
+
+ public IChatClient? GetClient(ModelDetails model)
+ {
+ if (model is null)
+ {
+ return null;
+ }
+
+ var reference = !string.IsNullOrWhiteSpace(model.Url) ? model.Url : model.Id;
+ return GetClient(reference);
+ }
+
+ public IChatClient? GetClient(string? modelReference)
+ {
+ if (string.IsNullOrWhiteSpace(modelReference))
+ {
+ return null;
+ }
+
+ var provider = GetProviderFor(modelReference);
+
+ return provider?.GetIChatClient(modelReference);
+ }
+}
diff --git a/src/common/LanguageModelProvider/ModelDetails.cs b/src/common/LanguageModelProvider/ModelDetails.cs
new file mode 100644
index 0000000000..2e68ca6feb
--- /dev/null
+++ b/src/common/LanguageModelProvider/ModelDetails.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.Collections.Generic;
+
+namespace LanguageModelProvider;
+
+public class ModelDetails
+{
+ public string Id { get; set; } = string.Empty;
+
+ public string Name { get; set; } = string.Empty;
+
+ public string Url { get; set; } = string.Empty;
+
+ public string Description { get; set; } = string.Empty;
+
+ public long Size { get; set; }
+
+ public bool IsUserAdded { get; set; }
+
+ public string Icon { get; set; } = string.Empty;
+
+ public List HardwareAccelerators { get; set; } = [];
+
+ public bool SupportedOnQualcomm { get; set; }
+
+ public string License { get; set; } = string.Empty;
+
+ public object? ProviderModelDetails { get; set; }
+}
diff --git a/src/common/ManagedCommon/Logger.cs b/src/common/ManagedCommon/Logger.cs
index 11115b1846..1173920340 100644
--- a/src/common/ManagedCommon/Logger.cs
+++ b/src/common/ManagedCommon/Logger.cs
@@ -26,6 +26,16 @@ namespace ManagedCommon
private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "Unknown";
+ ///
+ /// Gets the path to the log directory for the current version of the app.
+ ///
+ public static string CurrentVersionLogDirectoryPath { get; private set; }
+
+ ///
+ /// Gets the path to the log directory for the app.
+ ///
+ public static string AppLogDirectoryPath { get; private set; }
+
///
/// Initializes the logger and sets the path for logging.
///
@@ -42,6 +52,9 @@ namespace ManagedCommon
Directory.CreateDirectory(versionedPath);
}
+ AppLogDirectoryPath = basePath;
+ CurrentVersionLogDirectoryPath = versionedPath;
+
var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log");
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
@@ -130,7 +143,7 @@ namespace ManagedCommon
{
exMessage +=
"Inner exception: " + Environment.NewLine +
- ex.InnerException.GetType() + " (" + ex.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
+ ex.InnerException.GetType() + " (" + ex.InnerException.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
}
exMessage +=
diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs
index aa741e2f3a..d7ae386191 100644
--- a/src/common/ManagedCommon/ModuleType.cs
+++ b/src/common/ManagedCommon/ModuleType.cs
@@ -12,6 +12,7 @@ namespace ManagedCommon
ColorPicker,
CmdPal,
CropAndLock,
+ CursorWrap,
EnvironmentVariables,
FancyZones,
FileLocksmith,
diff --git a/src/common/UITestAutomation/SettingsConfigHelper.cs b/src/common/UITestAutomation/SettingsConfigHelper.cs
new file mode 100644
index 0000000000..833ec4f19d
--- /dev/null
+++ b/src/common/UITestAutomation/SettingsConfigHelper.cs
@@ -0,0 +1,175 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
+using Microsoft.PowerToys.Settings.UI.Library.Utilities;
+
+namespace Microsoft.PowerToys.UITest
+{
+ ///
+ /// Helper class for configuring PowerToys settings for UI tests.
+ ///
+ public class SettingsConfigHelper
+ {
+ private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true };
+ private static readonly SettingsUtils SettingsUtils = new SettingsUtils();
+
+ ///
+ /// Configures global PowerToys settings to enable only specified modules and disable all others.
+ ///
+ /// Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.
+ /// Thrown when modulesToEnable is null.
+ /// Thrown when settings file operations fail.
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
+ [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
+ public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable)
+ {
+ ArgumentNullException.ThrowIfNull(modulesToEnable);
+
+ try
+ {
+ GeneralSettings settings;
+ try
+ {
+ settings = SettingsUtils.GetSettingsOrDefault();
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"Failed to load settings, creating defaults: {ex.Message}");
+ settings = new GeneralSettings();
+ }
+
+ string settingsJson = settings.ToJsonString();
+ using (JsonDocument doc = JsonDocument.Parse(settingsJson))
+ {
+ var options = new JsonSerializerOptions { WriteIndented = true };
+ var root = doc.RootElement.Clone();
+
+ if (root.TryGetProperty("enabled", out var enabledElement))
+ {
+ var enabledModules = new Dictionary();
+
+ foreach (var property in enabledElement.EnumerateObject())
+ {
+ string moduleName = property.Name;
+
+ bool shouldEnable = Array.Exists(modulesToEnable, m => string.Equals(m, moduleName, StringComparison.Ordinal));
+ enabledModules[moduleName] = shouldEnable;
+ }
+
+ var settingsDict = JsonSerializer.Deserialize>(settingsJson);
+ if (settingsDict != null)
+ {
+ settingsDict["enabled"] = enabledModules;
+ settingsJson = JsonSerializer.Serialize(settingsDict, IndentedJsonOptions);
+ }
+ }
+ }
+
+ SettingsUtils.SaveSettings(settingsJson);
+
+ string enabledList = modulesToEnable.Length > 0 ? string.Join(", ", modulesToEnable) : "none";
+ Debug.WriteLine($"Successfully updated global settings");
+ Debug.WriteLine($"Enabled modules: {enabledList}");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR in ConfigureGlobalModuleSettings: {ex.Message}");
+ throw new InvalidOperationException($"Failed to configure global module settings: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Updates a module's settings file. If the file doesn't exist, creates it with default content.
+ /// If the file exists, reads it and applies the provided update function to modify the settings.
+ ///
+ /// The name of the module (e.g., "Peek", "FancyZones").
+ /// The default JSON content to use if the settings file doesn't exist.
+ ///
+ /// A callback function that modifies the settings dictionary. The function receives the deserialized settings
+ /// and should modify it in-place. The function should accept a Dictionary<string, object> and not return a value.
+ /// Example: (settings) => { ((Dictionary<string, object>)settings["properties"])["SomeSetting"] = newValue; }
+ ///
+ /// Thrown when moduleName or updateSettingsAction is null.
+ /// Thrown when settings file operations fail.
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")]
+ [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")]
+ public static void UpdateModuleSettings(
+ string moduleName,
+ string defaultSettingsContent,
+ Action> updateSettingsAction)
+ {
+ ArgumentNullException.ThrowIfNull(moduleName);
+ ArgumentNullException.ThrowIfNull(updateSettingsAction);
+
+ try
+ {
+ // Build the path to the module settings file
+ string powerToysSettingsDirectory = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "Microsoft",
+ "PowerToys");
+
+ string moduleDirectory = Path.Combine(powerToysSettingsDirectory, moduleName);
+ string settingsPath = Path.Combine(moduleDirectory, "settings.json");
+
+ // Ensure directory exists
+ Directory.CreateDirectory(moduleDirectory);
+
+ // Read existing settings or use default
+ string existingJson = string.Empty;
+ if (File.Exists(settingsPath))
+ {
+ existingJson = File.ReadAllText(settingsPath);
+ }
+
+ Dictionary? settings;
+
+ // If file doesn't exist or is empty, create from defaults
+ if (string.IsNullOrWhiteSpace(existingJson))
+ {
+ if (string.IsNullOrWhiteSpace(defaultSettingsContent))
+ {
+ throw new ArgumentException("Default settings content must be provided when file doesn't exist.", nameof(defaultSettingsContent));
+ }
+
+ settings = JsonSerializer.Deserialize>(defaultSettingsContent)
+ ?? throw new InvalidOperationException($"Failed to deserialize default settings for {moduleName}");
+
+ Debug.WriteLine($"Created default settings for {moduleName} at {settingsPath}");
+ }
+ else
+ {
+ // Parse existing settings
+ settings = JsonSerializer.Deserialize>(existingJson)
+ ?? throw new InvalidOperationException($"Failed to deserialize existing settings for {moduleName}");
+
+ Debug.WriteLine($"Loaded existing settings for {moduleName} from {settingsPath}");
+ }
+
+ // Apply the update action to modify settings
+ updateSettingsAction(settings);
+
+ // Serialize and save the updated settings using SettingsUtils
+ string updatedJson = JsonSerializer.Serialize(settings, IndentedJsonOptions);
+ SettingsUtils.SaveSettings(updatedJson, moduleName);
+
+ Debug.WriteLine($"Successfully updated settings for {moduleName}");
+ }
+ catch (Exception ex)
+ {
+ Debug.WriteLine($"ERROR in UpdateModuleSettings for {moduleName}: {ex.Message}");
+ throw new InvalidOperationException($"Failed to update settings for {moduleName}: {ex.Message}", ex);
+ }
+ }
+ }
+}
diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj
index add7acfeb9..549b8a430b 100644
--- a/src/common/UITestAutomation/UITestAutomation.csproj
+++ b/src/common/UITestAutomation/UITestAutomation.csproj
@@ -8,7 +8,7 @@
enable
true
true
- net9.0-windows10.0.22621.0
+ net9.0-windows10.0.26100.0
true
false
@@ -21,4 +21,8 @@
+
+
+
+
diff --git a/src/common/interop/PowerToys.Interop.vcxproj b/src/common/interop/PowerToys.Interop.vcxproj
index ca29e69cce..472119925e 100644
--- a/src/common/interop/PowerToys.Interop.vcxproj
+++ b/src/common/interop/PowerToys.Interop.vcxproj
@@ -63,14 +63,12 @@
- MultiThreadedDebug
true
true
- MultiThreaded
false
true
false
diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h
index b2e05fadfe..881633e05e 100644
--- a/src/common/logger/logger_settings.h
+++ b/src/common/logger/logger_settings.h
@@ -59,6 +59,7 @@ struct LogSettings
inline const static std::string mouseHighlighterLoggerName = "mouse-highlighter";
inline const static std::string mouseJumpLoggerName = "mouse-jump";
inline const static std::string mousePointerCrosshairsLoggerName = "mouse-pointer-crosshairs";
+ inline const static std::string cursorWrapLoggerName = "cursor-wrap";
inline const static std::string imageResizerLoggerName = "imageresizer";
inline const static std::string powerRenameLoggerName = "powerrename";
inline const static std::string alwaysOnTopLoggerName = "always-on-top";
diff --git a/src/common/notifications/BackgroundActivator/BackgroundActivator.vcxproj b/src/common/notifications/BackgroundActivator/BackgroundActivator.vcxproj
index 077333a664..b2ebc7cb72 100644
--- a/src/common/notifications/BackgroundActivator/BackgroundActivator.vcxproj
+++ b/src/common/notifications/BackgroundActivator/BackgroundActivator.vcxproj
@@ -46,16 +46,6 @@
notifications
-
-
- MultiThreadedDebugDLL
-
-
-
-
- MultiThreadedDLL
-
-
$(IntDir)pch.pch
diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h
index 471cefe480..ecf338d212 100644
--- a/src/common/utils/gpo.h
+++ b/src/common/utils/gpo.h
@@ -3,6 +3,7 @@
#include
#include
#include
+#include
namespace powertoys_gpo
{
@@ -51,6 +52,7 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_HIGHLIGHTER = L"ConfigureEnabledUtilityMouseHighlighter";
const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_JUMP = L"ConfigureEnabledUtilityMouseJump";
const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS = L"ConfigureEnabledUtilityMousePointerCrosshairs";
+ const std::wstring POLICY_CONFIGURE_ENABLED_CURSOR_WRAP = L"ConfigureEnabledUtilityCursorWrap";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_RENAME = L"ConfigureEnabledUtilityPowerRename";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER = L"ConfigureEnabledUtilityPowerLauncher";
const std::wstring POLICY_CONFIGURE_ENABLED_QUICK_ACCENT = L"ConfigureEnabledUtilityQuickAccent";
@@ -82,6 +84,14 @@ namespace powertoys_gpo
const std::wstring POLICY_CONFIGURE_RUN_AT_STARTUP = L"ConfigureRunAtStartup";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS = L"PowerLauncherAllPluginsEnabledState";
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS = L"AllowPowerToysAdvancedPasteOnlineAIModels";
+ const std::wstring POLICY_ALLOW_ADVANCED_PASTE_OPENAI = L"AllowAdvancedPasteOpenAI";
+ const std::wstring POLICY_ALLOW_ADVANCED_PASTE_AZURE_OPENAI = L"AllowAdvancedPasteAzureOpenAI";
+ const std::wstring POLICY_ALLOW_ADVANCED_PASTE_AZURE_AI_INFERENCE = L"AllowAdvancedPasteAzureAIInference";
+ const std::wstring POLICY_ALLOW_ADVANCED_PASTE_MISTRAL = L"AllowAdvancedPasteMistral";
+ const std::wstring POLICY_ALLOW_ADVANCED_PASTE_GOOGLE = L"AllowAdvancedPasteGoogle";
+ const std::wstring POLICY_ALLOW_ADVANCED_PASTE_ANTHROPIC = L"AllowAdvancedPasteAnthropic";
+ const std::wstring POLICY_ALLOW_ADVANCED_PASTE_OLLAMA = L"AllowAdvancedPasteOllama";
+ const std::wstring POLICY_ALLOW_ADVANCED_PASTE_FOUNDRY_LOCAL = L"AllowAdvancedPasteFoundryLocal";
const std::wstring POLICY_MWB_CLIPBOARD_SHARING_ENABLED = L"MwbClipboardSharingEnabled";
const std::wstring POLICY_MWB_FILE_TRANSFER_ENABLED = L"MwbFileTransferEnabled";
const std::wstring POLICY_MWB_USE_ORIGINAL_USER_INTERFACE = L"MwbUseOriginalUserInterface";
@@ -401,6 +411,11 @@ namespace powertoys_gpo
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS);
}
+ inline gpo_rule_configured_t getConfiguredCursorWrapEnabledValue()
+ {
+ return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_CURSOR_WRAP);
+ }
+
inline gpo_rule_configured_t getConfiguredPowerRenameEnabledValue()
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_RENAME);
@@ -575,6 +590,46 @@ namespace powertoys_gpo
return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS);
}
+ inline gpo_rule_configured_t getAllowedAdvancedPasteOpenAIValue()
+ {
+ return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_OPENAI);
+ }
+
+ inline gpo_rule_configured_t getAllowedAdvancedPasteAzureOpenAIValue()
+ {
+ return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_AZURE_OPENAI);
+ }
+
+ inline gpo_rule_configured_t getAllowedAdvancedPasteAzureAIInferenceValue()
+ {
+ return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_AZURE_AI_INFERENCE);
+ }
+
+ inline gpo_rule_configured_t getAllowedAdvancedPasteMistralValue()
+ {
+ return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_MISTRAL);
+ }
+
+ inline gpo_rule_configured_t getAllowedAdvancedPasteGoogleValue()
+ {
+ return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_GOOGLE);
+ }
+
+ inline gpo_rule_configured_t getAllowedAdvancedPasteAnthropicValue()
+ {
+ return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_ANTHROPIC);
+ }
+
+ inline gpo_rule_configured_t getAllowedAdvancedPasteOllamaValue()
+ {
+ return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_OLLAMA);
+ }
+
+ inline gpo_rule_configured_t getAllowedAdvancedPasteFoundryLocalValue()
+ {
+ return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_FOUNDRY_LOCAL);
+ }
+
inline gpo_rule_configured_t getConfiguredMwbClipboardSharingEnabledValue()
{
return getConfiguredValue(POLICY_MWB_CLIPBOARD_SHARING_ENABLED);
diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj
index 3186a01d43..b36e602d25 100644
--- a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj
+++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj
@@ -33,9 +33,4 @@
-
-
-
-
-
diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs
index deae2eb832..45fd36d10e 100644
--- a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs
+++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs
@@ -23,7 +23,8 @@ public sealed class SettingsResourceAdvancedPasteModuleTest : SettingsResourceMo
{
s.Properties.ShowCustomPreview = !s.Properties.ShowCustomPreview;
s.Properties.CloseAfterLosingFocus = !s.Properties.CloseAfterLosingFocus;
- s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled;
+
+ // s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled;
s.Properties.AdvancedPasteUIShortcut = new HotkeySettings
{
Key = "mock",
diff --git a/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs b/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs
index dcb6abf4a1..5eb91acec3 100644
--- a/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs
+++ b/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs
@@ -13,7 +13,7 @@ namespace PowerToys.DSC.Models;
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 const string Executable = @"..\PowerToys.DSC.exe";
private readonly string _type;
private readonly string _version;
diff --git a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
index 230cd4556b..9dc11a0a8a 100644
--- a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
+++ b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj
@@ -40,9 +40,11 @@
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx
index 07d4f44bde..4b77a6783f 100644
--- a/src/gpo/assets/PowerToys.admx
+++ b/src/gpo/assets/PowerToys.admx
@@ -1,11 +1,11 @@
-
+
-
+
@@ -26,6 +26,7 @@
+
@@ -614,6 +615,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml
index 2703358bb0..1bfa55866d 100644
--- a/src/gpo/assets/en-US/PowerToys.adml
+++ b/src/gpo/assets/en-US/PowerToys.adml
@@ -1,7 +1,7 @@
-
+
PowerToys
PowerToys
@@ -33,6 +33,7 @@
PowerToys version 0.88.0 or later
PowerToys version 0.89.0 or later
PowerToys version 0.90.0 or later
+ PowerToys version 0.96.0 or later
From PowerToys version 0.64.0 until PowerToys version 0.87.1
This policy configures the enabled state for all PowerToys utilities.
@@ -291,6 +292,54 @@ If you don't configure this policy, the user will be able to control the setting
QOI file preview: Configure enabled state
QOI file thumbnail: Configure enabled state
Allow using online AI models
+ Advanced Paste: Allow OpenAI endpoint
+ This policy controls whether users can use the OpenAI endpoint in Advanced Paste.
+
+If you enable or don't configure this policy, users can configure and use OpenAI as their AI provider.
+
+If you disable this policy, users will not be able to select or use OpenAI endpoint in Advanced Paste settings.
+ Advanced Paste: Allow Azure OpenAI endpoint
+ This policy controls whether users can use the Azure OpenAI endpoint in Advanced Paste.
+
+If you enable or don't configure this policy, users can configure and use Azure OpenAI as their AI provider.
+
+If you disable this policy, users will not be able to select or use Azure OpenAI endpoint in Advanced Paste settings.
+ Advanced Paste: Allow Azure AI Inference endpoint
+ This policy controls whether users can use the Azure AI Inference endpoint in Advanced Paste.
+
+If you enable or don't configure this policy, users can configure and use Azure AI Inference as their AI provider.
+
+If you disable this policy, users will not be able to select or use Azure AI Inference endpoint in Advanced Paste settings.
+ Advanced Paste: Allow Mistral endpoint
+ This policy controls whether users can use the Mistral AI endpoint in Advanced Paste.
+
+If you enable or don't configure this policy, users can configure and use Mistral as their AI provider.
+
+If you disable this policy, users will not be able to select or use Mistral endpoint in Advanced Paste settings.
+ Advanced Paste: Allow Google endpoint
+ This policy controls whether users can use the Google (Gemini) endpoint in Advanced Paste.
+
+If you enable or don't configure this policy, users can configure and use Google as their AI provider.
+
+If you disable this policy, users will not be able to select or use Google endpoint in Advanced Paste settings.
+ Advanced Paste: Allow Anthropic endpoint
+ This policy controls whether users can use the Anthropic (Claude) endpoint in Advanced Paste.
+
+If you enable or don't configure this policy, users can configure and use Anthropic as their AI provider.
+
+If you disable this policy, users will not be able to select or use Anthropic endpoint in Advanced Paste settings.
+ Advanced Paste: Allow Ollama endpoint
+ This policy controls whether users can use the Ollama local model endpoint in Advanced Paste.
+
+If you enable or don't configure this policy, users can configure and use Ollama as their AI provider.
+
+If you disable this policy, users will not be able to select or use Ollama endpoint in Advanced Paste settings.
+ Advanced Paste: Allow Foundry Local endpoint
+ This policy controls whether users can use the Foundry Local model endpoint in Advanced Paste.
+
+If you enable or don't configure this policy, users can configure and use Foundry Local as their AI provider.
+
+If you disable this policy, users will not be able to select or use Foundry Local endpoint in Advanced Paste settings.
Clipboard sharing enabled
File transfer enabled
Original user interface is available
diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs
new file mode 100644
index 0000000000..f6c2f5098d
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs
@@ -0,0 +1,66 @@
+// 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.Collections.ObjectModel;
+using System.Threading.Tasks;
+using AdvancedPaste.Models;
+using AdvancedPaste.Settings;
+using Microsoft.PowerToys.Settings.UI.Library;
+
+namespace AdvancedPaste.UnitTests.Mocks;
+
+///
+/// Minimal implementation used by integration tests that
+/// need to construct the runtime Advanced Paste services.
+///
+internal sealed class IntegrationTestUserSettings : IUserSettings
+{
+ private readonly PasteAIConfiguration _configuration;
+ private readonly IReadOnlyList _customActions;
+ private readonly IReadOnlyList _additionalActions;
+
+ public IntegrationTestUserSettings()
+ {
+ var provider = new PasteAIProviderDefinition
+ {
+ Id = "integration-openai",
+ EnableAdvancedAI = true,
+ ServiceTypeKind = AIServiceType.OpenAI,
+ ModelName = "gpt-4o",
+ ModerationEnabled = true,
+ };
+
+ _configuration = new PasteAIConfiguration
+ {
+ ActiveProviderId = provider.Id,
+ Providers = new ObservableCollection { provider },
+ };
+
+ _customActions = Array.Empty();
+ _additionalActions = Array.Empty();
+ }
+
+ public bool IsAIEnabled => true;
+
+ public bool ShowCustomPreview => false;
+
+ public bool CloseAfterLosingFocus => false;
+
+ public IReadOnlyList CustomActions => _customActions;
+
+ public IReadOnlyList AdditionalActions => _additionalActions;
+
+ public PasteAIConfiguration PasteAIConfiguration => _configuration;
+
+ public event EventHandler Changed;
+
+ public Task SetActiveAIProviderAsync(string providerId)
+ {
+ _configuration.ActiveProviderId = providerId ?? string.Empty;
+ Changed?.Invoke(this, EventArgs.Empty);
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs
index 3782b057f1..17b8139bad 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs
@@ -13,6 +13,8 @@ using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
+using AdvancedPaste.Services;
+using AdvancedPaste.Services.CustomActions;
using AdvancedPaste.Services.OpenAI;
using AdvancedPaste.UnitTests.Mocks;
using ManagedCommon;
@@ -79,7 +81,9 @@ public sealed class AIServiceBatchIntegrationTests
Assert.IsTrue(results.Count <= inputs.Count);
CollectionAssert.AreEqual(results.Select(result => result.ToInput()).ToList(), inputs.Take(results.Count).ToList());
+ #pragma warning disable IL2026, IL3050 // The tests rely on runtime JSON serialization for ad-hoc data files.
async Task WriteResultsAsync() => await File.WriteAllTextAsync(resultsFile, JsonSerializer.Serialize(results, SerializerOptions));
+ #pragma warning restore IL2026, IL3050
Logger.LogInfo($"Starting {nameof(TestGenerateBatchResults)}; Count={inputs.Count}, InCache={results.Count}");
@@ -101,8 +105,12 @@ public sealed class AIServiceBatchIntegrationTests
await WriteResultsAsync();
}
- private static async Task> GetDataListAsync(string filePath) =>
- File.Exists(filePath) ? JsonSerializer.Deserialize>(await File.ReadAllTextAsync(filePath)) : [];
+ private static async Task> GetDataListAsync(string filePath)
+ {
+ #pragma warning disable IL2026, IL3050 // Tests only run locally and can depend on runtime JSON serialization.
+ return File.Exists(filePath) ? JsonSerializer.Deserialize>(await File.ReadAllTextAsync(filePath)) : [];
+ #pragma warning restore IL2026, IL3050
+ }
private static async Task GetTextOutputAsync(BatchTestInput input, PasteFormats format)
{
@@ -130,23 +138,35 @@ public sealed class AIServiceBatchIntegrationTests
private static async Task GetOutputDataPackageAsync(BatchTestInput batchTestInput, PasteFormats format)
{
- VaultCredentialsProvider credentialsProvider = new();
- PromptModerationService promptModerationService = new(credentialsProvider);
+ var services = CreateServices();
NoOpProgress progress = new();
- CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
switch (format)
{
case PasteFormats.CustomTextTransformation:
- return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress));
+ var transformResult = await services.CustomActionTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress);
+ return DataPackageHelpers.CreateFromText(transformResult.Content ?? string.Empty);
case PasteFormats.KernelQuery:
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
- KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
- return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
+ return await services.KernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
default:
throw new InvalidOperationException($"Unexpected format {format}");
}
}
+
+ private static IntegrationTestServices CreateServices()
+ {
+ IntegrationTestUserSettings userSettings = new();
+ EnhancedVaultCredentialsProvider credentialsProvider = new(userSettings);
+ PromptModerationService promptModerationService = new(credentialsProvider);
+ PasteAIProviderFactory providerFactory = new();
+ ICustomActionTransformService customActionTransformService = new CustomActionTransformService(promptModerationService, providerFactory, credentialsProvider, userSettings);
+ IKernelService kernelService = new AdvancedAIKernelService(credentialsProvider, new NoOpKernelQueryCacheService(), promptModerationService, userSettings, customActionTransformService);
+
+ return new IntegrationTestServices(customActionTransformService, kernelService);
+ }
+
+ private readonly record struct IntegrationTestServices(ICustomActionTransformService CustomActionTransformService, IKernelService KernelService);
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs
index 998534cf5e..7c16089cd5 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs
@@ -11,6 +11,8 @@ using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
+using AdvancedPaste.Services;
+using AdvancedPaste.Services.CustomActions;
using AdvancedPaste.Services.OpenAI;
using AdvancedPaste.Telemetry;
using AdvancedPaste.UnitTests.Mocks;
@@ -27,16 +29,19 @@ namespace AdvancedPaste.UnitTests.ServicesTests;
public sealed class KernelServiceIntegrationTests : IDisposable
{
private const string StandardImageFile = "image_with_text_example.png";
- private KernelService _kernelService;
+ private IKernelService _kernelService;
private AdvancedPasteEventListener _eventListener;
[TestInitialize]
public void TestInitialize()
{
- VaultCredentialsProvider credentialsProvider = new();
+ IntegrationTestUserSettings userSettings = new();
+ EnhancedVaultCredentialsProvider credentialsProvider = new(userSettings);
PromptModerationService promptModerationService = new(credentialsProvider);
+ PasteAIProviderFactory providerFactory = new();
+ CustomActionTransformService customActionTransformService = new(promptModerationService, providerFactory, credentialsProvider, userSettings);
- _kernelService = new KernelService(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, new CustomTextTransformService(credentialsProvider, promptModerationService));
+ _kernelService = new AdvancedAIKernelService(credentialsProvider, new NoOpKernelQueryCacheService(), promptModerationService, userSettings, customActionTransformService);
_eventListener = new();
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj
index fba18de07c..54c1343c93 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj
@@ -33,11 +33,14 @@
+
+
+
@@ -49,7 +52,6 @@
-
@@ -57,10 +59,17 @@
+
+
+
+
+
+
+
@@ -102,6 +111,7 @@
+
@@ -114,9 +124,38 @@
true
+
+
+ MSBuild:Compile
+
+
MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
+
+
+
+
+ Assets\Settings\Icons\Models\%(Filename)%(Extension)
+ PreserveNewest
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+ PreserveNewest
+
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml
index da79c36a11..df6ed811ac 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml
@@ -9,6 +9,7 @@
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
index 3ac3baa9d0..2737fb44c2 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
@@ -10,10 +10,10 @@ using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
-
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Services;
+using AdvancedPaste.Services.CustomActions;
using AdvancedPaste.Settings;
using AdvancedPaste.ViewModels;
using ManagedCommon;
@@ -77,11 +77,12 @@ namespace AdvancedPaste
{
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
}).Build();
@@ -111,7 +112,11 @@ namespace AdvancedPaste
/// Invoked when the application is launched.
///
/// Details about the launch request and process.
+#if DEBUG
+ protected async override void OnLaunched(LaunchActivatedEventArgs args)
+#else
protected override void OnLaunched(LaunchActivatedEventArgs args)
+#endif
{
var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1)
@@ -133,6 +138,10 @@ namespace AdvancedPaste
{
ProcessNamedPipe(cmdArgs[2]);
}
+
+#if DEBUG
+ await ShowWindow(); // This allows for direct access without using PowerToys Runner, not all functionality might work
+#endif
}
private void ProcessNamedPipe(string pipeName)
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml
index f03a579821..a250fdffdc 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml
@@ -11,7 +11,7 @@
-
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml
new file mode 100644
index 0000000000..e008d35a9a
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml.cs
new file mode 100644
index 0000000000..a28c87ac61
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml.cs
@@ -0,0 +1,127 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using AdvancedPaste.Helpers;
+using AdvancedPaste.Models;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+
+namespace AdvancedPaste.Controls
+{
+ public sealed partial class ClipboardHistoryItemPreviewControl : UserControl
+ {
+ public static readonly DependencyProperty ClipboardItemProperty = DependencyProperty.Register(
+ nameof(ClipboardItem),
+ typeof(ClipboardItem),
+ typeof(ClipboardHistoryItemPreviewControl),
+ new PropertyMetadata(defaultValue: null, OnClipboardItemChanged));
+
+ public ClipboardItem ClipboardItem
+ {
+ get => (ClipboardItem)GetValue(ClipboardItemProperty);
+ set => SetValue(ClipboardItemProperty, value);
+ }
+
+ // Computed properties for display
+ public string Header => ClipboardItem != null ? GetHeaderFromFormat(ClipboardItem.Format) : string.Empty;
+
+ public string IconGlyph => ClipboardItem != null ? GetGlyphFromFormat(ClipboardItem.Format) : string.Empty;
+
+ public string ContentText => ClipboardItem?.Content ?? string.Empty;
+
+ public ImageSource ContentImage => ClipboardItem?.Image;
+
+ public DateTimeOffset? Timestamp => ClipboardItem?.Timestamp ?? ClipboardItem?.Item?.Timestamp;
+
+ public bool HasImage => ContentImage is not null;
+
+ public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage;
+
+ public bool HasGlyph => !HasImage && !HasText && !string.IsNullOrEmpty(IconGlyph);
+
+ public ClipboardHistoryItemPreviewControl()
+ {
+ InitializeComponent();
+ }
+
+ private static void OnClipboardItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is ClipboardHistoryItemPreviewControl control)
+ {
+ // Notify bindings that all computed properties may have changed
+ control.Bindings.Update();
+ }
+ }
+
+ private static string GetHeaderFromFormat(ClipboardFormat format)
+ {
+ // Check flags in priority order (most specific first)
+ if (format.HasFlag(ClipboardFormat.Image))
+ {
+ return GetStringOrFallback("ClipboardPreviewCategoryImage", "Image");
+ }
+
+ if (format.HasFlag(ClipboardFormat.Video))
+ {
+ return GetStringOrFallback("ClipboardPreviewCategoryVideo", "Video");
+ }
+
+ if (format.HasFlag(ClipboardFormat.Audio))
+ {
+ return GetStringOrFallback("ClipboardPreviewCategoryAudio", "Audio");
+ }
+
+ if (format.HasFlag(ClipboardFormat.File))
+ {
+ return GetStringOrFallback("ClipboardPreviewCategoryFile", "File");
+ }
+
+ if (format.HasFlag(ClipboardFormat.Text) || format.HasFlag(ClipboardFormat.Html))
+ {
+ return GetStringOrFallback("ClipboardPreviewCategoryText", "Text");
+ }
+
+ return GetStringOrFallback("ClipboardPreviewCategoryUnknown", "Clipboard");
+ }
+
+ private static string GetGlyphFromFormat(ClipboardFormat format)
+ {
+ // Check flags in priority order (most specific first)
+ if (format.HasFlag(ClipboardFormat.Image))
+ {
+ return "\uEB9F"; // Image icon
+ }
+
+ if (format.HasFlag(ClipboardFormat.Video))
+ {
+ return "\uE714"; // Video icon
+ }
+
+ if (format.HasFlag(ClipboardFormat.Audio))
+ {
+ return "\uE189"; // Audio icon
+ }
+
+ if (format.HasFlag(ClipboardFormat.File))
+ {
+ return "\uE8A5"; // File icon
+ }
+
+ if (format.HasFlag(ClipboardFormat.Text) || format.HasFlag(ClipboardFormat.Html))
+ {
+ return "\uE8D2"; // Text icon
+ }
+
+ return "\uE77B"; // Generic clipboard icon
+ }
+
+ private static string GetStringOrFallback(string resourceKey, string fallback)
+ {
+ var value = ResourceLoaderInstance.ResourceLoader.GetString(resourceKey);
+ return string.IsNullOrEmpty(value) ? fallback : value;
+ }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
index dd09c717b0..8b7e3e5c6a 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
@@ -7,34 +7,21 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:AdvancedPaste.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:settings="using:Microsoft.PowerToys.Settings.UI.Library"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI"
+ x:Name="PromptBoxControl"
mc:Ignorable="d">
-
-
- #65C8F2
-
-
-
-
-
-
-
- #005FB8
-
-
-
-
-
-
-
- #48B1E9
-
-
-
+
+
+
+
+
+
+ 44
+
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Assets/AdvancedPaste/Gradient.png b/src/modules/AdvancedPaste/AdvancedPaste/Assets/AdvancedPaste/Gradient.png
index 73621edfc0..78a9a18606 100644
Binary files a/src/modules/AdvancedPaste/AdvancedPaste/Assets/AdvancedPaste/Gradient.png and b/src/modules/AdvancedPaste/AdvancedPaste/Assets/AdvancedPaste/Gradient.png differ
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceUsageHelper.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceUsageHelper.cs
new file mode 100644
index 0000000000..ba7d33f4fb
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceUsageHelper.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using AdvancedPaste.Models;
+using Microsoft.SemanticKernel;
+
+namespace AdvancedPaste.Helpers;
+
+///
+/// Helper class for extracting AI service usage information from chat messages.
+///
+public static class AIServiceUsageHelper
+{
+ ///
+ /// Extracts AI service usage information from OpenAI chat message metadata.
+ ///
+ /// The chat message containing usage metadata.
+ /// AI service usage information or AIServiceUsage.None if extraction fails.
+ public static AIServiceUsage GetOpenAIServiceUsage(ChatMessageContent chatMessage)
+ {
+ // Try to get usage information from metadata
+ if (chatMessage.Metadata?.TryGetValue("Usage", out var usageObj) == true)
+ {
+ // Handle different possible usage types through reflection to be version-agnostic
+ var usageType = usageObj.GetType();
+
+ try
+ {
+ // Try common property names for prompt tokens
+ var promptTokensProp = usageType.GetProperty("PromptTokens") ??
+ usageType.GetProperty("InputTokens") ??
+ usageType.GetProperty("InputTokenCount");
+
+ var completionTokensProp = usageType.GetProperty("CompletionTokens") ??
+ usageType.GetProperty("OutputTokens") ??
+ usageType.GetProperty("OutputTokenCount");
+
+ if (promptTokensProp != null && completionTokensProp != null)
+ {
+ var promptTokens = (int)(promptTokensProp.GetValue(usageObj) ?? 0);
+ var completionTokens = (int)(completionTokensProp.GetValue(usageObj) ?? 0);
+ return new AIServiceUsage(promptTokens, completionTokens);
+ }
+ }
+ catch
+ {
+ // If reflection fails, fall back to no usage
+ }
+ }
+
+ return AIServiceUsage.None;
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs
new file mode 100644
index 0000000000..9f824d3399
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs
@@ -0,0 +1,84 @@
+// 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.Threading.Tasks;
+using AdvancedPaste.Models;
+using Microsoft.UI.Xaml.Media.Imaging;
+using Windows.ApplicationModel.DataTransfer;
+
+namespace AdvancedPaste.Helpers
+{
+ internal static class ClipboardItemHelper
+ {
+ ///
+ /// Creates a ClipboardItem from current clipboard data.
+ ///
+ public static async Task CreateFromCurrentClipboardAsync(
+ DataPackageView clipboardData,
+ ClipboardFormat availableFormats,
+ DateTimeOffset? timestamp = null,
+ BitmapImage existingImage = null)
+ {
+ if (clipboardData == null || availableFormats == ClipboardFormat.None)
+ {
+ return null;
+ }
+
+ var clipboardItem = new ClipboardItem
+ {
+ Format = availableFormats,
+ Timestamp = timestamp,
+ };
+
+ // Text or HTML content
+ if (availableFormats.HasFlag(ClipboardFormat.Text) || availableFormats.HasFlag(ClipboardFormat.Html))
+ {
+ clipboardItem.Content = await clipboardData.GetTextOrEmptyAsync();
+ }
+
+ // Image content
+ else if (availableFormats.HasFlag(ClipboardFormat.Image))
+ {
+ // Reuse existing image if provided
+ if (existingImage != null)
+ {
+ clipboardItem.Image = existingImage;
+ }
+ else
+ {
+ clipboardItem.Image = await TryCreateBitmapImageAsync(clipboardData);
+ }
+ }
+
+ return clipboardItem;
+ }
+
+ ///
+ /// Creates a BitmapImage from clipboard data.
+ ///
+ private static async Task TryCreateBitmapImageAsync(DataPackageView clipboardData)
+ {
+ try
+ {
+ var imageReference = await clipboardData.GetBitmapAsync();
+ if (imageReference != null)
+ {
+ using (var imageStream = await imageReference.OpenReadAsync())
+ {
+ var bitmapImage = new BitmapImage();
+ await bitmapImage.SetSourceAsync(imageStream);
+ return bitmapImage;
+ }
+ }
+ }
+ catch
+ {
+ // Silently fail - caller can check for null
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs
index 529773f9a6..2cd7554a50 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs
@@ -6,11 +6,13 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
-
using AdvancedPaste.Models;
using ManagedCommon;
+using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.Data.Html;
@@ -180,6 +182,46 @@ internal static class DataPackageHelpers
}
}
+ internal static async Task GetClipboardTextOrThrowAsync(this DataPackageView dataPackageView, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(dataPackageView);
+
+ try
+ {
+ if (dataPackageView.Contains(StandardDataFormats.Text))
+ {
+ return await dataPackageView.GetTextAsync();
+ }
+
+ if (dataPackageView.Contains(StandardDataFormats.Html))
+ {
+ var html = await dataPackageView.GetHtmlFormatAsync();
+ return HtmlUtilities.ConvertToText(html);
+ }
+
+ if (dataPackageView.Contains(StandardDataFormats.Bitmap))
+ {
+ var bitmap = await dataPackageView.GetImageContentAsync();
+ if (bitmap != null)
+ {
+ return await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken);
+ }
+ }
+ }
+ catch (Exception ex) when (ex is COMException or InvalidOperationException)
+ {
+ throw CreateClipboardTextMissingException(ex);
+ }
+
+ throw CreateClipboardTextMissingException();
+ }
+
+ private static PasteActionException CreateClipboardTextMissingException(Exception innerException = null)
+ {
+ var message = ResourceLoaderInstance.ResourceLoader.GetString("ClipboardEmptyWarning");
+ return new PasteActionException(message, innerException ?? new InvalidOperationException("Clipboard does not contain text content."));
+ }
+
internal static async Task GetHtmlContentAsync(this DataPackageView dataPackageView) =>
dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty;
@@ -195,6 +237,22 @@ internal static class DataPackageHelpers
return null;
}
+ internal static async Task GetPreviewBitmapAsync(this DataPackageView dataPackageView)
+ {
+ var stream = await dataPackageView.GetImageStreamAsync();
+ if (stream == null)
+ {
+ return null;
+ }
+
+ using (stream)
+ {
+ var bitmapImage = new BitmapImage();
+ bitmapImage.SetSource(stream);
+ return bitmapImage;
+ }
+ }
+
private static async Task GetImageStreamAsync(this DataPackageView dataPackageView)
{
if (dataPackageView.Contains(StandardDataFormats.StorageItems))
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
index 105fe2c0d8..e32cf61af4 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
using AdvancedPaste.Models;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -12,7 +13,7 @@ namespace AdvancedPaste.Settings
{
public interface IUserSettings
{
- public bool IsAdvancedAIEnabled { get; }
+ public bool IsAIEnabled { get; }
public bool ShowCustomPreview { get; }
@@ -22,6 +23,10 @@ namespace AdvancedPaste.Settings
public IReadOnlyList AdditionalActions { get; }
+ public PasteAIConfiguration PasteAIConfiguration { get; }
+
public event EventHandler Changed;
+
+ Task SetActiveAIProviderAsync(string providerId);
}
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
index 8a25b70f07..b6b6c19734 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs
@@ -13,6 +13,7 @@ using AdvancedPaste.Models;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
+using Windows.Security.Credentials;
namespace AdvancedPaste.Settings
{
@@ -33,7 +34,7 @@ namespace AdvancedPaste.Settings
public event EventHandler Changed;
- public bool IsAdvancedAIEnabled { get; private set; }
+ public bool IsAIEnabled { get; private set; }
public bool ShowCustomPreview { get; private set; }
@@ -43,13 +44,16 @@ namespace AdvancedPaste.Settings
public IReadOnlyList CustomActions => _customActions;
+ public PasteAIConfiguration PasteAIConfiguration { get; private set; }
+
public UserSettings(IFileSystem fileSystem)
{
_settingsUtils = new SettingsUtils(fileSystem);
- IsAdvancedAIEnabled = false;
+ IsAIEnabled = false;
ShowCustomPreview = true;
CloseAfterLosingFocus = false;
+ PasteAIConfiguration = new PasteAIConfiguration();
_additionalActions = [];
_customActions = [];
_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
@@ -94,13 +98,16 @@ namespace AdvancedPaste.Settings
var settings = _settingsUtils.GetSettingsOrDefault(AdvancedPasteModuleName);
if (settings != null)
{
+ bool migratedLegacyEnablement = TryMigrateLegacyAIEnablement(settings);
+
void UpdateSettings()
{
var properties = settings.Properties;
- IsAdvancedAIEnabled = properties.IsAdvancedAIEnabled;
+ IsAIEnabled = properties.IsAIEnabled;
ShowCustomPreview = properties.ShowCustomPreview;
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
+ PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
var sourceAdditionalActions = properties.AdditionalActions;
(PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats =
@@ -126,6 +133,11 @@ namespace AdvancedPaste.Settings
Task.Factory
.StartNew(UpdateSettings, CancellationToken.None, TaskCreationOptions.None, _taskScheduler)
.Wait();
+
+ if (migratedLegacyEnablement)
+ {
+ settings.Save(_settingsUtils);
+ }
}
retry = false;
@@ -144,6 +156,114 @@ namespace AdvancedPaste.Settings
}
}
+ private static bool TryMigrateLegacyAIEnablement(AdvancedPasteSettings settings)
+ {
+ if (settings?.Properties is null)
+ {
+ return false;
+ }
+
+ if (settings.Properties.IsAIEnabled || !LegacyOpenAIKeyExists())
+ {
+ return false;
+ }
+
+ settings.Properties.IsAIEnabled = true;
+ return true;
+ }
+
+ private static bool LegacyOpenAIKeyExists()
+ {
+ try
+ {
+ PasswordVault vault = new();
+ return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ public async Task SetActiveAIProviderAsync(string providerId)
+ {
+ if (string.IsNullOrWhiteSpace(providerId))
+ {
+ return;
+ }
+
+ await Task.Run(() =>
+ {
+ lock (_loadingSettingsLock)
+ {
+ var settings = _settingsUtils.GetSettingsOrDefault(AdvancedPasteModuleName);
+ var configuration = settings?.Properties?.PasteAIConfiguration;
+ var providers = configuration?.Providers;
+
+ if (configuration == null || providers == null || providers.Count == 0)
+ {
+ return;
+ }
+
+ var target = providers.FirstOrDefault(provider => string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase));
+ if (target == null)
+ {
+ return;
+ }
+
+ if (string.Equals(configuration.ActiveProvider?.Id, providerId, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ configuration.ActiveProviderId = providerId;
+
+ foreach (var provider in providers)
+ {
+ provider.IsActive = string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase);
+ }
+
+ try
+ {
+ settings.Save(_settingsUtils);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to set active AI provider", ex);
+ return;
+ }
+
+ try
+ {
+ Task.Factory
+ .StartNew(
+ () =>
+ {
+ PasteAIConfiguration.ActiveProviderId = providerId;
+
+ if (PasteAIConfiguration.Providers is not null)
+ {
+ foreach (var provider in PasteAIConfiguration.Providers)
+ {
+ provider.IsActive = string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ Changed?.Invoke(this, EventArgs.Empty);
+ },
+ CancellationToken.None,
+ TaskCreationOptions.None,
+ _taskScheduler)
+ .Wait();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to dispatch active AI provider change", ex);
+ }
+ }
+ });
+ }
+
public void Dispose()
{
Dispose(true);
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs
index 1013108bc9..16814e7001 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs
@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System;
using AdvancedPaste.Helpers;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.ApplicationModel.DataTransfer;
@@ -12,10 +13,15 @@ public class ClipboardItem
{
public string Content { get; set; }
- public ClipboardHistoryItem Item { get; set; }
-
public BitmapImage Image { get; set; }
+ public ClipboardFormat Format { get; set; }
+
+ public DateTimeOffset? Timestamp { get; set; }
+
+ // Only used for clipboard history items that have a ClipboardHistoryItem
+ public ClipboardHistoryItem Item { get; set; }
+
public string Description => !string.IsNullOrEmpty(Content) ? Content :
Image is not null ? ResourceLoaderInstance.ResourceLoader.GetString("ClipboardHistoryImage") :
string.Empty;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/AdvancedAIKernelService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/AdvancedAIKernelService.cs
new file mode 100644
index 0000000000..b9566ed481
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/AdvancedAIKernelService.cs
@@ -0,0 +1,227 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using AdvancedPaste.Helpers;
+using AdvancedPaste.Models;
+using AdvancedPaste.Services.CustomActions;
+using AdvancedPaste.Settings;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.Amazon;
+using Microsoft.SemanticKernel.Connectors.AzureAIInference;
+using Microsoft.SemanticKernel.Connectors.Google;
+using Microsoft.SemanticKernel.Connectors.HuggingFace;
+using Microsoft.SemanticKernel.Connectors.MistralAI;
+using Microsoft.SemanticKernel.Connectors.Ollama;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+
+namespace AdvancedPaste.Services;
+
+public sealed class AdvancedAIKernelService : KernelServiceBase
+{
+ private sealed record RuntimeConfiguration(
+ AIServiceType ServiceType,
+ string ModelName,
+ string Endpoint,
+ string DeploymentName,
+ string ModelPath,
+ string SystemPrompt,
+ bool ModerationEnabled) : IKernelRuntimeConfiguration;
+
+ private readonly IAICredentialsProvider credentialsProvider;
+
+ public AdvancedAIKernelService(
+ IAICredentialsProvider credentialsProvider,
+ IKernelQueryCacheService queryCacheService,
+ IPromptModerationService promptModerationService,
+ IUserSettings userSettings,
+ ICustomActionTransformService customActionTransformService)
+ : base(queryCacheService, promptModerationService, userSettings, customActionTransformService)
+ {
+ ArgumentNullException.ThrowIfNull(credentialsProvider);
+
+ this.credentialsProvider = credentialsProvider;
+ }
+
+ protected override string AdvancedAIModelName => GetRuntimeConfiguration().ModelName;
+
+ protected override PromptExecutionSettings PromptExecutionSettings => CreatePromptExecutionSettings();
+
+ protected override void AddChatCompletionService(IKernelBuilder kernelBuilder)
+ {
+ ArgumentNullException.ThrowIfNull(kernelBuilder);
+
+ var runtimeConfig = GetRuntimeConfiguration();
+ var serviceType = runtimeConfig.ServiceType;
+ var modelName = runtimeConfig.ModelName;
+ var requiresApiKey = RequiresApiKey(serviceType);
+ var apiKey = string.Empty;
+ if (requiresApiKey)
+ {
+ this.credentialsProvider.Refresh();
+ apiKey = (this.credentialsProvider.GetKey() ?? string.Empty).Trim();
+ if (string.IsNullOrWhiteSpace(apiKey))
+ {
+ throw new InvalidOperationException($"An API key is required for {serviceType} but none was found in the credential vault.");
+ }
+ }
+
+ var endpoint = string.IsNullOrWhiteSpace(runtimeConfig.Endpoint) ? null : runtimeConfig.Endpoint.Trim();
+ var deployment = string.IsNullOrWhiteSpace(runtimeConfig.DeploymentName) ? modelName : runtimeConfig.DeploymentName;
+
+ switch (serviceType)
+ {
+ case AIServiceType.OpenAI:
+ kernelBuilder.AddOpenAIChatCompletion(modelName, apiKey, serviceId: modelName);
+ break;
+ case AIServiceType.AzureOpenAI:
+ kernelBuilder.AddAzureOpenAIChatCompletion(deployment, RequireEndpoint(endpoint, serviceType), apiKey, serviceId: modelName);
+ break;
+ default:
+ throw new NotSupportedException($"Service type '{runtimeConfig.ServiceType}' is not supported");
+ }
+ }
+
+ protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage)
+ {
+ return AIServiceUsageHelper.GetOpenAIServiceUsage(chatMessage);
+ }
+
+ protected override bool ShouldModerateAdvancedAI()
+ {
+ if (!TryGetRuntimeConfiguration(out var runtimeConfig))
+ {
+ return false;
+ }
+
+ return runtimeConfig.ModerationEnabled && (runtimeConfig.ServiceType == AIServiceType.OpenAI || runtimeConfig.ServiceType == AIServiceType.AzureOpenAI);
+ }
+
+ private static string GetModelName(PasteAIProviderDefinition config)
+ {
+ if (!string.IsNullOrWhiteSpace(config?.ModelName))
+ {
+ return config.ModelName;
+ }
+
+ return "gpt-4o";
+ }
+
+ protected override IKernelRuntimeConfiguration GetRuntimeConfiguration()
+ {
+ if (TryGetRuntimeConfiguration(out var runtimeConfig))
+ {
+ return runtimeConfig;
+ }
+
+ throw new InvalidOperationException("No Advanced AI provider is configured.");
+ }
+
+ private bool TryGetRuntimeConfiguration(out IKernelRuntimeConfiguration runtimeConfig)
+ {
+ runtimeConfig = null;
+
+ if (!TryResolveAdvancedProvider(out var provider))
+ {
+ return false;
+ }
+
+ var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
+ if (!IsServiceTypeSupported(serviceType))
+ {
+ return false;
+ }
+
+ runtimeConfig = new RuntimeConfiguration(
+ serviceType,
+ GetModelName(provider),
+ provider.EndpointUrl,
+ provider.DeploymentName,
+ provider.ModelPath,
+ provider.SystemPrompt,
+ provider.ModerationEnabled);
+ return true;
+ }
+
+ private bool TryResolveAdvancedProvider(out PasteAIProviderDefinition provider)
+ {
+ provider = null;
+
+ var configuration = this.UserSettings?.PasteAIConfiguration;
+ if (configuration is null)
+ {
+ return false;
+ }
+
+ var activeProvider = configuration.ActiveProvider;
+ if (IsAdvancedProvider(activeProvider))
+ {
+ provider = activeProvider;
+ return true;
+ }
+
+ if (activeProvider is not null)
+ {
+ return false;
+ }
+
+ var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedProvider);
+ if (fallback is not null)
+ {
+ provider = fallback;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsAdvancedProvider(PasteAIProviderDefinition provider)
+ {
+ if (provider is null || !provider.EnableAdvancedAI)
+ {
+ return false;
+ }
+
+ var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
+ return IsServiceTypeSupported(serviceType);
+ }
+
+ private static bool IsServiceTypeSupported(AIServiceType serviceType)
+ {
+ return serviceType is AIServiceType.OpenAI or AIServiceType.AzureOpenAI;
+ }
+
+ private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
+ {
+ return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
+ }
+
+ private static bool RequiresApiKey(AIServiceType serviceType)
+ {
+ return true;
+ }
+
+ private static string RequireEndpoint(string endpoint, AIServiceType serviceType)
+ {
+ if (!string.IsNullOrWhiteSpace(endpoint))
+ {
+ return endpoint;
+ }
+
+ throw new InvalidOperationException($"Endpoint is required for {serviceType} configuration but was not provided.");
+ }
+
+ private PromptExecutionSettings CreatePromptExecutionSettings()
+ {
+ var serviceType = GetRuntimeConfiguration().ServiceType;
+ return new OpenAIPromptExecutionSettings
+ {
+ FunctionChoiceBehavior = FunctionChoiceBehavior.Required(),
+ Temperature = 0.01,
+ };
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformResult.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformResult.cs
new file mode 100644
index 0000000000..562ea3976c
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformResult.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using AdvancedPaste.Models;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public sealed class CustomActionTransformResult
+ {
+ public CustomActionTransformResult(string content, AIServiceUsage usage)
+ {
+ Content = content;
+ Usage = usage;
+ }
+
+ public string Content { get; }
+
+ public AIServiceUsage Usage { get; }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs
new file mode 100644
index 0000000000..721a96070d
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs
@@ -0,0 +1,200 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using AdvancedPaste.Helpers;
+using AdvancedPaste.Models;
+using AdvancedPaste.Settings;
+using AdvancedPaste.Telemetry;
+using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public sealed class CustomActionTransformService : ICustomActionTransformService
+ {
+ private const string DefaultSystemPrompt = """
+ You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it.
+ Do not output anything else besides the reformatted clipboard content.
+ """;
+
+ private readonly IPromptModerationService promptModerationService;
+ private readonly IPasteAIProviderFactory providerFactory;
+ private readonly IAICredentialsProvider credentialsProvider;
+ private readonly IUserSettings userSettings;
+
+ public CustomActionTransformService(IPromptModerationService promptModerationService, IPasteAIProviderFactory providerFactory, IAICredentialsProvider credentialsProvider, IUserSettings userSettings)
+ {
+ this.promptModerationService = promptModerationService;
+ this.providerFactory = providerFactory;
+ this.credentialsProvider = credentialsProvider;
+ this.userSettings = userSettings;
+ }
+
+ public async Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress)
+ {
+ var pasteConfig = userSettings?.PasteAIConfiguration;
+ var providerConfig = BuildProviderConfig(pasteConfig);
+
+ return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress);
+ }
+
+ private async Task TransformAsync(string prompt, string inputText, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress progress)
+ {
+ ArgumentNullException.ThrowIfNull(providerConfig);
+
+ if (string.IsNullOrWhiteSpace(prompt))
+ {
+ return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
+ }
+
+ if (string.IsNullOrWhiteSpace(inputText))
+ {
+ Logger.LogWarning("Clipboard has no usable text data");
+ return new CustomActionTransformResult(string.Empty, AIServiceUsage.None);
+ }
+
+ var systemPrompt = providerConfig.SystemPrompt ?? DefaultSystemPrompt;
+
+ var fullPrompt = (systemPrompt ?? string.Empty) + "\n\n" + (inputText ?? string.Empty);
+
+ if (ShouldModerate(providerConfig))
+ {
+ await promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
+ }
+
+ try
+ {
+ var provider = providerFactory.CreateProvider(providerConfig);
+
+ var request = new PasteAIRequest
+ {
+ Prompt = prompt,
+ InputText = inputText,
+ SystemPrompt = systemPrompt,
+ };
+
+ var providerContent = await provider.ProcessPasteAsync(
+ request,
+ cancellationToken,
+ progress);
+
+ var usage = request.Usage;
+ var content = providerContent ?? string.Empty;
+
+ // Log endpoint usage
+ var endpointEvent = new AdvancedPasteEndpointUsageEvent(providerConfig.ProviderType);
+ PowerToysTelemetry.Log.WriteEvent(endpointEvent);
+
+ Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}");
+
+ return new CustomActionTransformResult(content, usage);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} failed", ex);
+
+ if (ex is PasteActionException or OperationCanceledException)
+ {
+ throw;
+ }
+
+ var statusCode = ExtractStatusCode(ex);
+ var failureMessage = providerConfig.ProviderType switch
+ {
+ AIServiceType.OpenAI or AIServiceType.AzureOpenAI => ErrorHelpers.TranslateErrorText(statusCode),
+ _ => ResourceLoaderInstance.ResourceLoader.GetString("PasteError"),
+ };
+
+ throw new PasteActionException(failureMessage, ex);
+ }
+ }
+
+ private static int ExtractStatusCode(Exception exception)
+ {
+ if (exception is HttpOperationException httpOperationException)
+ {
+ return (int?)httpOperationException.StatusCode ?? -1;
+ }
+
+ if (exception is HttpRequestException httpRequestException && httpRequestException.StatusCode is HttpStatusCode statusCode)
+ {
+ return (int)statusCode;
+ }
+
+ return -1;
+ }
+
+ private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
+ {
+ return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
+ }
+
+ private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config)
+ {
+ config ??= new PasteAIConfiguration();
+ var provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition();
+ var serviceType = NormalizeServiceType(provider.ServiceTypeKind);
+ var systemPrompt = string.IsNullOrWhiteSpace(provider.SystemPrompt) ? DefaultSystemPrompt : provider.SystemPrompt;
+ var apiKey = AcquireApiKey(serviceType);
+ var modelName = provider.ModelName;
+
+ var providerConfig = new PasteAIConfig
+ {
+ ProviderType = serviceType,
+ ApiKey = apiKey,
+ Model = modelName,
+ Endpoint = provider.EndpointUrl,
+ DeploymentName = provider.DeploymentName,
+ LocalModelPath = provider.ModelPath,
+ ModelPath = provider.ModelPath,
+ SystemPrompt = systemPrompt,
+ ModerationEnabled = provider.ModerationEnabled,
+ };
+
+ return providerConfig;
+ }
+
+ private string AcquireApiKey(AIServiceType serviceType)
+ {
+ if (!RequiresApiKey(serviceType))
+ {
+ return string.Empty;
+ }
+
+ credentialsProvider.Refresh();
+ return credentialsProvider.GetKey() ?? string.Empty;
+ }
+
+ private static bool RequiresApiKey(AIServiceType serviceType)
+ {
+ return serviceType switch
+ {
+ AIServiceType.Onnx => false,
+ AIServiceType.Ollama => false,
+ AIServiceType.Anthropic => false,
+ AIServiceType.AmazonBedrock => false,
+ _ => true,
+ };
+ }
+
+ private static bool ShouldModerate(PasteAIConfig providerConfig)
+ {
+ if (providerConfig is null || !providerConfig.ModerationEnabled)
+ {
+ return false;
+ }
+
+ return providerConfig.ProviderType == AIServiceType.OpenAI || providerConfig.ProviderType == AIServiceType.AzureOpenAI;
+ }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs
new file mode 100644
index 0000000000..4b4148f995
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs
@@ -0,0 +1,194 @@
+// 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;
+using System.Threading.Tasks;
+using AdvancedPaste.Models;
+using LanguageModelProvider;
+using Microsoft.Extensions.AI;
+using Microsoft.PowerToys.Settings.UI.Library;
+
+namespace AdvancedPaste.Services.CustomActions;
+
+public sealed class FoundryLocalPasteProvider : IPasteAIProvider
+{
+ private static readonly IReadOnlyCollection SupportedTypes = new[]
+ {
+ AIServiceType.FoundryLocal,
+ };
+
+ public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new FoundryLocalPasteProvider(config));
+
+ private static readonly LanguageModelService LanguageModels = LanguageModelService.CreateDefault();
+
+ private readonly PasteAIConfig _config;
+
+ public FoundryLocalPasteProvider(PasteAIConfig config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+ _config = config;
+ }
+
+ public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey();
+
+ public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model;
+
+ public async Task IsAvailableAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return await FoundryLocalModelProvider.Instance.IsAvailable().ConfigureAwait(false);
+ }
+
+ public async Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ try
+ {
+ var systemPrompt = request.SystemPrompt;
+ if (string.IsNullOrWhiteSpace(systemPrompt))
+ {
+ throw new PasteActionException(
+ "System prompt is required for Foundry Local",
+ new ArgumentException("System prompt must be provided", nameof(request)));
+ }
+
+ var prompt = request.Prompt;
+ var inputText = request.InputText;
+ if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
+ {
+ throw new PasteActionException(
+ "Prompt and input text are required",
+ new ArgumentException("Prompt and input text must be provided", nameof(request)));
+ }
+
+ var modelReference = _config?.Model;
+ if (string.IsNullOrWhiteSpace(modelReference))
+ {
+ throw new PasteActionException(
+ "No Foundry Local model selected",
+ new InvalidOperationException("Model identifier is required"),
+ aiServiceMessage: "Please select a model in the AI provider settings. Model identifier should be in the format 'fl://model-name'.");
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+ var chatClient = LanguageModels.GetClient(modelReference);
+ if (chatClient is null)
+ {
+ throw new PasteActionException(
+ $"Unable to load Foundry Local model: {modelReference}",
+ new InvalidOperationException("Chat client resolution failed"),
+ aiServiceMessage: "The model may not be downloaded or the Foundry Local service may not be running. Please check the model status in settings.");
+ }
+
+ // Extract actual model ID from the URL (format: fl://modelId)
+ var actualModelId = modelReference.Replace("fl://", string.Empty).Trim('/');
+
+ var userMessageContent = $"""
+ User instructions:
+ {prompt}
+
+ Text:
+ {inputText}
+
+ Output:
+ """;
+
+ var chatMessages = new List
+ {
+ new(ChatRole.System, systemPrompt),
+ new(ChatRole.User, userMessageContent),
+ };
+
+ var chatOptions = CreateChatOptions(_config?.SystemPrompt, actualModelId);
+
+ progress?.Report(0.1);
+
+ var response = await chatClient.GetResponseAsync(chatMessages, chatOptions, cancellationToken).ConfigureAwait(false);
+
+ progress?.Report(0.8);
+
+ var responseText = GetResponseText(response);
+ request.Usage = ToUsage(response.Usage);
+
+ progress?.Report(1.0);
+
+ return responseText ?? string.Empty;
+ }
+ catch (OperationCanceledException)
+ {
+ // Let cancellation exceptions pass through unchanged
+ throw;
+ }
+ catch (PasteActionException)
+ {
+ // Let our custom exceptions pass through unchanged
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // Wrap any other exceptions with context
+ var modelInfo = !string.IsNullOrWhiteSpace(_config?.Model) ? $" (Model: {_config.Model})" : string.Empty;
+ throw new PasteActionException(
+ $"Failed to generate response using Foundry Local{modelInfo}",
+ ex,
+ aiServiceMessage: $"Error details: {ex.Message}");
+ }
+ }
+
+ private static ChatOptions CreateChatOptions(string systemPrompt, string modelReference)
+ {
+ var options = new ChatOptions
+ {
+ ModelId = modelReference,
+ };
+
+ if (!string.IsNullOrWhiteSpace(systemPrompt))
+ {
+ options.Instructions = systemPrompt;
+ }
+
+ return options;
+ }
+
+ private static string GetResponseText(ChatResponse response)
+ {
+ if (!string.IsNullOrWhiteSpace(response.Text))
+ {
+ return response.Text;
+ }
+
+ if (response.Messages is { Count: > 0 })
+ {
+ var lastMessage = response.Messages.LastOrDefault(m => !string.IsNullOrWhiteSpace(m.Text));
+ if (!string.IsNullOrWhiteSpace(lastMessage?.Text))
+ {
+ return lastMessage.Text;
+ }
+ }
+
+ return string.Empty;
+ }
+
+ private static AIServiceUsage ToUsage(UsageDetails usageDetails)
+ {
+ if (usageDetails is null)
+ {
+ return AIServiceUsage.None;
+ }
+
+ int promptTokens = (int)(usageDetails.InputTokenCount ?? 0);
+ int completionTokens = (int)(usageDetails.OutputTokenCount ?? 0);
+
+ if (promptTokens == 0 && completionTokens == 0)
+ {
+ return AIServiceUsage.None;
+ }
+
+ return new AIServiceUsage(promptTokens, completionTokens);
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs
new file mode 100644
index 0000000000..1c3ecb980c
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using AdvancedPaste.Settings;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public interface ICustomActionTransformService
+ {
+ Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress);
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProvider.cs
new file mode 100644
index 0000000000..764d99f942
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProvider.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;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.PowerToys.Settings.UI.Library;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public interface IPasteAIProvider
+ {
+ Task IsAvailableAsync(CancellationToken cancellationToken);
+
+ Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress);
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProviderFactory.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProviderFactory.cs
new file mode 100644
index 0000000000..aacc61bec9
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProviderFactory.cs
@@ -0,0 +1,11 @@
+// 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 AdvancedPaste.Services.CustomActions
+{
+ public interface IPasteAIProviderFactory
+ {
+ IPasteAIProvider CreateProvider(PasteAIConfig config);
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs
new file mode 100644
index 0000000000..f4d45ccd74
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.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;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AdvancedPaste.Models;
+using Microsoft.PowerToys.Settings.UI.Library;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public sealed class LocalModelPasteProvider : IPasteAIProvider
+ {
+ private static readonly IReadOnlyCollection SupportedTypes = new[]
+ {
+ AIServiceType.Onnx,
+ AIServiceType.ML,
+ };
+
+ public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new LocalModelPasteProvider(config));
+
+ private readonly PasteAIConfig _config;
+
+ public LocalModelPasteProvider(PasteAIConfig config)
+ {
+ _config = config ?? throw new ArgumentNullException(nameof(config));
+ }
+
+ public Task IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true);
+
+ public Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ // TODO: Implement local model inference logic using _config.LocalModelPath/_config.ModelPath
+ var content = request.InputText ?? string.Empty;
+ request.Usage = AIServiceUsage.None;
+ return Task.FromResult(content);
+ }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIConfig.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIConfig.cs
new file mode 100644
index 0000000000..1d8a60f041
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIConfig.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 AdvancedPaste.Models;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.SemanticKernel.ChatCompletion;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public class PasteAIConfig
+ {
+ public AIServiceType ProviderType { get; set; }
+
+ public string Model { get; set; }
+
+ public string ApiKey { get; set; }
+
+ public string Endpoint { get; set; }
+
+ public string DeploymentName { get; set; }
+
+ public string LocalModelPath { get; set; }
+
+ public string ModelPath { get; set; }
+
+ public string SystemPrompt { get; set; }
+
+ public bool ModerationEnabled { get; set; }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs
new file mode 100644
index 0000000000..7339b4e4e3
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs
@@ -0,0 +1,61 @@
+// 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 Microsoft.PowerToys.Settings.UI.Library;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public sealed class PasteAIProviderFactory : IPasteAIProviderFactory
+ {
+ private static readonly IReadOnlyList ProviderRegistrations = new[]
+ {
+ SemanticKernelPasteProvider.Registration,
+ LocalModelPasteProvider.Registration,
+ FoundryLocalPasteProvider.Registration,
+ };
+
+ private static readonly IReadOnlyDictionary> ProviderFactories = CreateProviderFactories();
+
+ public IPasteAIProvider CreateProvider(PasteAIConfig config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+
+ var serviceType = config.ProviderType;
+ if (serviceType == AIServiceType.Unknown)
+ {
+ serviceType = AIServiceType.OpenAI;
+ config.ProviderType = serviceType;
+ }
+
+ if (!ProviderFactories.TryGetValue(serviceType, out var factory))
+ {
+ throw new NotSupportedException($"Provider {config.ProviderType} not supported");
+ }
+
+ return factory(config);
+ }
+
+ private static IReadOnlyDictionary> CreateProviderFactories()
+ {
+ var map = new Dictionary>();
+
+ foreach (var registration in ProviderRegistrations)
+ {
+ Register(map, registration.SupportedTypes, registration.Factory);
+ }
+
+ return map;
+ }
+
+ private static void Register(Dictionary> map, IReadOnlyCollection types, Func factory)
+ {
+ foreach (var type in types)
+ {
+ map[type] = factory;
+ }
+ }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderRegistration.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderRegistration.cs
new file mode 100644
index 0000000000..6bd78450e8
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderRegistration.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public sealed class PasteAIProviderRegistration
+ {
+ public PasteAIProviderRegistration(IReadOnlyCollection supportedTypes, Func factory)
+ {
+ SupportedTypes = supportedTypes ?? throw new ArgumentNullException(nameof(supportedTypes));
+ Factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ }
+
+ public IReadOnlyCollection SupportedTypes { get; }
+
+ public Func Factory { get; }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIRequest.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIRequest.cs
new file mode 100644
index 0000000000..0e15c93e05
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIRequest.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 AdvancedPaste.Models;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public sealed class PasteAIRequest
+ {
+ public string Prompt { get; init; }
+
+ public string InputText { get; init; }
+
+ public string SystemPrompt { get; init; }
+
+ public AIServiceUsage Usage { get; set; } = AIServiceUsage.None;
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs
new file mode 100644
index 0000000000..00517e96d8
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs
@@ -0,0 +1,203 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using AdvancedPaste.Helpers;
+using AdvancedPaste.Models;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Microsoft.SemanticKernel.Connectors.Amazon;
+using Microsoft.SemanticKernel.Connectors.AzureAIInference;
+using Microsoft.SemanticKernel.Connectors.Google;
+using Microsoft.SemanticKernel.Connectors.HuggingFace;
+using Microsoft.SemanticKernel.Connectors.MistralAI;
+using Microsoft.SemanticKernel.Connectors.Ollama;
+using Microsoft.SemanticKernel.Connectors.OpenAI;
+
+namespace AdvancedPaste.Services.CustomActions
+{
+ public sealed class SemanticKernelPasteProvider : IPasteAIProvider
+ {
+ private static readonly IReadOnlyCollection SupportedTypes = new[]
+ {
+ AIServiceType.OpenAI,
+ AIServiceType.AzureOpenAI,
+ AIServiceType.Mistral,
+ AIServiceType.Google,
+ AIServiceType.HuggingFace,
+ AIServiceType.AzureAIInference,
+ AIServiceType.Ollama,
+ AIServiceType.Anthropic,
+ AIServiceType.AmazonBedrock,
+ };
+
+ public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new SemanticKernelPasteProvider(config));
+
+ private readonly PasteAIConfig _config;
+ private readonly AIServiceType _serviceType;
+
+ public SemanticKernelPasteProvider(PasteAIConfig config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+ _config = config;
+ _serviceType = config.ProviderType;
+ if (_serviceType == AIServiceType.Unknown)
+ {
+ _serviceType = AIServiceType.OpenAI;
+ _config.ProviderType = _serviceType;
+ }
+ }
+
+ public IReadOnlyCollection SupportedServiceTypes => SupportedTypes;
+
+ public Task IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true);
+
+ public async Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress)
+ {
+ ArgumentNullException.ThrowIfNull(request);
+
+ var systemPrompt = request.SystemPrompt;
+ if (string.IsNullOrWhiteSpace(systemPrompt))
+ {
+ throw new ArgumentException("System prompt must be provided", nameof(request));
+ }
+
+ var prompt = request.Prompt;
+ var inputText = request.InputText;
+ if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText))
+ {
+ throw new ArgumentException("Prompt and input text must be provided", nameof(request));
+ }
+
+ var userMessageContent = $"""
+ User instructions:
+ {prompt}
+
+ Clipboard Content:
+ {inputText}
+
+ Output:
+ """;
+
+ var executionSettings = CreateExecutionSettings();
+ var kernel = CreateKernel();
+ var modelId = _config.Model;
+
+ IChatCompletionService chatService;
+ if (!string.IsNullOrWhiteSpace(modelId))
+ {
+ try
+ {
+ chatService = kernel.GetRequiredService(modelId);
+ }
+ catch (Exception)
+ {
+ chatService = kernel.GetRequiredService();
+ }
+ }
+ else
+ {
+ chatService = kernel.GetRequiredService();
+ }
+
+ var chatHistory = new ChatHistory();
+ chatHistory.AddSystemMessage(systemPrompt);
+ chatHistory.AddUserMessage(userMessageContent);
+
+ var response = await chatService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel, cancellationToken);
+ chatHistory.Add(response);
+
+ request.Usage = AIServiceUsageHelper.GetOpenAIServiceUsage(response);
+ return response.Content;
+ }
+
+ private Kernel CreateKernel()
+ {
+ var kernelBuilder = Kernel.CreateBuilder();
+ var endpoint = string.IsNullOrWhiteSpace(_config.Endpoint) ? null : _config.Endpoint.Trim();
+ var apiKey = _config.ApiKey?.Trim() ?? string.Empty;
+
+ if (RequiresApiKey(_serviceType) && string.IsNullOrWhiteSpace(apiKey))
+ {
+ throw new InvalidOperationException($"API key is required for {_serviceType} but was not provided.");
+ }
+
+ switch (_serviceType)
+ {
+ case AIServiceType.OpenAI:
+ kernelBuilder.AddOpenAIChatCompletion(_config.Model, apiKey, serviceId: _config.Model);
+ break;
+ case AIServiceType.AzureOpenAI:
+ var deploymentName = string.IsNullOrWhiteSpace(_config.DeploymentName) ? _config.Model : _config.DeploymentName;
+ kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey, serviceId: _config.Model);
+ break;
+ case AIServiceType.Mistral:
+ kernelBuilder.AddMistralChatCompletion(_config.Model, apiKey: apiKey);
+ break;
+ case AIServiceType.Google:
+ kernelBuilder.AddGoogleAIGeminiChatCompletion(_config.Model, apiKey: apiKey);
+ break;
+ case AIServiceType.HuggingFace:
+ kernelBuilder.AddHuggingFaceChatCompletion(_config.Model, apiKey: apiKey);
+ break;
+ case AIServiceType.AzureAIInference:
+ kernelBuilder.AddAzureAIInferenceChatCompletion(_config.Model, apiKey: apiKey, endpoint: new Uri(endpoint));
+ break;
+ case AIServiceType.Ollama:
+ kernelBuilder.AddOllamaChatCompletion(_config.Model, endpoint: new Uri(endpoint));
+ break;
+ case AIServiceType.Anthropic:
+ kernelBuilder.AddBedrockChatCompletionService(_config.Model);
+ break;
+ case AIServiceType.AmazonBedrock:
+ kernelBuilder.AddBedrockChatCompletionService(_config.Model);
+ break;
+
+ default:
+ throw new NotSupportedException($"Provider '{_config.ProviderType}' is not supported by {nameof(SemanticKernelPasteProvider)}");
+ }
+
+ return kernelBuilder.Build();
+ }
+
+ private PromptExecutionSettings CreateExecutionSettings()
+ {
+ return _serviceType switch
+ {
+ AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
+ {
+ Temperature = 0.01,
+ MaxTokens = 2000,
+ FunctionChoiceBehavior = null,
+ },
+ _ => new PromptExecutionSettings(),
+ };
+ }
+
+ private static bool RequiresApiKey(AIServiceType serviceType)
+ {
+ return serviceType switch
+ {
+ AIServiceType.Ollama => false,
+ AIServiceType.Anthropic => false,
+ AIServiceType.AmazonBedrock => false,
+ _ => true,
+ };
+ }
+
+ private static string RequireEndpoint(string endpoint, AIServiceType serviceType)
+ {
+ if (!string.IsNullOrWhiteSpace(endpoint))
+ {
+ return endpoint;
+ }
+
+ throw new InvalidOperationException($"Endpoint is required for {serviceType} but was not provided.");
+ }
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs
new file mode 100644
index 0000000000..2542f7310e
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs
@@ -0,0 +1,188 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using System.Threading;
+using AdvancedPaste.Settings;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Windows.Security.Credentials;
+
+namespace AdvancedPaste.Services;
+
+///
+/// Enhanced credentials provider that supports different AI service types
+/// Keys are stored in Windows Credential Vault with service-specific identifiers
+///
+public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
+{
+ private sealed class CredentialSlot
+ {
+ public AIServiceType ServiceType { get; set; } = AIServiceType.Unknown;
+
+ public string ProviderId { get; set; } = string.Empty;
+
+ public (string Resource, string Username)? Entry { get; set; }
+
+ public string Key { get; set; } = string.Empty;
+ }
+
+ private readonly IUserSettings _userSettings;
+ private readonly CredentialSlot _slot;
+ private readonly Lock _syncRoot = new();
+
+ public EnhancedVaultCredentialsProvider(IUserSettings userSettings)
+ {
+ _userSettings = userSettings ?? throw new ArgumentNullException(nameof(userSettings));
+
+ _slot = new CredentialSlot();
+
+ Refresh();
+ }
+
+ public string GetKey()
+ {
+ using (_syncRoot.EnterScope())
+ {
+ UpdateSlot(forceRefresh: false);
+ return _slot.Key;
+ }
+ }
+
+ public bool IsConfigured()
+ {
+ return !string.IsNullOrEmpty(GetKey());
+ }
+
+ public bool Refresh()
+ {
+ using (_syncRoot.EnterScope())
+ {
+ return UpdateSlot(forceRefresh: true);
+ }
+ }
+
+ private bool UpdateSlot(bool forceRefresh)
+ {
+ var (serviceType, providerId) = ResolveCredentialTarget();
+ var desiredServiceType = NormalizeServiceType(serviceType);
+ providerId ??= string.Empty;
+
+ var hasChanged = false;
+
+ if (_slot.ServiceType != desiredServiceType || !string.Equals(_slot.ProviderId, providerId, StringComparison.Ordinal))
+ {
+ _slot.ServiceType = desiredServiceType;
+ _slot.ProviderId = providerId;
+ _slot.Entry = BuildCredentialEntry(desiredServiceType, providerId);
+ forceRefresh = true;
+ hasChanged = true;
+ }
+
+ if (!forceRefresh)
+ {
+ return hasChanged;
+ }
+
+ var newKey = LoadKey(_slot.Entry);
+ if (!string.Equals(_slot.Key, newKey, StringComparison.Ordinal))
+ {
+ _slot.Key = newKey;
+ hasChanged = true;
+ }
+
+ return hasChanged;
+ }
+
+ private (AIServiceType ServiceType, string ProviderId) ResolveCredentialTarget()
+ {
+ var provider = _userSettings.PasteAIConfiguration?.ActiveProvider;
+ if (provider is null)
+ {
+ return (AIServiceType.OpenAI, string.Empty);
+ }
+
+ return (provider.ServiceTypeKind, provider.Id ?? string.Empty);
+ }
+
+ private static AIServiceType NormalizeServiceType(AIServiceType serviceType)
+ {
+ return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType;
+ }
+
+ private static string LoadKey((string Resource, string Username)? entry)
+ {
+ if (entry is null)
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ var credential = new PasswordVault().Retrieve(entry.Value.Resource, entry.Value.Username);
+ return credential?.Password ?? string.Empty;
+ }
+ catch (Exception)
+ {
+ return string.Empty;
+ }
+ }
+
+ private static (string Resource, string Username)? BuildCredentialEntry(AIServiceType serviceType, string providerId)
+ {
+ string resource;
+ string serviceKey;
+
+ switch (serviceType)
+ {
+ case AIServiceType.OpenAI:
+ resource = "https://platform.openai.com/api-keys";
+ serviceKey = "openai";
+ break;
+ case AIServiceType.AzureOpenAI:
+ resource = "https://azure.microsoft.com/products/ai-services/openai-service";
+ serviceKey = "azureopenai";
+ break;
+ case AIServiceType.AzureAIInference:
+ resource = "https://azure.microsoft.com/products/ai-services/ai-inference";
+ serviceKey = "azureaiinference";
+ break;
+ case AIServiceType.Mistral:
+ resource = "https://console.mistral.ai/account/api-keys";
+ serviceKey = "mistral";
+ break;
+ case AIServiceType.Google:
+ resource = "https://ai.google.dev/";
+ serviceKey = "google";
+ break;
+ case AIServiceType.HuggingFace:
+ resource = "https://huggingface.co/settings/tokens";
+ serviceKey = "huggingface";
+ break;
+ case AIServiceType.FoundryLocal:
+ case AIServiceType.ML:
+ case AIServiceType.Onnx:
+ case AIServiceType.Ollama:
+ case AIServiceType.Anthropic:
+ case AIServiceType.AmazonBedrock:
+ return null;
+ default:
+ return null;
+ }
+
+ string username = $"PowerToys_AdvancedPaste_PasteAI_{serviceKey}_{NormalizeProviderIdentifier(providerId)}";
+ return (resource, username);
+ }
+
+ private static string NormalizeProviderIdentifier(string providerId)
+ {
+ if (string.IsNullOrWhiteSpace(providerId))
+ {
+ return "default";
+ }
+
+ var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray());
+ return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant();
+ }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs
index 54759b7dc8..7aa6f63b19 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs
@@ -4,11 +4,26 @@
namespace AdvancedPaste.Services;
+///
+/// Provides access to AI credentials stored for Advanced Paste scenarios.
+///
public interface IAICredentialsProvider
{
- bool IsConfigured { get; }
+ ///
+ /// Gets a value indicating whether any credential is configured.
+ ///
+ /// when a non-empty credential exists for the active AI provider.
+ bool IsConfigured();
- string Key { get; }
+ ///
+ /// Retrieves the credential for the active AI provider.
+ ///
+ /// Credential string or when missing.
+ string GetKey();
+ ///
+ /// Refreshes the cached credential for the active AI provider.
+ ///
+ /// when the credential changed.
bool Refresh();
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs
deleted file mode 100644
index 75f1df259e..0000000000
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace AdvancedPaste.Services;
-
-public interface ICustomTextTransformService
-{
- Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress);
-}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelRuntimeConfiguration.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelRuntimeConfiguration.cs
new file mode 100644
index 0000000000..d634c13e30
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelRuntimeConfiguration.cs
@@ -0,0 +1,27 @@
+// 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.PowerToys.Settings.UI.Library;
+
+namespace AdvancedPaste.Services;
+
+///
+/// Represents runtime information required to configure an AI kernel service.
+///
+public interface IKernelRuntimeConfiguration
+{
+ AIServiceType ServiceType { get; }
+
+ string ModelName { get; }
+
+ string Endpoint { get; }
+
+ string DeploymentName { get; }
+
+ string ModelPath { get; }
+
+ string SystemPrompt { get; }
+
+ bool ModerationEnabled { get; }
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs
index e921b21e54..0ea9ef40bc 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs
@@ -5,15 +5,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Models.KernelQueryCache;
+using AdvancedPaste.Services.CustomActions;
+using AdvancedPaste.Settings;
using AdvancedPaste.Telemetry;
using ManagedCommon;
+using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
@@ -21,15 +22,20 @@ using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.Services;
-public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheService, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) : IKernelService
+public abstract class KernelServiceBase(
+ IKernelQueryCacheService queryCacheService,
+ IPromptModerationService promptModerationService,
+ IUserSettings userSettings,
+ ICustomActionTransformService customActionTransformService) : IKernelService
{
private const string PromptParameterName = "prompt";
private readonly IKernelQueryCacheService _queryCacheService = queryCacheService;
private readonly IPromptModerationService _promptModerationService = promptModerationService;
- private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
+ private readonly IUserSettings _userSettings = userSettings;
+ private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
- protected abstract string ModelName { get; }
+ protected abstract string AdvancedAIModelName { get; }
protected abstract PromptExecutionSettings PromptExecutionSettings { get; }
@@ -37,6 +43,8 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage);
+ protected abstract IKernelRuntimeConfiguration GetRuntimeConfiguration();
+
public async Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress progress)
{
Logger.LogTrace();
@@ -132,21 +140,20 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt, CancellationToken cancellationToken)
{
+ var runtimeConfig = GetRuntimeConfiguration();
+
ChatHistory chatHistory = [];
- chatHistory.AddSystemMessage("""
- You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task.
- You never need to ask permission, always try to do as the user asks. The user will only input one message and will not be available for further questions, so try your best.
- The user will put in a request to format their clipboard data and you will fulfill it.
- You will not directly see the output clipboard content, and do not need to provide it in the chat. You just need to do the transform operations as needed.
- If you are unable to fulfill the request, end with an error message in the language of the user's request.
- """);
+ chatHistory.AddSystemMessage(runtimeConfig.SystemPrompt);
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
chatHistory.AddUserMessage(prompt);
- await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
+ if (ShouldModerateAdvancedAI())
+ {
+ await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
+ }
- var chatResult = await kernel.GetRequiredService()
+ var chatResult = await kernel.GetRequiredService(AdvancedAIModelName)
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken);
chatHistory.Add(chatResult);
@@ -175,10 +182,18 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
return ([], AIServiceUsage.None);
}
+ protected IUserSettings UserSettings => _userSettings;
+
private void LogResult(bool cacheUsed, bool isSavedQuery, IEnumerable actionChain, AIServiceUsage usage)
{
- AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, ModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
+ AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, AdvancedAIModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
+
+ // Log endpoint usage
+ var runtimeConfig = GetRuntimeConfiguration();
+ var endpointEvent = new AdvancedPasteEndpointUsageEvent(runtimeConfig.ServiceType);
+ PowerToysTelemetry.Log.WriteEvent(endpointEvent);
+
var logEvent = new AIServiceFormatEvent(telemetryEvent);
Logger.LogDebug($"{nameof(TransformClipboardAsync)} complete; {logEvent.ToJsonString()}");
}
@@ -191,20 +206,96 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
return kernelBuilder.Build();
}
- private IEnumerable GetKernelFunctions() =>
- from format in Enum.GetValues()
- let metadata = PasteFormat.MetadataDict[format]
- let coreDescription = metadata.KernelFunctionDescription
- where !string.IsNullOrEmpty(coreDescription)
- let requiresPrompt = metadata.RequiresPrompt
- orderby requiresPrompt descending
- select KernelFunctionFactory.CreateFromMethod(
- method: requiresPrompt ? async (Kernel kernel, string prompt) => await ExecutePromptTransformAsync(kernel, format, prompt)
- : async (Kernel kernel) => await ExecuteStandardTransformAsync(kernel, format),
- functionName: format.ToString(),
- description: requiresPrompt ? coreDescription : $"{coreDescription} Puts the result back on the clipboard.",
- parameters: requiresPrompt ? [new(PromptParameterName) { Description = "Input instructions to AI", ParameterType = typeof(string) }] : null,
- returnParameter: new() { Description = "Array of available clipboard formats after operation" });
+ private IEnumerable GetKernelFunctions()
+ {
+ // Get standard format functions
+ var standardFunctions =
+ from format in Enum.GetValues()
+ let metadata = PasteFormat.MetadataDict[format]
+ let coreDescription = metadata.KernelFunctionDescription
+ where !string.IsNullOrEmpty(coreDescription)
+ let requiresPrompt = metadata.RequiresPrompt
+ orderby requiresPrompt descending
+ select KernelFunctionFactory.CreateFromMethod(
+ method: requiresPrompt ? async (Kernel kernel, string prompt) => await ExecutePromptTransformAsync(kernel, format, prompt)
+ : async (Kernel kernel) => await ExecuteStandardTransformAsync(kernel, format),
+ functionName: format.ToString(),
+ description: requiresPrompt ? coreDescription : $"{coreDescription} Puts the result back on the clipboard.",
+ parameters: requiresPrompt ? [new(PromptParameterName) { Description = "Input instructions to AI", ParameterType = typeof(string) }] : null,
+ returnParameter: new() { Description = "Array of available clipboard formats after operation" });
+
+ HashSet usedFunctionNames = new(Enum.GetNames(), StringComparer.OrdinalIgnoreCase);
+
+ // Get custom action functions
+ var customActionFunctions = _userSettings.CustomActions
+ .Where(customAction => !string.IsNullOrWhiteSpace(customAction.Name) && !string.IsNullOrWhiteSpace(customAction.Prompt))
+ .Select(customAction =>
+ {
+ var sanitizedBaseName = SanitizeFunctionName(customAction.Name);
+ var functionName = GetUniqueFunctionName(sanitizedBaseName, usedFunctionNames, customAction.Id);
+ var description = string.IsNullOrWhiteSpace(customAction.Description)
+ ? $"Runs the \"{customAction.Name}\" custom action."
+ : customAction.Description;
+ return KernelFunctionFactory.CreateFromMethod(
+ method: async (Kernel kernel) => await ExecuteCustomActionAsync(kernel, customAction.Prompt),
+ functionName: functionName,
+ description: description,
+ parameters: null,
+ returnParameter: new() { Description = "Array of available clipboard formats after operation" });
+ });
+
+ return standardFunctions.Concat(customActionFunctions);
+ }
+
+ private static string GetUniqueFunctionName(string baseName, HashSet usedFunctionNames, int customActionId)
+ {
+ ArgumentNullException.ThrowIfNull(usedFunctionNames);
+
+ var candidate = string.IsNullOrEmpty(baseName) ? "_CustomAction" : baseName;
+
+ if (usedFunctionNames.Add(candidate))
+ {
+ return candidate;
+ }
+
+ int suffix = 1;
+ while (true)
+ {
+ var nextCandidate = $"{candidate}_{customActionId}_{suffix}";
+ if (usedFunctionNames.Add(nextCandidate))
+ {
+ return nextCandidate;
+ }
+
+ suffix++;
+ }
+ }
+
+ private static string SanitizeFunctionName(string name)
+ {
+ // Remove invalid characters and ensure the function name is valid for kernel
+ var sanitized = new string(name.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray());
+
+ // Ensure it starts with a letter or underscore
+ if (sanitized.Length > 0 && !char.IsLetter(sanitized[0]) && sanitized[0] != '_')
+ {
+ sanitized = "_" + sanitized;
+ }
+
+ // Ensure it's not empty
+ return string.IsNullOrEmpty(sanitized) ? "_CustomAction" : sanitized;
+ }
+
+ private Task ExecuteCustomActionAsync(Kernel kernel, string fixedPrompt) =>
+ ExecuteTransformAsync(
+ kernel,
+ new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }),
+ async dataPackageView =>
+ {
+ var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
+ var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
+ return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty);
+ });
private Task ExecutePromptTransformAsync(Kernel kernel, PasteFormats format, string prompt) =>
ExecuteTransformAsync(
@@ -212,7 +303,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }),
async dataPackageView =>
{
- var input = await dataPackageView.GetTextAsync();
+ var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken());
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
return DataPackageHelpers.CreateFromText(output);
});
@@ -220,7 +311,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
private async Task GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress progress) =>
format switch
{
- PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress),
+ PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty,
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
};
@@ -281,4 +372,9 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
var usageString = usage.HasUsage ? $" [{usage}]" : string.Empty;
return $"-> {role}: {redactedContent}{usageString}";
}
+
+ protected virtual bool ShouldModerateAdvancedAI()
+ {
+ return false;
+ }
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs
deleted file mode 100644
index b6aa156b9d..0000000000
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-using System.Text.Json;
-using System.Threading;
-using System.Threading.Tasks;
-
-using AdvancedPaste.Helpers;
-using AdvancedPaste.Models;
-using AdvancedPaste.Telemetry;
-using Azure;
-using Azure.AI.OpenAI;
-using ManagedCommon;
-using Microsoft.PowerToys.Telemetry;
-
-namespace AdvancedPaste.Services.OpenAI;
-
-public sealed class CustomTextTransformService(IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService) : ICustomTextTransformService
-{
- private const string ModelName = "gpt-3.5-turbo-instruct";
-
- private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
- private readonly IPromptModerationService _promptModerationService = promptModerationService;
-
- private async Task GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
- {
- var fullPrompt = systemInstructions + "\n\n" + userMessage;
-
- await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
-
- OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
-
- var response = await azureAIClient.GetCompletionsAsync(
- new()
- {
- DeploymentName = ModelName,
- Prompts =
- {
- fullPrompt,
- },
- Temperature = 0.01F,
- MaxTokens = 2000,
- },
- cancellationToken);
-
- if (response.Value.Choices[0].FinishReason == "length")
- {
- Logger.LogDebug("Cut off due to length constraints");
- }
-
- return response;
- }
-
- public async Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress)
- {
- if (string.IsNullOrWhiteSpace(prompt))
- {
- return string.Empty;
- }
-
- if (string.IsNullOrWhiteSpace(inputText))
- {
- Logger.LogWarning("Clipboard has no usable text data");
- return string.Empty;
- }
-
- string systemInstructions =
-$@"You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it.
-Do not output anything else besides the reformatted clipboard content.";
-
- string userMessage =
-$@"User instructions:
-{prompt}
-
-Clipboard Content:
-{inputText}
-
-Output:
-";
-
- try
- {
- var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
-
- var usage = response.Usage;
- AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
- PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
- var logEvent = new AIServiceFormatEvent(telemetryEvent);
-
- Logger.LogDebug($"{nameof(TransformTextAsync)} complete; {logEvent.ToJsonString()}");
-
- return response.Choices[0].Text;
- }
- catch (Exception ex)
- {
- Logger.LogError($"{nameof(TransformTextAsync)} failed", ex);
-
- AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
- PowerToysTelemetry.Log.WriteEvent(errorEvent);
-
- if (ex is PasteActionException or OperationCanceledException)
- {
- throw;
- }
- else
- {
- throw new PasteActionException(ErrorHelpers.TranslateErrorText((ex as RequestFailedException)?.Status ?? -1), ex);
- }
- }
- }
-}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs
deleted file mode 100644
index b19a6d51cb..0000000000
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Collections.Generic;
-
-using AdvancedPaste.Models;
-using Azure.AI.OpenAI;
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
-
-namespace AdvancedPaste.Services.OpenAI;
-
-public sealed class KernelService(IKernelQueryCacheService queryCacheService, IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) :
- KernelServiceBase(queryCacheService, promptModerationService, customTextTransformService)
-{
- private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
-
- protected override string ModelName => "gpt-4o";
-
- protected override PromptExecutionSettings PromptExecutionSettings =>
- new OpenAIPromptExecutionSettings()
- {
- ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions,
- Temperature = 0.01,
- };
-
- protected override void AddChatCompletionService(IKernelBuilder kernelBuilder) => kernelBuilder.AddOpenAIChatCompletion(ModelName, _aiCredentialsProvider.Key);
-
- protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage) =>
- chatMessage.Metadata?.GetValueOrDefault("Usage") is CompletionsUsage completionsUsage
- ? new(PromptTokens: completionsUsage.PromptTokens, CompletionTokens: completionsUsage.CompletionTokens)
- : AIServiceUsage.None;
-}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
index 0ca15e4161..2668300526 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
+using AdvancedPaste.Services;
using ManagedCommon;
using OpenAI.Moderations;
@@ -23,7 +24,16 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials
{
try
{
- ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
+ _aiCredentialsProvider.Refresh();
+ var apiKey = _aiCredentialsProvider.GetKey()?.Trim() ?? string.Empty;
+
+ if (string.IsNullOrEmpty(apiKey))
+ {
+ Logger.LogWarning("Skipping OpenAI moderation because no credential is configured.");
+ return;
+ }
+
+ ModerationClient moderationClient = new(ModelName, apiKey);
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
var moderationResult = moderationClientResult.Value;
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/VaultCredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/VaultCredentialsProvider.cs
deleted file mode 100644
index 169c1c2422..0000000000
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/VaultCredentialsProvider.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System;
-
-using Windows.Security.Credentials;
-
-namespace AdvancedPaste.Services.OpenAI;
-
-public sealed class VaultCredentialsProvider : IAICredentialsProvider
-{
- public VaultCredentialsProvider() => Refresh();
-
- public string Key { get; private set; }
-
- public bool IsConfigured => !string.IsNullOrEmpty(Key);
-
- public bool Refresh()
- {
- var oldKey = Key;
- Key = LoadKey();
- return oldKey != Key;
- }
-
- private static string LoadKey()
- {
- try
- {
- return new PasswordVault().Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey")?.Password ?? string.Empty;
- }
- catch (Exception)
- {
- return string.Empty;
- }
- }
-}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs
index 5d6740977b..aef9e39bb9 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs
@@ -8,15 +8,16 @@ using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
+using AdvancedPaste.Services.CustomActions;
using Microsoft.PowerToys.Telemetry;
using Windows.ApplicationModel.DataTransfer;
namespace AdvancedPaste.Services;
-public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTextTransformService customTextTransformService) : IPasteFormatExecutor
+public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService) : IPasteFormatExecutor
{
private readonly IKernelService _kernelService = kernelService;
- private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
+ private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService;
public async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress progress)
{
@@ -36,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
pasteFormat.Format switch
{
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
- PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)),
+ PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty),
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
});
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
index 604cbf403b..f6c66154af 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw
@@ -144,16 +144,67 @@
The paste operation was moderated due to sensitive content. Please try another query.
-
+
Clipboard history
Clipboard history
+
+ AI provider selector
+
+
+ Select an AI provider
+
+
+ Active provider: {0}
+
+
+ AI providers
+
+
+ No AI providers configured
+
+
+ Configure models in Settings
+
Image data
Label used to represent an image in the clipboard history
+
+ Text
+
+
+ Image
+
+
+ Audio
+
+
+ Video
+
+
+ File
+
+
+ Clipboard
+
+
+ Copied just now
+
+
+ Copied {0} sec ago
+
+
+ Copied {0} min ago
+
+
+ Copied {0} hr ago
+
+
+ Copied {0} day ago
+
More options
@@ -196,7 +247,7 @@
Transcode to .mp3
Option to transcode audio files to MP3 format
-
+
Transcode to .mp4 (H.264/AAC)
Option to transcode video files to MP4 format with H.264 video codec and AAC audio codec
@@ -272,11 +323,11 @@
Next result
-
- OpenAI Privacy
+
+ Privacy Policy
-
- OpenAI Terms
+
+ Terms
To custom with AI is disabled by your organization
@@ -287,4 +338,27 @@
PowerToys_Paste_
+
+ Just now
+
+
+ 1 minute ago
+
+
+ {0} minutes ago
+
+
+ Today, {0}
+
+
+ Yesterday, {0}
+
+
+ {0}, {1}
+ (e.g., “Wednesday, 17:05”)
+
+
+ {0}, {1}
+ (e.g., “10/20/2025, 17:05” in the user’s locale)
+
\ No newline at end of file
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteEndpointUsageEvent.cs b/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteEndpointUsageEvent.cs
new file mode 100644
index 0000000000..04777eff79
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteEndpointUsageEvent.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.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Tracing;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace AdvancedPaste.Telemetry;
+
+[EventData]
+[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
+public class AdvancedPasteEndpointUsageEvent : EventBase, IEvent
+{
+ ///
+ /// Gets or sets the AI provider type (e.g., OpenAI, AzureOpenAI, Anthropic).
+ ///
+ public string ProviderType { get; set; }
+
+ public AdvancedPasteEndpointUsageEvent(AIServiceType providerType)
+ {
+ ProviderType = providerType.ToString();
+ }
+
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
index 688c3047e2..0cda4fc6e5 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
+using System.Globalization;
using System.IO.Abstractions;
using System.Linq;
using System.Runtime.InteropServices;
@@ -22,6 +23,8 @@ using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.System;
@@ -37,12 +40,20 @@ namespace AdvancedPaste.ViewModels
private readonly DispatcherTimer _clipboardTimer;
private readonly IUserSettings _userSettings;
private readonly IPasteFormatExecutor _pasteFormatExecutor;
- private readonly IAICredentialsProvider _aiCredentialsProvider;
+ private readonly IAICredentialsProvider _credentialsProvider;
private CancellationTokenSource _pasteActionCancellationTokenSource;
+ private string _currentClipboardHistoryId;
+ private DateTimeOffset? _currentClipboardTimestamp;
+ private ClipboardFormat _lastClipboardFormats = ClipboardFormat.None;
+ private bool _clipboardHistoryUnavailableLogged;
+
public DataPackageView ClipboardData { get; set; }
+ [ObservableProperty]
+ private ClipboardItem _currentClipboardItem;
+
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))]
[NotifyPropertyChangedFor(nameof(ClipboardHasData))]
@@ -58,6 +69,13 @@ namespace AdvancedPaste.ViewModels
[NotifyPropertyChangedFor(nameof(CustomAIUnavailableErrorText))]
[NotifyPropertyChangedFor(nameof(IsCustomAIServiceEnabled))]
[NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))]
+ [NotifyPropertyChangedFor(nameof(AllowedAIProviders))]
+ [NotifyPropertyChangedFor(nameof(ActiveAIProvider))]
+ [NotifyPropertyChangedFor(nameof(ActiveAIProviderTooltip))]
+ [NotifyPropertyChangedFor(nameof(TermsLinkUri))]
+ [NotifyPropertyChangedFor(nameof(PrivacyLinkUri))]
+ [NotifyPropertyChangedFor(nameof(HasTermsLink))]
+ [NotifyPropertyChangedFor(nameof(HasPrivacyLink))]
private bool _isAllowedByGPO;
[ObservableProperty]
@@ -79,11 +97,129 @@ namespace AdvancedPaste.ViewModels
public ObservableCollection CustomActionPasteFormats { get; } = [];
- public bool IsCustomAIServiceEnabled => IsAllowedByGPO && _aiCredentialsProvider.IsConfigured;
+ public bool IsCustomAIServiceEnabled
+ {
+ get
+ {
+ if (!IsAllowedByGPO || !_userSettings.IsAIEnabled)
+ {
+ return false;
+ }
+
+ // Check if there are any allowed providers
+ if (!AllowedAIProviders.Any())
+ {
+ return false;
+ }
+
+ // We should handle the IsAIEnabled logic in settings, don't check again here.
+ // If setting says yes, and here should pass check, and if error happens, it happens.
+ return true;
+ }
+ }
public bool IsCustomAIAvailable => IsCustomAIServiceEnabled && ClipboardHasDataForCustomAI;
- public bool IsAdvancedAIEnabled => IsCustomAIServiceEnabled && _userSettings.IsAdvancedAIEnabled;
+ public bool IsAdvancedAIEnabled
+ {
+ get
+ {
+ if (!IsAllowedByGPO || !_userSettings.IsAIEnabled)
+ {
+ return false;
+ }
+
+ if (!TryResolveAdvancedAIProvider(out _))
+ {
+ return false;
+ }
+
+ return _credentialsProvider.IsConfigured();
+ }
+ }
+
+ public ObservableCollection AIProviders => _userSettings?.PasteAIConfiguration?.Providers ?? new ObservableCollection();
+
+ public IEnumerable AllowedAIProviders
+ {
+ get
+ {
+ var providers = AIProviders;
+ if (providers is null || providers.Count == 0)
+ {
+ return Enumerable.Empty();
+ }
+
+ return providers.Where(IsProviderAllowedByGPO);
+ }
+ }
+
+ public PasteAIProviderDefinition ActiveAIProvider
+ {
+ get
+ {
+ var provider = _userSettings?.PasteAIConfiguration?.ActiveProvider;
+ if (provider is null || !IsProviderAllowedByGPO(provider))
+ {
+ return null;
+ }
+
+ return provider;
+ }
+ }
+
+ public string ActiveAIProviderTooltip
+ {
+ get
+ {
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+ var provider = ActiveAIProvider;
+
+ if (provider is null)
+ {
+ return resourceLoader.GetString("AIProviderButtonTooltipEmpty");
+ }
+
+ var format = resourceLoader.GetString("AIProviderButtonTooltipFormat");
+ var displayName = provider.DisplayName;
+
+ if (!string.IsNullOrEmpty(format))
+ {
+ return string.Format(CultureInfo.CurrentCulture, format, displayName);
+ }
+
+ return displayName;
+ }
+ }
+
+ private AIServiceTypeMetadata GetActiveProviderMetadata()
+ {
+ var provider = ActiveAIProvider ?? AllowedAIProviders.FirstOrDefault();
+ var serviceType = provider?.ServiceTypeKind ?? AIServiceType.OpenAI;
+ return AIServiceTypeRegistry.GetMetadata(serviceType);
+ }
+
+ public Uri TermsLinkUri
+ {
+ get
+ {
+ var metadata = GetActiveProviderMetadata();
+ return metadata.HasTermsLink ? metadata.TermsUri : null;
+ }
+ }
+
+ public Uri PrivacyLinkUri
+ {
+ get
+ {
+ var metadata = GetActiveProviderMetadata();
+ return metadata.HasPrivacyLink ? metadata.PrivacyUri : null;
+ }
+ }
+
+ public bool HasTermsLink => GetActiveProviderMetadata().HasTermsLink;
+
+ public bool HasPrivacyLink => GetActiveProviderMetadata().HasPrivacyLink;
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
@@ -91,7 +227,10 @@ namespace AdvancedPaste.ViewModels
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
- private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
+ private PasteFormats CustomAIFormat =>
+ _userSettings.IsAIEnabled && TryResolveAdvancedAIProvider(out _)
+ ? PasteFormats.KernelQuery
+ : PasteFormats.CustomTextTransformation;
private bool Visible
{
@@ -110,9 +249,9 @@ namespace AdvancedPaste.ViewModels
public event EventHandler PreviewRequested;
- public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider aiCredentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
+ public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
{
- _aiCredentialsProvider = aiCredentialsProvider;
+ _credentialsProvider = credentialsProvider;
_userSettings = userSettings;
_pasteFormatExecutor = pasteFormatExecutor;
@@ -130,6 +269,7 @@ namespace AdvancedPaste.ViewModels
_clipboardTimer.Start();
RefreshPasteFormats();
+ UpdateAIProviderActiveFlags();
_userSettings.Changed += UserSettings_Changed;
PropertyChanged += (_, e) =>
{
@@ -158,15 +298,20 @@ namespace AdvancedPaste.ViewModels
if (Visible)
{
await ReadClipboardAsync();
- UpdateAllowedByGPO();
}
}
private void UserSettings_Changed(object sender, EventArgs e)
{
+ UpdateAIProviderActiveFlags();
+ OnPropertyChanged(nameof(IsCustomAIServiceEnabled));
OnPropertyChanged(nameof(ClipboardHasDataForCustomAI));
OnPropertyChanged(nameof(IsCustomAIAvailable));
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
+ OnPropertyChanged(nameof(AIProviders));
+ OnPropertyChanged(nameof(AllowedAIProviders));
+
+ NotifyActiveProviderChanged();
EnqueueRefreshPasteFormats();
}
@@ -192,6 +337,32 @@ namespace AdvancedPaste.ViewModels
private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery) =>
PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled);
+ private void UpdateAIProviderActiveFlags()
+ {
+ var providers = _userSettings?.PasteAIConfiguration?.Providers;
+ if (providers is not null)
+ {
+ var activeId = ActiveAIProvider?.Id;
+
+ foreach (var provider in providers)
+ {
+ provider.IsActive = !string.IsNullOrEmpty(activeId) && string.Equals(provider.Id, activeId, StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ NotifyActiveProviderChanged();
+ }
+
+ private void NotifyActiveProviderChanged()
+ {
+ OnPropertyChanged(nameof(ActiveAIProvider));
+ OnPropertyChanged(nameof(ActiveAIProviderTooltip));
+ OnPropertyChanged(nameof(TermsLinkUri));
+ OnPropertyChanged(nameof(PrivacyLinkUri));
+ OnPropertyChanged(nameof(HasTermsLink));
+ OnPropertyChanged(nameof(HasPrivacyLink));
+ }
+
private void RefreshPasteFormats()
{
var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey");
@@ -253,8 +424,96 @@ namespace AdvancedPaste.ViewModels
return;
}
- ClipboardData = Clipboard.GetContent();
- AvailableClipboardFormats = await ClipboardData.GetAvailableFormatsAsync();
+ try
+ {
+ ClipboardData = Clipboard.GetContent();
+ AvailableClipboardFormats = ClipboardData != null ? await ClipboardData.GetAvailableFormatsAsync() : ClipboardFormat.None;
+ }
+ catch (Exception ex) when (ex is COMException or InvalidOperationException)
+ {
+ // Logger.LogDebug("Failed to read clipboard content", ex);
+ ClipboardData = null;
+ AvailableClipboardFormats = ClipboardFormat.None;
+ }
+
+ await UpdateClipboardPreviewAsync();
+ }
+
+ private async Task UpdateClipboardPreviewAsync()
+ {
+ if (ClipboardData is null || !ClipboardHasData)
+ {
+ ResetClipboardPreview();
+ _currentClipboardHistoryId = null;
+ _currentClipboardTimestamp = null;
+ _lastClipboardFormats = ClipboardFormat.None;
+ return;
+ }
+
+ var formatsChanged = AvailableClipboardFormats != _lastClipboardFormats;
+ _lastClipboardFormats = AvailableClipboardFormats;
+
+ var clipboardChanged = await UpdateClipboardTimestampAsync(formatsChanged);
+
+ // Create ClipboardItem directly from current clipboard data using helper
+ CurrentClipboardItem = await ClipboardItemHelper.CreateFromCurrentClipboardAsync(
+ ClipboardData,
+ AvailableClipboardFormats,
+ _currentClipboardTimestamp,
+ clipboardChanged ? null : CurrentClipboardItem?.Image);
+ }
+
+ private async Task UpdateClipboardTimestampAsync(bool formatsChanged)
+ {
+ bool clipboardChanged = formatsChanged;
+
+ if (Clipboard.IsHistoryEnabled())
+ {
+ try
+ {
+ var historyItems = await Clipboard.GetHistoryItemsAsync();
+ if (historyItems.Status == ClipboardHistoryItemsResultStatus.Success && historyItems.Items.Count > 0)
+ {
+ var latest = historyItems.Items[0];
+ if (_currentClipboardHistoryId != latest.Id)
+ {
+ clipboardChanged = true;
+ _currentClipboardHistoryId = latest.Id;
+ }
+
+ _currentClipboardTimestamp = latest.Timestamp;
+ _clipboardHistoryUnavailableLogged = false;
+ return clipboardChanged;
+ }
+ }
+ catch (Exception ex)
+ {
+ if (!_clipboardHistoryUnavailableLogged)
+ {
+ Logger.LogDebug("Failed to access clipboard history timestamp", ex.Message);
+ _clipboardHistoryUnavailableLogged = true;
+ }
+ }
+ }
+
+ if (!_currentClipboardTimestamp.HasValue || clipboardChanged)
+ {
+ _currentClipboardTimestamp = DateTimeOffset.Now;
+ clipboardChanged = true;
+ }
+
+ return clipboardChanged;
+ }
+
+ private void ResetClipboardPreview()
+ {
+ // Clear to avoid leaks due to Garbage Collection not clearing the bitmap from memory
+ if (CurrentClipboardItem?.Image is not null)
+ {
+ CurrentClipboardItem.Image.ClearValue(BitmapImage.UriSourceProperty);
+ }
+
+ CurrentClipboardItem = null;
}
public async Task OnShowAsync()
@@ -270,7 +529,7 @@ namespace AdvancedPaste.ViewModels
_dispatcherQueue.TryEnqueue(() =>
{
- GetMainWindow()?.FinishLoading(_aiCredentialsProvider.IsConfigured);
+ GetMainWindow()?.FinishLoading(IsCustomAIServiceEnabled);
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
OnPropertyChanged(nameof(CustomAIUnavailableErrorText));
OnPropertyChanged(nameof(IsCustomAIServiceEnabled));
@@ -319,7 +578,7 @@ namespace AdvancedPaste.ViewModels
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
}
- if (!_aiCredentialsProvider.IsConfigured)
+ if (!IsCustomAIServiceEnabled)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
}
@@ -515,11 +774,114 @@ namespace AdvancedPaste.ViewModels
IsAllowedByGPO = PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled;
}
+ private bool IsProviderAllowedByGPO(PasteAIProviderDefinition provider)
+ {
+ if (provider is null)
+ {
+ return false;
+ }
+
+ var serviceType = provider.ServiceType.ToAIServiceType();
+ var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
+
+ // Check global online AI GPO for online services
+ if (metadata.IsOnlineService && !IsAllowedByGPO)
+ {
+ return false;
+ }
+
+ // Check individual endpoint GPO
+ return serviceType switch
+ {
+ AIServiceType.OpenAI => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOpenAIValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
+ AIServiceType.AzureOpenAI => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteAzureOpenAIValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
+ AIServiceType.AzureAIInference => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteAzureAIInferenceValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
+ AIServiceType.Mistral => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteMistralValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
+ AIServiceType.Google => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteGoogleValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
+ AIServiceType.Anthropic => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteAnthropicValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
+ AIServiceType.Ollama => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOllamaValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
+ AIServiceType.FoundryLocal => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteFoundryLocalValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled,
+ _ => true, // Allow unknown types by default
+ };
+ }
+
+ private bool TryResolveAdvancedAIProvider(out PasteAIProviderDefinition provider)
+ {
+ provider = null;
+
+ var configuration = _userSettings?.PasteAIConfiguration;
+ if (configuration is null)
+ {
+ return false;
+ }
+
+ var activeProvider = configuration.ActiveProvider;
+ if (IsAdvancedAIProvider(activeProvider))
+ {
+ provider = activeProvider;
+ return true;
+ }
+
+ if (activeProvider is not null)
+ {
+ return false;
+ }
+
+ var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedAIProvider);
+ if (fallback is not null)
+ {
+ provider = fallback;
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsAdvancedAIProvider(PasteAIProviderDefinition provider)
+ {
+ return provider is not null && provider.EnableAdvancedAI && SupportsAdvancedAI(provider.ServiceTypeKind);
+ }
+
+ private static bool SupportsAdvancedAI(AIServiceType serviceType)
+ {
+ return serviceType is AIServiceType.OpenAI
+ or AIServiceType.AzureOpenAI;
+ }
+
private bool UpdateOpenAIKey()
{
UpdateAllowedByGPO();
- return IsAllowedByGPO && _aiCredentialsProvider.Refresh();
+ return _credentialsProvider.Refresh();
+ }
+
+ [RelayCommand]
+ private async Task SetActiveProviderAsync(PasteAIProviderDefinition provider)
+ {
+ if (provider is null || string.IsNullOrEmpty(provider.Id))
+ {
+ return;
+ }
+
+ if (string.Equals(ActiveAIProvider?.Id, provider.Id, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ try
+ {
+ await _userSettings.SetActiveAIProviderAsync(provider.Id);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to activate AI provider", ex);
+ return;
+ }
+
+ UpdateAIProviderActiveFlags();
+ OnPropertyChanged(nameof(AIProviders));
+ NotifyActiveProviderChanged();
+ EnqueueRefreshPasteFormats();
}
public async Task CancelPasteActionAsync()
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj
index 083aa868d3..2cf2920673 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj
@@ -2,7 +2,7 @@
-
+
15.0
diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
index 6af0d636ac..c7d22d474f 100644
--- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
+++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp
@@ -16,7 +16,8 @@
#include
#include
-#include
+#include
+#include
#include
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
@@ -54,12 +55,14 @@ namespace
const wchar_t JSON_KEY_ADVANCED_PASTE_UI_HOTKEY[] = L"advanced-paste-ui-hotkey";
const wchar_t JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY[] = L"paste-as-markdown-hotkey";
const wchar_t JSON_KEY_PASTE_AS_JSON_HOTKEY[] = L"paste-as-json-hotkey";
- const wchar_t JSON_KEY_IS_ADVANCED_AI_ENABLED[] = L"IsAdvancedAIEnabled";
+ const wchar_t JSON_KEY_IS_AI_ENABLED[] = L"IsAIEnabled";
+ const wchar_t JSON_KEY_IS_OPEN_AI_ENABLED[] = L"IsOpenAIEnabled";
const wchar_t JSON_KEY_SHOW_CUSTOM_PREVIEW[] = L"ShowCustomPreview";
+ const wchar_t JSON_KEY_PASTE_AI_CONFIGURATION[] = L"paste-ai-configuration";
+ const wchar_t JSON_KEY_PROVIDERS[] = L"providers";
+ const wchar_t JSON_KEY_SERVICE_TYPE[] = L"service-type";
+ const wchar_t JSON_KEY_ENABLE_ADVANCED_AI[] = L"enable-advanced-ai";
const wchar_t JSON_KEY_VALUE[] = L"value";
-
- const wchar_t OPENAI_VAULT_RESOURCE[] = L"https://platform.openai.com/api-keys";
- const wchar_t OPENAI_VAULT_USERNAME[] = L"PowerToys_AdvancedPaste_OpenAIKey";
}
class AdvancedPaste : public PowertoyModuleIface
@@ -94,6 +97,7 @@ private:
using CustomAction = ActionData;
std::vector m_custom_actions;
+ bool m_is_ai_enabled = false;
bool m_is_advanced_ai_enabled = false;
bool m_preview_custom_format_output = true;
@@ -145,32 +149,11 @@ private:
return jsonObject;
}
- static bool open_ai_key_exists()
- {
- try
- {
- winrt::Windows::Security::Credentials::PasswordVault().Retrieve(OPENAI_VAULT_RESOURCE, OPENAI_VAULT_USERNAME);
- return true;
- }
- catch (const winrt::hresult_error& ex)
- {
- // Looks like the only way to access the PasswordVault is through an API that throws an exception in case the resource doesn't exist.
- // If the debugger breaks here, just continue.
- // If you want to disable breaking here in a more permanent way, just add a condition in Visual Studio's Exception Settings to not break on win::hresult_error, but that might make you not hit other exceptions you might want to catch.
- if (ex.code() == HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
- {
- return false; // Credential doesn't exist.
- }
- Logger::error("Unexpected error while retrieving OpenAI key from vault: {}", winrt::to_string(ex.message()));
- return false;
- }
- }
-
- bool is_open_ai_enabled()
+ bool is_ai_enabled()
{
return gpo_policy_enabled_configuration() != powertoys_gpo::gpo_rule_configured_disabled &&
powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue() != powertoys_gpo::gpo_rule_configured_disabled &&
- open_ai_key_exists();
+ m_is_ai_enabled;
}
static std::wstring kebab_to_pascal_case(const std::wstring& kebab_str)
@@ -201,6 +184,13 @@ private:
return result;
}
+ static std::wstring to_lower_case(const std::wstring& value)
+ {
+ std::wstring result = value;
+ std::transform(result.begin(), result.end(), result.begin(), [](wchar_t ch) { return std::towlower(ch); });
+ return result;
+ }
+
bool migrate_data_and_remove_data_file(Hotkey& old_paste_as_plain_hotkey)
{
const wchar_t OLD_JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
@@ -267,6 +257,61 @@ private:
}
}
+ bool has_advanced_ai_provider(const winrt::Windows::Data::Json::JsonObject& propertiesObject)
+ {
+ if (!propertiesObject.HasKey(JSON_KEY_PASTE_AI_CONFIGURATION))
+ {
+ return false;
+ }
+
+ const auto configValue = propertiesObject.GetNamedValue(JSON_KEY_PASTE_AI_CONFIGURATION);
+ if (configValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
+ {
+ return false;
+ }
+
+ const auto configObject = configValue.GetObjectW();
+ if (!configObject.HasKey(JSON_KEY_PROVIDERS))
+ {
+ return false;
+ }
+
+ const auto providersValue = configObject.GetNamedValue(JSON_KEY_PROVIDERS);
+ if (providersValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Array)
+ {
+ return false;
+ }
+
+ const auto providers = providersValue.GetArray();
+ for (const auto providerValue : providers)
+ {
+ if (providerValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
+ {
+ continue;
+ }
+
+ const auto providerObject = providerValue.GetObjectW();
+ if (!providerObject.GetNamedBoolean(JSON_KEY_ENABLE_ADVANCED_AI, false))
+ {
+ continue;
+ }
+
+ if (!providerObject.HasKey(JSON_KEY_SERVICE_TYPE))
+ {
+ continue;
+ }
+
+ const std::wstring serviceType = providerObject.GetNamedString(JSON_KEY_SERVICE_TYPE, L"").c_str();
+ const auto normalizedServiceType = to_lower_case(serviceType);
+ if (normalizedServiceType == L"openai" || normalizedServiceType == L"azureopenai")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
void read_settings(PowerToysSettings::PowerToyValues& settings)
{
const auto settingsObject = settings.get_raw_json();
@@ -341,7 +386,7 @@ private:
if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS))
{
const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE);
- if (customActions.Size() > 0 && is_open_ai_enabled())
+ if (customActions.Size() > 0 && is_ai_enabled())
{
for (const auto& customAction : customActions)
{
@@ -365,9 +410,19 @@ private:
{
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
- if (propertiesObject.HasKey(JSON_KEY_IS_ADVANCED_AI_ENABLED))
+ m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject);
+
+ if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED))
{
- m_is_advanced_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_ADVANCED_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE);
+ m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
+ }
+ else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED))
+ {
+ m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false);
+ }
+ else
+ {
+ m_is_ai_enabled = false;
}
if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json
index 31ad05c701..bc0803796e 100644
--- a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json
+++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json
@@ -1 +1 @@
-{"properties":{"IsAdvancedAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}}},"name":"AdvancedPaste","version":"1"}
\ No newline at end of file
+{"properties":{"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}},"paste-ai-configuration":{"active-provider-id":"","providers":[],"use-shared-credentials":true}},"name":"AdvancedPaste","version":"1"}
\ No newline at end of file
diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj
index c3e9e4f3f1..71b535c629 100644
--- a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj
+++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj
@@ -82,8 +82,6 @@
Disabled
_DEBUG;%(PreprocessorDefinitions)
- MultiThreadedDebug
- MultiThreadedDebug
false
@@ -95,8 +93,6 @@
true
true
NDEBUG;%(PreprocessorDefinitions)
- MultiThreaded
- MultiThreaded
true
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml
index c48b7fbb25..ae77a78caa 100644
--- a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml
+++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml
@@ -20,7 +20,7 @@
-
+
_DEBUG;%(PreprocessorDefinitions)
- MultiThreadedDebugDLL
true
true
@@ -49,7 +48,6 @@
NDEBUG;%(PreprocessorDefinitions)
true
true
- MultiThreadedDLL
false
true
false
diff --git a/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithXAML/MainWindow.xaml b/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithXAML/MainWindow.xaml
index 7292173836..01403ba36e 100644
--- a/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithXAML/MainWindow.xaml
+++ b/src/modules/FileLocksmith/FileLocksmithUI/FileLocksmithXAML/MainWindow.xaml
@@ -20,7 +20,7 @@
-
+
_userSettings;
private static Mock _elevationHelper;
+ private static Mock _backupManager;
// Case1: Fuzzing method for ValidIPv4
public static void FuzzValidIPv4(ReadOnlySpan input)
@@ -73,9 +70,10 @@ namespace Hosts.FuzzTests
_userSettings = new Mock();
_elevationHelper = new Mock();
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
+ _backupManager = new Mock();
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
string input = System.Text.Encoding.UTF8.GetString(data);
diff --git a/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj b/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj
index 667cfcc0ad..51dee7a40b 100644
--- a/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj
+++ b/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj
@@ -30,8 +30,11 @@
+
+
+
diff --git a/src/modules/Hosts/Hosts.Tests/BackupManagerTest.cs b/src/modules/Hosts/Hosts.Tests/BackupManagerTest.cs
new file mode 100644
index 0000000000..6aeb834029
--- /dev/null
+++ b/src/modules/Hosts/Hosts.Tests/BackupManagerTest.cs
@@ -0,0 +1,156 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.IO.Abstractions.TestingHelpers;
+using HostsUILib.Helpers;
+using HostsUILib.Settings;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+
+namespace Hosts.Tests
+{
+ [TestClass]
+ public class BackupManagerTest
+ {
+ private const string HostsPath = @"C:\Windows\System32\Drivers\etc\hosts";
+ private const string BackupPath = @"C:\Backup\hosts";
+ private const string BackupSearchPattern = $"*_PowerToysBackup_*";
+
+ [TestMethod]
+ public void Hosts_Backup_Not_Executed()
+ {
+ var fileSystem = new MockFileSystem();
+ SetupFiles(fileSystem, true);
+ var userSettings = new Mock();
+ userSettings.Setup(m => m.BackupHosts).Returns(false);
+ userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
+ var backupManager = new BackupManager(fileSystem, userSettings.Object);
+ backupManager.Create(HostsPath);
+
+ Assert.AreEqual(0, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
+ }
+
+ [TestMethod]
+ public void Hosts_Backup_Executed_Once()
+ {
+ var fileSystem = new MockFileSystem();
+ SetupFiles(fileSystem, true);
+ var userSettings = new Mock();
+ userSettings.Setup(m => m.BackupHosts).Returns(true);
+ userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
+ var backupManager = new BackupManager(fileSystem, userSettings.Object);
+ backupManager.Create(HostsPath);
+ backupManager.Create(HostsPath);
+
+ Assert.AreEqual(1, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
+ var hostsContent = fileSystem.File.ReadAllText(HostsPath);
+ var backupContent = fileSystem.File.ReadAllText(fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern)[0]);
+ Assert.AreEqual(hostsContent, backupContent);
+ }
+
+ [DataTestMethod]
+ [DataRow(-10, -10)]
+ [DataRow(-10, 0)]
+ [DataRow(-10, 10)]
+ [DataRow(0, -10)]
+ [DataRow(0, 0)]
+ [DataRow(0, 10)]
+ [DataRow(10, -10)]
+ [DataRow(10, 0)]
+ [DataRow(10, 10)]
+ public void Hosts_Backups_Delete_Never(int count, int days)
+ {
+ var fileSystem = new MockFileSystem();
+ SetupFiles(fileSystem, false);
+ var userSettings = new Mock();
+ userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
+ userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Never);
+ var backupManager = new BackupManager(fileSystem, userSettings.Object);
+ backupManager.Delete();
+
+ Assert.AreEqual(30, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
+ Assert.AreEqual(31, fileSystem.Directory.GetFiles(BackupPath).Length);
+ }
+
+ [DataTestMethod]
+ [DataRow(-10, 30)]
+ [DataRow(0, 30)]
+ [DataRow(10, 10)]
+ public void Hosts_Backups_Delete_ByCount(int count, int expectedBackups)
+ {
+ var fileSystem = new MockFileSystem();
+ SetupFiles(fileSystem, false);
+ var userSettings = new Mock();
+ userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
+ userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Count);
+ userSettings.Setup(m => m.DeleteBackupsCount).Returns(count);
+ var backupManager = new BackupManager(fileSystem, userSettings.Object);
+ backupManager.Delete();
+
+ Assert.AreEqual(expectedBackups, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
+ Assert.AreEqual(expectedBackups + 1, fileSystem.Directory.GetFiles(BackupPath).Length);
+ }
+
+ [DataTestMethod]
+ [DataRow(-10, -10, 30)]
+ [DataRow(-10, 0, 30)]
+ [DataRow(-10, 10, 5)]
+ [DataRow(0, -10, 30)]
+ [DataRow(0, 0, 30)]
+ [DataRow(0, 10, 5)]
+ [DataRow(10, -10, 30)]
+ [DataRow(10, 0, 30)]
+ [DataRow(5, 1, 5)]
+ [DataRow(1, 15, 10)]
+ [DataRow(2, 2, 2)]
+ public void Hosts_Backups_Delete_ByAge(int count, int days, int expectedBackups)
+ {
+ var fileSystem = new MockFileSystem();
+ SetupFiles(fileSystem, false);
+ var userSettings = new Mock();
+ userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
+ userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Age);
+ userSettings.Setup(m => m.DeleteBackupsCount).Returns(count);
+ userSettings.Setup(m => m.DeleteBackupsDays).Returns(days);
+ var backupManager = new BackupManager(fileSystem, userSettings.Object);
+ backupManager.Delete();
+
+ Assert.AreEqual(expectedBackups, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length);
+ Assert.AreEqual(expectedBackups + 1, fileSystem.Directory.GetFiles(BackupPath).Length);
+ }
+
+ private void SetupFiles(MockFileSystem fileSystem, bool hostsOnly)
+ {
+ fileSystem.AddDirectory(BackupPath);
+ fileSystem.AddFile(HostsPath, new MockFileData("HOSTS FILE CONTENT"));
+
+ if (hostsOnly)
+ {
+ return;
+ }
+
+ var today = new DateTimeOffset(DateTime.Today);
+
+ var notBackupData = new MockFileData("NOT A BACKUP")
+ {
+ CreationTime = today.AddDays(-100),
+ };
+
+ fileSystem.AddFile(fileSystem.Path.Combine(BackupPath, "hosts_not_a_backup"), notBackupData);
+
+ // The first backup is from 5 days ago. There are 30 backups, one for each day.
+ var offset = 5;
+ for (var i = 0; i < 30; i++)
+ {
+ var backupData = new MockFileData("THIS IS A BACKUP")
+ {
+ CreationTime = today.AddDays(-i - offset),
+ };
+
+ fileSystem.AddFile(fileSystem.Path.Combine(BackupPath, $"hosts_PowerToysBackup_{i}"), backupData);
+ }
+ }
+ }
+}
diff --git a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs
index 81052fd101..4c6ee77f8c 100644
--- a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs
+++ b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs
@@ -20,8 +20,10 @@ namespace Hosts.Tests
[TestClass]
public class HostsServiceTest
{
+ private const string BackupPath = @"C:\Backup\hosts";
private static Mock _userSettings;
private static Mock _elevationHelper;
+ private static Mock _backupManager;
[ClassInitialize]
public static void ClassInitialize(TestContext context)
@@ -29,27 +31,7 @@ namespace Hosts.Tests
_userSettings = new Mock();
_elevationHelper = new Mock();
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
- }
-
- [TestMethod]
- public void Hosts_Exists()
- {
- var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
- fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
- var result = service.Exists();
-
- Assert.IsTrue(result);
- }
-
- [TestMethod]
- public void Hosts_Not_Exists()
- {
- var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
- var result = service.Exists();
-
- Assert.IsFalse(result);
+ _backupManager = new Mock();
}
[TestMethod]
@@ -67,7 +49,7 @@ namespace Hosts.Tests
";
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@@ -92,7 +74,7 @@ namespace Hosts.Tests
";
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@@ -118,7 +100,7 @@ namespace Hosts.Tests
";
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@@ -137,7 +119,7 @@ namespace Hosts.Tests
public async Task Empty_Hosts()
{
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
await service.WriteAsync(string.Empty, Enumerable.Empty());
@@ -168,7 +150,7 @@ namespace Hosts.Tests
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock();
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Top);
- var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@@ -200,7 +182,7 @@ namespace Hosts.Tests
var fileSystem = new CustomMockFileSystem();
var userSettings = new Mock();
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom);
- var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@@ -224,7 +206,7 @@ namespace Hosts.Tests
";
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
var data = await service.ReadAsync();
@@ -241,7 +223,7 @@ namespace Hosts.Tests
var elevationHelper = new Mock();
elevationHelper.Setup(m => m.IsElevated).Returns(false);
- var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object, _backupManager.Object);
await Assert.ThrowsExceptionAsync(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty()));
}
@@ -249,7 +231,7 @@ namespace Hosts.Tests
public async Task Save_ReadOnlyHostsException()
{
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
var hostsFile = new MockFileData(string.Empty)
{
@@ -265,7 +247,7 @@ namespace Hosts.Tests
public void Remove_ReadOnly_Attribute()
{
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
var hostsFile = new MockFileData(string.Empty)
{
@@ -284,7 +266,7 @@ namespace Hosts.Tests
public async Task Save_Hidden_Hosts()
{
var fileSystem = new CustomMockFileSystem();
- var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
var hostsFile = new MockFileData(string.Empty)
{
@@ -316,7 +298,7 @@ namespace Hosts.Tests
var fs = new CustomMockFileSystem();
var settings = new Mock();
settings.Setup(s => s.NoLeadingSpaces).Returns(true);
- var svc = new HostsService(fs, settings.Object, _elevationHelper.Object);
+ var svc = new HostsService(fs, settings.Object, _elevationHelper.Object, _backupManager.Object);
fs.AddFile(svc.HostsFilePath, new MockFileData(content));
var data = await svc.ReadAsync();
@@ -327,5 +309,57 @@ namespace Hosts.Tests
var result = fs.GetFile(svc.HostsFilePath);
Assert.AreEqual(expected, result.TextContents);
}
+
+ [TestMethod]
+ public async Task Hosts_Backup_Not_Executed()
+ {
+ var content =
+@"10.1.1.1 host host.local # comment
+10.1.1.2 host2 host2.local # another comment
+";
+
+ var fileSystem = new CustomMockFileSystem();
+ fileSystem.AddDirectory(BackupPath);
+ _userSettings.Setup(m => m.BackupHosts).Returns(false);
+ _userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
+ var backupManager = new BackupManager(fileSystem, _userSettings.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, backupManager);
+
+ fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
+
+ var data = await service.ReadAsync();
+ var entries = data.Entries.ToList();
+ entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false));
+ await service.WriteAsync(data.AdditionalLines, data.Entries);
+
+ Assert.AreEqual(0, fileSystem.Directory.GetFiles(BackupPath).Length);
+ }
+
+ [TestMethod]
+ public async Task Hosts_Backup_Executed_Once()
+ {
+ var content =
+@"10.1.1.1 host host.local # comment
+10.1.1.2 host2 host2.local # another comment
+";
+
+ var fileSystem = new CustomMockFileSystem();
+ _userSettings.Setup(m => m.BackupHosts).Returns(true);
+ _userSettings.Setup(m => m.BackupPath).Returns(BackupPath);
+ var backupManager = new BackupManager(fileSystem, _userSettings.Object);
+ var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, backupManager);
+
+ fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
+
+ var data = await service.ReadAsync();
+ var entries = data.Entries.ToList();
+ entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false));
+ await service.WriteAsync(data.AdditionalLines, data.Entries);
+ await service.WriteAsync(data.AdditionalLines, data.Entries);
+
+ Assert.AreEqual(1, fileSystem.Directory.GetFiles(BackupPath).Length);
+ var backupContent = fileSystem.File.ReadAllText(fileSystem.Directory.GetFiles(BackupPath)[0]);
+ Assert.AreEqual(content, backupContent);
+ }
}
}
diff --git a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs
index fbe5d3662d..8dbec70de8 100644
--- a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs
+++ b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs
@@ -56,6 +56,7 @@ namespace Hosts
{
// Core Services
services.AddSingleton();
+ services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
@@ -74,7 +75,7 @@ namespace Hosts
}).
Build();
- var cleanupBackupThread = new Thread(() =>
+ var deleteBackupThread = new Thread(() =>
{
// Delete old backups only if running elevated
if (!Host.GetService().IsElevated)
@@ -84,7 +85,7 @@ namespace Hosts
try
{
- Host.GetService().CleanupBackup();
+ Host.GetService().Delete();
}
catch (Exception ex)
{
@@ -92,8 +93,8 @@ namespace Hosts
}
});
- cleanupBackupThread.IsBackground = true;
- cleanupBackupThread.Start();
+ deleteBackupThread.IsBackground = true;
+ deleteBackupThread.Start();
UnhandledException += App_UnhandledException;
diff --git a/src/modules/Hosts/Hosts/HostsXAML/MainWindow.xaml b/src/modules/Hosts/Hosts/HostsXAML/MainWindow.xaml
index 92d1594556..001cbeb3ed 100644
--- a/src/modules/Hosts/Hosts/HostsXAML/MainWindow.xaml
+++ b/src/modules/Hosts/Hosts/HostsXAML/MainWindow.xaml
@@ -20,7 +20,7 @@
-
+
-
+
16.0
@@ -46,7 +46,7 @@
- $(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)
+ ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
diff --git a/src/modules/Hosts/HostsUILib/Helpers/BackupManager.cs b/src/modules/Hosts/HostsUILib/Helpers/BackupManager.cs
new file mode 100644
index 0000000000..5417408409
--- /dev/null
+++ b/src/modules/Hosts/HostsUILib/Helpers/BackupManager.cs
@@ -0,0 +1,112 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO.Abstractions;
+using System.Linq;
+using HostsUILib.Settings;
+
+namespace HostsUILib.Helpers
+{
+ public class BackupManager : IBackupManager
+ {
+ private const string BackupSuffix = "_PowerToysBackup_";
+ private readonly IFileSystem _fileSystem;
+ private readonly IUserSettings _userSettings;
+ private bool _backupDone;
+
+ public BackupManager(IFileSystem fileSystem, IUserSettings userSettings)
+ {
+ _fileSystem = fileSystem;
+ _userSettings = userSettings;
+ }
+
+ public void Create(string hostsFilePath)
+ {
+ if (_backupDone || !_userSettings.BackupHosts || !_fileSystem.File.Exists(hostsFilePath))
+ {
+ return;
+ }
+
+ try
+ {
+ if (!_fileSystem.Directory.Exists(_userSettings.BackupPath))
+ {
+ _fileSystem.Directory.CreateDirectory(_userSettings.BackupPath);
+ }
+
+ var backupPath = _fileSystem.Path.Combine(_userSettings.BackupPath, $"hosts{BackupSuffix}{DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)}");
+
+ _fileSystem.File.Copy(hostsFilePath, backupPath);
+ _backupDone = true;
+ }
+ catch (Exception ex)
+ {
+ LoggerInstance.Logger.LogError("Backup failed", ex);
+ }
+ }
+
+ public void Delete()
+ {
+ switch (_userSettings.DeleteBackupsMode)
+ {
+ case HostsDeleteBackupMode.Count:
+ DeleteByCount(_userSettings.DeleteBackupsCount);
+ break;
+ case HostsDeleteBackupMode.Age:
+ DeleteByAge(_userSettings.DeleteBackupsDays, _userSettings.DeleteBackupsCount);
+ break;
+ }
+ }
+
+ public void DeleteByCount(int count)
+ {
+ if (count < 1)
+ {
+ return;
+ }
+
+ var backups = GetAll().OrderByDescending(f => f.CreationTime).Skip(count).ToArray();
+ DeleteAll(backups);
+ }
+
+ public void DeleteByAge(int days, int count)
+ {
+ if (days < 1)
+ {
+ return;
+ }
+
+ var backupsEnumerable = GetAll();
+
+ if (count > 0)
+ {
+ backupsEnumerable = backupsEnumerable.OrderByDescending(f => f.CreationTime).Skip(count);
+ }
+
+ var backups = backupsEnumerable.Where(f => f.CreationTime < DateTime.Now.AddDays(-days)).ToArray();
+ DeleteAll(backups);
+ }
+
+ private IEnumerable GetAll()
+ {
+ if (!_fileSystem.Directory.Exists(_userSettings.BackupPath))
+ {
+ return [];
+ }
+
+ return _fileSystem.Directory.GetFiles(_userSettings.BackupPath, $"*{BackupSuffix}*").Select(_fileSystem.FileInfo.New);
+ }
+
+ private void DeleteAll(IFileInfo[] files)
+ {
+ foreach (var f in files)
+ {
+ _fileSystem.File.Delete(f.FullName);
+ }
+ }
+ }
+}
diff --git a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs
index 83aa3544b1..9b16e04f20 100644
--- a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs
+++ b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
@@ -23,16 +22,15 @@ namespace HostsUILib.Helpers
{
public partial class HostsService : IHostsService, IDisposable
{
- private const string _backupSuffix = $"_PowerToysBackup_";
- private const int _defaultBufferSize = 4096; // From System.IO.File source code
+ private const int DefaultBufferSize = 4096; // From System.IO.File source code
private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1);
private readonly IFileSystem _fileSystem;
private readonly IUserSettings _userSettings;
private readonly IElevationHelper _elevationHelper;
private readonly IFileSystemWatcher _fileSystemWatcher;
+ private readonly IBackupManager _backupManager;
private readonly string _hostsFilePath;
- private bool _backupDone;
private bool _disposed;
public string HostsFilePath => _hostsFilePath;
@@ -44,11 +42,13 @@ namespace HostsUILib.Helpers
public HostsService(
IFileSystem fileSystem,
IUserSettings userSettings,
- IElevationHelper elevationHelper)
+ IElevationHelper elevationHelper,
+ IBackupManager backupManager)
{
_fileSystem = fileSystem;
_userSettings = userSettings;
_elevationHelper = elevationHelper;
+ _backupManager = backupManager;
_hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc\hosts");
@@ -60,18 +60,13 @@ namespace HostsUILib.Helpers
_fileSystemWatcher.EnableRaisingEvents = true;
}
- public bool Exists()
- {
- return _fileSystem.File.Exists(HostsFilePath);
- }
-
public async Task ReadAsync()
{
var entries = new List();
var unparsedBuilder = new StringBuilder();
var splittedEntries = false;
- if (!Exists())
+ if (!_fileSystem.File.Exists(HostsFilePath))
{
return new HostsData(entries, unparsedBuilder.ToString(), false);
}
@@ -192,15 +187,10 @@ namespace HostsUILib.Helpers
{
await _asyncLock.WaitAsync();
_fileSystemWatcher.EnableRaisingEvents = false;
-
- if (!_backupDone && Exists())
- {
- _fileSystem.File.Copy(HostsFilePath, HostsFilePath + _backupSuffix + DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture));
- _backupDone = true;
- }
+ _backupManager.Create(HostsFilePath);
// FileMode.OpenOrCreate is necessary to prevent UnauthorizedAccessException when the hosts file is hidden
- using var stream = _fileSystem.FileStream.New(HostsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, _defaultBufferSize, FileOptions.Asynchronous);
+ using var stream = _fileSystem.FileStream.New(HostsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, DefaultBufferSize, FileOptions.Asynchronous);
using var writer = new StreamWriter(stream, Encoding);
foreach (var line in lines)
{
@@ -231,15 +221,6 @@ namespace HostsUILib.Helpers
}
}
- public void CleanupBackup()
- {
- Directory.GetFiles(Path.GetDirectoryName(HostsFilePath), $"*{_backupSuffix}*")
- .Select(f => new FileInfo(f))
- .Where(f => f.CreationTime < DateTime.Now.AddDays(-15))
- .ToList()
- .ForEach(f => f.Delete());
- }
-
public void OpenHostsFile()
{
var notepadFallback = false;
diff --git a/src/modules/Hosts/HostsUILib/Helpers/IBackupManager.cs b/src/modules/Hosts/HostsUILib/Helpers/IBackupManager.cs
new file mode 100644
index 0000000000..9da9802a26
--- /dev/null
+++ b/src/modules/Hosts/HostsUILib/Helpers/IBackupManager.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace HostsUILib.Helpers
+{
+ public interface IBackupManager
+ {
+ void Create(string hostsFilePath);
+
+ void Delete();
+ }
+}
diff --git a/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs
index fe75946a12..c6f2678156 100644
--- a/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs
+++ b/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs
@@ -22,8 +22,6 @@ namespace HostsUILib.Helpers
Task PingAsync(string address);
- void CleanupBackup();
-
void OpenHostsFile();
void RemoveReadOnlyAttribute();
diff --git a/src/modules/Hosts/HostsUILib/Settings/HostsDeleteBackupMode.cs b/src/modules/Hosts/HostsUILib/Settings/HostsDeleteBackupMode.cs
new file mode 100644
index 0000000000..d1e1d79ded
--- /dev/null
+++ b/src/modules/Hosts/HostsUILib/Settings/HostsDeleteBackupMode.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace HostsUILib.Settings
+{
+ public enum HostsDeleteBackupMode
+ {
+ Never = 0,
+ Count = 1,
+ Age = 2,
+ }
+}
diff --git a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs
index 46c7a7dab5..4f175398ad 100644
--- a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs
+++ b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs
@@ -16,6 +16,16 @@ namespace HostsUILib.Settings
public HostsEncoding Encoding { get; }
+ public bool BackupHosts { get; }
+
+ public string BackupPath { get; }
+
+ public HostsDeleteBackupMode DeleteBackupsMode { get; }
+
+ public int DeleteBackupsDays { get; }
+
+ public int DeleteBackupsCount { get; }
+
event EventHandler LoopbackDuplicatesChanged;
public delegate void OpenSettingsFunction();
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp
index 7ebe4a67eb..503a98de29 100644
--- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp
@@ -1,4 +1,4 @@
-#include
+#include
#include
#include "ThemeScheduler.h"
#include "ThemeHelper.h"
@@ -15,9 +15,11 @@
SERVICE_STATUS g_ServiceStatus = {};
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
HANDLE g_ServiceStopEvent = nullptr;
-static int g_lastUpdatedDay = -1;
+extern int g_lastUpdatedDay = -1;
static ScheduleMode prevMode = ScheduleMode::Off;
static std::wstring prevLat, prevLon;
+static int prevMinutes = -1;
+static bool lastOverrideStatus = false;
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
@@ -148,7 +150,6 @@ static void update_sun_times(auto& settings)
std::wstring wmsg(e.what(), e.what() + strlen(e.what()));
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
}
-
}
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
@@ -161,136 +162,73 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
Logger::info(L"[LightSwitchService] Worker thread starting...");
Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid);
- // 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);
- Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
- }
- if (settings.changeApps && !isAppsCurrentlyLight)
- {
- SetAppsTheme(true);
- Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
- }
- }
- else
- {
- if (settings.changeSystem && isSystemCurrentlyLight)
- {
- SetSystemTheme(false);
- Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
- }
- if (settings.changeApps && isAppsCurrentlyLight)
- {
- SetAppsTheme(false);
- Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
- }
- }
- };
-
- // --- Initial settings load ---
LightSwitchSettings::instance().LoadSettings();
auto& settings = LightSwitchSettings::instance().settings();
- // --- Initial theme application (if schedule enabled) ---
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ int nowMinutes = st.wHour * 60 + st.wMinute;
+
+ // Handle initial theme application if necessary
if (settings.scheduleMode != ScheduleMode::Off)
{
- SYSTEMTIME st;
- GetLocalTime(&st);
- int nowMinutes = st.wHour * 60 + st.wMinute;
- applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings);
+ Logger::info(L"[LightSwitchService] Schedule mode is set to {}. Applying theme if necessary.", settings.scheduleMode);
+ LightSwitchSettings::instance().ApplyThemeIfNecessary();
}
else
{
- Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change.");
+ Logger::info(L"[LightSwitchService] Schedule mode is set to Off.");
}
- // --- Main loop ---
+ g_lastUpdatedDay = st.wDay;
+ Logger::info(L"[LightSwitchService] Initializing g_lastUpdatedDay to {}.", g_lastUpdatedDay);
+ ULONGLONG lastSettingsReload = 0;
+
+ // ticker loop
for (;;)
{
HANDLE waits[2] = { g_ServiceStopEvent, hParent };
DWORD count = hParent ? 2 : 1;
+ bool skipRest = false;
- LightSwitchSettings::instance().LoadSettings();
const auto& settings = LightSwitchSettings::instance().settings();
- // Check for changes in schedule mode or coordinates
- bool modeChangedToSunset = (prevMode != settings.scheduleMode &&
- settings.scheduleMode == ScheduleMode::SunsetToSunrise);
- bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude);
-
- if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise)
- {
- Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times.");
- update_sun_times(settings);
- SYSTEMTIME st;
- GetLocalTime(&st);
- g_lastUpdatedDay = st.wDay;
- prevMode = settings.scheduleMode;
- prevLat = settings.latitude;
- prevLon = settings.longitude;
- }
-
- // If schedule is off, idle but keep watching settings and manual override
+ // If the mode is set to Off, suspend the scheduler and avoid extra work
if (settings.scheduleMode == ScheduleMode::Off)
{
- Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive.");
+ Logger::info(L"[LightSwitchService] Schedule mode is OFF - suspending scheduler but keeping service alive.");
if (!hManualOverride)
- {
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
- }
- HANDLE waits[4];
- DWORD count = 0;
- waits[count++] = g_ServiceStopEvent;
+ HANDLE waitsOff[4];
+ DWORD countOff = 0;
+ waitsOff[countOff++] = g_ServiceStopEvent;
if (hParent)
- waits[count++] = hParent;
+ waitsOff[countOff++] = hParent;
if (hManualOverride)
- waits[count++] = hManualOverride;
- waits[count++] = LightSwitchSettings::instance().GetSettingsChangedEvent();
+ waitsOff[countOff++] = hManualOverride;
+ waitsOff[countOff++] = LightSwitchSettings::instance().GetSettingsChangedEvent();
for (;;)
{
- DWORD wait = WaitForMultipleObjects(count, waits, FALSE, INFINITE);
+ DWORD wait = WaitForMultipleObjects(countOff, waitsOff, FALSE, INFINITE);
- // --- Handle exit signals ---
- if (wait == WAIT_OBJECT_0) // stop event
+ if (wait == WAIT_OBJECT_0)
{
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
- break;
+ goto cleanup;
}
if (hParent && wait == WAIT_OBJECT_0 + 1)
{
Logger::info(L"[LightSwitchService] Parent exited - stopping service.");
- break;
+ goto cleanup;
}
- // --- Manual override triggered ---
if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
{
Logger::info(L"[LightSwitchService] Manual override received while schedule OFF.");
@@ -298,15 +236,12 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
continue;
}
- // --- Settings file changed ---
if (wait == WAIT_OBJECT_0 + (hParent ? 3 : 2))
{
Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings...");
-
ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent());
-
- LightSwitchSettings::instance().LoadSettings();
const auto& newSettings = LightSwitchSettings::instance().settings();
+ lastSettingsReload = GetTickCount64();
if (newSettings.scheduleMode != ScheduleMode::Off)
{
@@ -315,68 +250,200 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
}
}
}
+
+ continue;
}
+ bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off);
+ prevMode = settings.scheduleMode;
- // --- When schedule is active, run once per minute ---
- SYSTEMTIME st;
- GetLocalTime(&st);
- int nowMinutes = st.wHour * 60 + st.wMinute;
+ ULONGLONG nowTick = GetTickCount64();
+ bool recentSettingsReload = (nowTick - lastSettingsReload < 2000);
- // Refresh suntimes at day boundary
- if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise))
+ Logger::debug(L"[LightSwitchService] Current g_lastUpdatedDay value = {}.", g_lastUpdatedDay);
+
+ // Manual Override Detection Logic
+ bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
+
+ if (manualOverrideActive != lastOverrideStatus)
{
- update_sun_times(settings);
- g_lastUpdatedDay = st.wDay;
- Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
+ Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive);
+ lastOverrideStatus = manualOverrideActive;
}
- // Have to do this again in case settings got updated in the refresh suntimes chunk
- LightSwitchSettings::instance().LoadSettings();
- const auto& currentSettings = LightSwitchSettings::instance().settings();
-
- wchar_t msg[160];
- swprintf_s(msg,
- L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d",
- st.wHour,
- st.wMinute,
- currentSettings.lightTime / 60,
- currentSettings.lightTime % 60,
- currentSettings.darkTime / 60,
- currentSettings.darkTime % 60,
- static_cast(currentSettings.scheduleMode));
- Logger::info(msg);
-
- // --- Manual override check ---
- bool manualOverrideActive = false;
- if (hManualOverride)
+ if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled && !manualOverrideActive)
{
- manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
- }
+ bool currentSystemTheme = GetCurrentSystemTheme();
+ bool currentAppsTheme = GetCurrentAppsTheme();
- if (manualOverrideActive)
- {
- if (nowMinutes == (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440 ||
- nowMinutes == (currentSettings.darkTime + currentSettings.sunset_offset) % 1440)
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ int nowMinutes = st.wHour * 60 + st.wMinute;
+
+ int lightBoundary = 0;
+ int darkBoundary = 0;
+
+ if (settings.scheduleMode == ScheduleMode::SunsetToSunrise)
{
- ResetEvent(hManualOverride);
- Logger::info(L"[LightSwitchService] Manual override cleared at boundary");
+ lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440;
+ darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440;
}
else
{
- Logger::info(L"[LightSwitchService] Skipping schedule due to manual override");
- goto sleep_until_next_minute;
+ lightBoundary = settings.lightTime;
+ darkBoundary = settings.darkTime;
+ }
+
+ bool shouldBeLight = (lightBoundary < darkBoundary) ? (nowMinutes >= lightBoundary && nowMinutes < darkBoundary) : (nowMinutes >= lightBoundary || nowMinutes < darkBoundary);
+
+ Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight);
+
+ bool systemMismatch = settings.changeSystem && (currentSystemTheme != shouldBeLight);
+ bool appsMismatch = settings.changeApps && (currentAppsTheme != shouldBeLight);
+
+ if (systemMismatch || appsMismatch)
+ {
+ // Make sure this is not because we crossed a boundary
+ bool crossedBoundary = false;
+ if (prevMinutes != -1)
+ {
+ if (nowMinutes < prevMinutes)
+ {
+ // wrapped around midnight
+ crossedBoundary = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary) ||
+ (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
+ }
+ else
+ {
+ crossedBoundary = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary) ||
+ (prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
+ }
+ }
+
+ if (crossedBoundary)
+ {
+ Logger::info(L"[LightSwitchService] Missed boundary detected. Applying theme instead of triggering manual override.");
+ LightSwitchSettings::instance().ApplyThemeIfNecessary();
+ }
+ else
+ {
+ Logger::info(L"[LightSwitchService] External {} theme change detected, enabling manual override.",
+ systemMismatch && appsMismatch ? L"system/app" :
+ systemMismatch ? L"system" :
+ L"app");
+ SetEvent(hManualOverride);
+ skipRest = true;
+ }
+ }
+ }
+ else
+ {
+ Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled).");
+ }
+
+ if (hManualOverride)
+ manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
+
+ if (manualOverrideActive)
+ {
+ int lightBoundary = (settings.lightTime + settings.sunrise_offset) % 1440;
+ int darkBoundary = (settings.darkTime + settings.sunset_offset) % 1440;
+
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ nowMinutes = st.wHour * 60 + st.wMinute;
+
+ bool crossedLight = false;
+ bool crossedDark = false;
+
+ if (prevMinutes != -1)
+ {
+ // this means we are in a new day cycle
+ if (nowMinutes < prevMinutes)
+ {
+ crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary);
+ crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
+ }
+ else
+ {
+ crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary);
+ crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
+ }
+ }
+
+ if (crossedLight || crossedDark)
+ {
+ ResetEvent(hManualOverride);
+ Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary.");
+ }
+ else
+ {
+ Logger::debug(L"[LightSwitchService] Skipping schedule due to manual override");
+ skipRest = true;
}
}
- applyTheme(nowMinutes, currentSettings.lightTime + currentSettings.sunrise_offset, currentSettings.darkTime + currentSettings.sunset_offset, currentSettings);
+ // Apply theme if nothing has made us skip
+ if (!skipRest)
+ {
+ // Next two conditionals check for any updates necessary to the sun times.
+ bool modeChangedToSunset = (prevMode != settings.scheduleMode &&
+ settings.scheduleMode == ScheduleMode::SunsetToSunrise);
+ bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude);
- sleep_until_next_minute:
+ if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise)
+ {
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+
+ Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times.");
+ update_sun_times(settings);
+ g_lastUpdatedDay = st.wDay;
+ prevMode = settings.scheduleMode;
+ prevLat = settings.latitude;
+ prevLon = settings.longitude;
+ }
+
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ int nowMinutes = st.wHour * 60 + st.wMinute;
+
+ if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise))
+ {
+ update_sun_times(settings);
+ g_lastUpdatedDay = st.wDay;
+ prevMinutes = -1;
+ Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary.");
+ }
+
+ // settings after any necessary updates.
+ LightSwitchSettings::instance().LoadSettings();
+ const auto& currentSettings = LightSwitchSettings::instance().settings();
+
+ wchar_t msg[160];
+ swprintf_s(msg,
+ L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%s",
+ st.wHour,
+ st.wMinute,
+ currentSettings.lightTime / 60,
+ currentSettings.lightTime % 60,
+ currentSettings.darkTime / 60,
+ currentSettings.darkTime % 60,
+ ToString(currentSettings.scheduleMode).c_str());
+ Logger::info(msg);
+
+ LightSwitchSettings::instance().ApplyThemeIfNecessary();
+ }
+
+ // ─── Wait For Next Minute Tick Or Stop Event ────────────────────────────────
+ SYSTEMTIME st;
GetLocalTime(&st);
int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds;
if (msToNextMinute < 50)
msToNextMinute = 50;
+ prevMinutes = nowMinutes;
+
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
if (wait == WAIT_OBJECT_0)
{
@@ -390,6 +457,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
}
}
+cleanup:
if (hManualOverride)
CloseHandle(hManualOverride);
if (hParent)
@@ -398,7 +466,6 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
return 0;
}
-
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled)
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
index a7f44cca6d..8105b0ab3a 100644
--- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp
@@ -2,7 +2,7 @@
#include
#include
#include "SettingsObserver.h"
-
+#include "ThemeHelper.h"
#include
#include
#include
@@ -38,12 +38,80 @@ void LightSwitchSettings::InitFileWatcher()
m_settingsFileWatcher = std::make_unique(
GetSettingsFileName(),
[this]() {
- Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event.");
- SetEvent(m_settingsChangedEvent);
+ using namespace std::chrono;
+
+ {
+ std::lock_guard lock(m_debounceMutex);
+ m_lastChangeTime = steady_clock::now();
+ if (m_debouncePending)
+ return;
+ m_debouncePending = true;
+ }
+
+ m_debounceThread = std::jthread([this](std::stop_token stop) {
+ using namespace std::chrono;
+ while (!stop.stop_requested())
+ {
+ std::this_thread::sleep_for(seconds(3));
+
+ auto elapsed = steady_clock::now() - m_lastChangeTime;
+ if (elapsed >= seconds(1))
+ break;
+ }
+
+ {
+ std::lock_guard lock(m_debounceMutex);
+ m_debouncePending = false;
+ }
+
+ Logger::info(L"[LightSwitchSettings] Settings file stabilized, reloading.");
+
+ try
+ {
+ LoadSettings();
+ ApplyThemeIfNecessary();
+ SetEvent(m_settingsChangedEvent);
+ }
+ catch (const std::exception& e)
+ {
+ std::wstring wmsg;
+ wmsg.assign(e.what(), e.what() + strlen(e.what()));
+ Logger::error(L"[LightSwitchSettings] Exception during debounced reload: {}", wmsg);
+ }
+ });
});
}
}
+LightSwitchSettings::~LightSwitchSettings()
+{
+ Logger::info(L"[LightSwitchSettings] Cleaning up settings resources...");
+
+ // Stop and join the debounce thread (std::jthread auto-joins, but we can signal stop too)
+ if (m_debounceThread.joinable())
+ {
+ m_debounceThread.request_stop();
+ }
+
+ // Release the file watcher so it closes file handles and background threads
+ if (m_settingsFileWatcher)
+ {
+ m_settingsFileWatcher.reset();
+ Logger::info(L"[LightSwitchSettings] File watcher stopped.");
+ }
+
+ // Close the Windows event handle
+ if (m_settingsChangedEvent)
+ {
+ CloseHandle(m_settingsChangedEvent);
+ m_settingsChangedEvent = nullptr;
+ Logger::info(L"[LightSwitchSettings] Settings changed event closed.");
+ }
+
+ Logger::info(L"[LightSwitchSettings] Cleanup complete.");
+}
+
+
void LightSwitchSettings::AddObserver(SettingsObserver& observer)
{
m_observers.insert(&observer);
@@ -65,8 +133,14 @@ void LightSwitchSettings::NotifyObservers(SettingId id) const
}
}
+HANDLE LightSwitchSettings::GetSettingsChangedEvent() const
+{
+ return m_settingsChangedEvent;
+}
+
void LightSwitchSettings::LoadSettings()
{
+ std::lock_guard guard(m_settingsMutex);
try
{
PowerToysSettings::PowerToyValues values =
@@ -175,4 +249,49 @@ void LightSwitchSettings::LoadSettings()
{
// Keeps defaults if load fails
}
+}
+
+void LightSwitchSettings::ApplyThemeIfNecessary()
+{
+ std::lock_guard guard(m_settingsMutex);
+
+ SYSTEMTIME st;
+ GetLocalTime(&st);
+ int nowMinutes = st.wHour * 60 + st.wMinute;
+
+ bool shouldBeLight = false;
+ if (m_settings.lightTime < m_settings.darkTime)
+ shouldBeLight = (nowMinutes >= m_settings.lightTime && nowMinutes < m_settings.darkTime);
+ else
+ shouldBeLight = (nowMinutes >= m_settings.lightTime || nowMinutes < m_settings.darkTime);
+
+ bool isSystemCurrentlyLight = GetCurrentSystemTheme();
+ bool isAppsCurrentlyLight = GetCurrentAppsTheme();
+
+ if (shouldBeLight)
+ {
+ if (m_settings.changeSystem && !isSystemCurrentlyLight)
+ {
+ SetSystemTheme(true);
+ Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
+ }
+ if (m_settings.changeApps && !isAppsCurrentlyLight)
+ {
+ SetAppsTheme(true);
+ Logger::info(L"[LightSwitchService] Changing apps theme to light mode.");
+ }
+ }
+ else
+ {
+ if (m_settings.changeSystem && isSystemCurrentlyLight)
+ {
+ SetSystemTheme(false);
+ Logger::info(L"[LightSwitchService] Changing system theme to dark mode.");
+ }
+ if (m_settings.changeApps && isAppsCurrentlyLight)
+ {
+ SetAppsTheme(false);
+ Logger::info(L"[LightSwitchService] Changing apps theme to dark mode.");
+ }
+ }
}
\ No newline at end of file
diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h
index 32d011313f..25f60cec82 100644
--- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h
+++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h
@@ -5,7 +5,10 @@
#include
#include
#include
-
+#include
+#include
+#include
+#include
#include
#include
#include
@@ -78,12 +81,13 @@ public:
void RemoveObserver(SettingsObserver& observer);
void LoadSettings();
+ void ApplyThemeIfNecessary();
- HANDLE GetSettingsChangedEvent() const { return m_settingsChangedEvent; }
+ HANDLE GetSettingsChangedEvent() const;
private:
LightSwitchSettings();
- ~LightSwitchSettings() = default;
+ ~LightSwitchSettings();
LightSwitchConfig m_settings;
std::unique_ptr m_settingsFileWatcher;
@@ -92,4 +96,11 @@ private:
void NotifyObservers(SettingId id) const;
HANDLE m_settingsChangedEvent = nullptr;
+ mutable std::mutex m_settingsMutex;
+
+ // Debounce state
+ std::atomic_bool m_debouncePending{ false };
+ std::mutex m_debounceMutex;
+ std::chrono::steady_clock::time_point m_lastChangeTime{};
+ std::jthread m_debounceThread;
};
diff --git a/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h b/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h
index 88d0194eef..b0ddde72ec 100644
--- a/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h
+++ b/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h
@@ -2,6 +2,7 @@
#include
#include "SettingsConstants.h"
+#include "LightSwitchSettings.h"
class LightSwitchSettings;
@@ -22,7 +23,7 @@ public:
// Override this in your class to respond to updates
virtual void SettingsUpdate(SettingId type) {}
- bool WantsToBeNotified(SettingId type) const noexcept
+ virtual bool WantsToBeNotified(SettingId type) const noexcept
{
return m_observedSettings.contains(type);
}
diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrap.rc b/src/modules/MouseUtils/CursorWrap/CursorWrap.rc
new file mode 100644
index 0000000000..37752edae0
--- /dev/null
+++ b/src/modules/MouseUtils/CursorWrap/CursorWrap.rc
@@ -0,0 +1,46 @@
+#include
+#include "resource.h"
+#include "../../../../common/version/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+#include "winres.h"
+#undef APSTUDIO_READONLY_SYMBOLS
+
+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"
+ BEGIN
+ VALUE "CompanyName", COMPANY_NAME
+ VALUE "FileDescription", "PowerToys CursorWrap"
+ VALUE "FileVersion", FILE_VERSION_STRING
+ VALUE "InternalName", "CursorWrap"
+ VALUE "LegalCopyright", COPYRIGHT_NOTE
+ VALUE "OriginalFilename", "PowerToys.CursorWrap.dll"
+ VALUE "ProductName", PRODUCT_NAME
+ VALUE "ProductVersion", PRODUCT_VERSION_STRING
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200
+ END
+END
+
+STRINGTABLE
+BEGIN
+ IDS_CURSORWRAP_NAME L"CursorWrap"
+ IDS_CURSORWRAP_DISABLE_WRAP_DURING_DRAG L"Disable wrapping during drag"
+END
\ No newline at end of file
diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj b/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj
new file mode 100644
index 0000000000..59e2095ca7
--- /dev/null
+++ b/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj
@@ -0,0 +1,130 @@
+
+
+
+
+ 15.0
+ {48a1db8c-5df8-4fb3-9e14-2b67f3f2d8b5}
+ Win32Proj
+ CursorWrap
+ CursorWrap
+
+
+
+
+ DynamicLibrary
+ true
+ v143
+ Unicode
+
+
+ DynamicLibrary
+ false
+ v143
+ true
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\..\..\$(Platform)\$(Configuration)\
+ PowerToys.CursorWrap
+
+
+ true
+
+
+ false
+
+
+
+ Level3
+ Disabled
+ true
+ _DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ MultiThreadedDebug
+ stdcpplatest
+
+
+ Windows
+ true
+ $(OutDir)$(TargetName)$(TargetExt)
+
+
+
+
+ Level3
+ MaxSpeed
+ true
+ true
+ true
+ NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+ true
+ MultiThreaded
+ stdcpplatest
+
+
+ Windows
+ true
+ true
+ true
+ $(OutDir)$(TargetName)$(TargetExt)
+
+
+
+
+ ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+
+
+ {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}
+
+
+ {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}.
+
+
+
+
+
diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrapTests.h b/src/modules/MouseUtils/CursorWrap/CursorWrapTests.h
new file mode 100644
index 0000000000..4274ad714f
--- /dev/null
+++ b/src/modules/MouseUtils/CursorWrap/CursorWrapTests.h
@@ -0,0 +1,213 @@
+#pragma once
+
+#include
+#include
+
+// Test case structure for comprehensive monitor layout testing
+struct MonitorTestCase
+{
+ std::string name;
+ std::string description;
+ int grid[3][3]; // 3x3 grid representing monitor layout (0 = no monitor, 1-9 = monitor ID)
+
+ // Test scenarios to validate
+ struct TestScenario
+ {
+ int sourceMonitor; // Which monitor to start cursor on (1-based)
+ int edgeDirection; // 0=top, 1=right, 2=bottom, 3=left
+ int expectedTargetMonitor; // Expected destination monitor (1-based, -1 = wrap within same monitor)
+ std::string description;
+ };
+
+ std::vector scenarios;
+};
+
+// Comprehensive test cases for all possible 3x3 monitor grid configurations
+class CursorWrapTestSuite
+{
+public:
+ static std::vector GetAllTestCases()
+ {
+ std::vector testCases;
+
+ // Test Case 1: Single monitor (center)
+ testCases.push_back({
+ "Single_Center",
+ "Single monitor in center position",
+ {
+ {0, 0, 0},
+ {0, 1, 0},
+ {0, 0, 0}
+ },
+ {
+ {1, 0, -1, "Top edge wraps to bottom of same monitor"},
+ {1, 1, -1, "Right edge wraps to left of same monitor"},
+ {1, 2, -1, "Bottom edge wraps to top of same monitor"},
+ {1, 3, -1, "Left edge wraps to right of same monitor"}
+ }
+ });
+
+ // Test Case 2: Two monitors horizontal (left + right)
+ testCases.push_back({
+ "Dual_Horizontal_Left_Right",
+ "Two monitors: left + right",
+ {
+ {0, 0, 0},
+ {1, 0, 2},
+ {0, 0, 0}
+ },
+ {
+ {1, 0, -1, "Monitor 1 top wraps to bottom of monitor 1"},
+ {1, 1, 2, "Monitor 1 right edge moves to monitor 2 left"},
+ {1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"},
+ {1, 3, -1, "Monitor 1 left edge wraps to right of monitor 1"},
+ {2, 0, -1, "Monitor 2 top wraps to bottom of monitor 2"},
+ {2, 1, -1, "Monitor 2 right edge wraps to left of monitor 2"},
+ {2, 2, -1, "Monitor 2 bottom wraps to top of monitor 2"},
+ {2, 3, 1, "Monitor 2 left edge moves to monitor 1 right"}
+ }
+ });
+
+ // Test Case 3: Two monitors vertical (Monitor 2 above Monitor 1) - CORRECTED FOR USER'S SETUP
+ testCases.push_back({
+ "Dual_Vertical_2_Above_1",
+ "Two monitors: Monitor 2 (top) above Monitor 1 (bottom/main)",
+ {
+ {0, 2, 0}, // Row 0: Monitor 2 (physically top monitor)
+ {0, 0, 0}, // Row 1: Empty
+ {0, 1, 0} // Row 2: Monitor 1 (physically bottom/main monitor)
+ },
+ {
+ // Monitor 1 (bottom/main monitor) tests
+ {1, 0, 2, "Monitor 1 (bottom) top edge should move to Monitor 2 (top) bottom"},
+ {1, 1, -1, "Monitor 1 right wraps to left of monitor 1"},
+ {1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"},
+ {1, 3, -1, "Monitor 1 left wraps to right of monitor 1"},
+
+ // Monitor 2 (top monitor) tests
+ {2, 0, -1, "Monitor 2 (top) top wraps to bottom of monitor 2"},
+ {2, 1, -1, "Monitor 2 right wraps to left of monitor 2"},
+ {2, 2, 1, "Monitor 2 (top) bottom edge should move to Monitor 1 (bottom) top"},
+ {2, 3, -1, "Monitor 2 left wraps to right of monitor 2"}
+ }
+ });
+
+ // Test Case 4: Three monitors L-shape (center + left + top)
+ testCases.push_back({
+ "Triple_L_Shape",
+ "Three monitors in L-shape: center + left + top",
+ {
+ {0, 3, 0},
+ {2, 1, 0},
+ {0, 0, 0}
+ },
+ {
+ {1, 0, 3, "Monitor 1 top moves to monitor 3 bottom"},
+ {1, 1, -1, "Monitor 1 right wraps to left of monitor 1"},
+ {1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"},
+ {1, 3, 2, "Monitor 1 left moves to monitor 2 right"},
+ {2, 0, -1, "Monitor 2 top wraps to bottom of monitor 2"},
+ {2, 1, 1, "Monitor 2 right moves to monitor 1 left"},
+ {2, 2, -1, "Monitor 2 bottom wraps to top of monitor 2"},
+ {2, 3, -1, "Monitor 2 left wraps to right of monitor 2"},
+ {3, 0, -1, "Monitor 3 top wraps to bottom of monitor 3"},
+ {3, 1, -1, "Monitor 3 right wraps to left of monitor 3"},
+ {3, 2, 1, "Monitor 3 bottom moves to monitor 1 top"},
+ {3, 3, -1, "Monitor 3 left wraps to right of monitor 3"}
+ }
+ });
+
+ // Test Case 5: Three monitors horizontal (left + center + right)
+ testCases.push_back({
+ "Triple_Horizontal",
+ "Three monitors horizontal: left + center + right",
+ {
+ {0, 0, 0},
+ {1, 2, 3},
+ {0, 0, 0}
+ },
+ {
+ {1, 0, -1, "Monitor 1 top wraps to bottom"},
+ {1, 1, 2, "Monitor 1 right moves to monitor 2"},
+ {1, 2, -1, "Monitor 1 bottom wraps to top"},
+ {1, 3, -1, "Monitor 1 left wraps to right"},
+ {2, 0, -1, "Monitor 2 top wraps to bottom"},
+ {2, 1, 3, "Monitor 2 right moves to monitor 3"},
+ {2, 2, -1, "Monitor 2 bottom wraps to top"},
+ {2, 3, 1, "Monitor 2 left moves to monitor 1"},
+ {3, 0, -1, "Monitor 3 top wraps to bottom"},
+ {3, 1, -1, "Monitor 3 right wraps to left"},
+ {3, 2, -1, "Monitor 3 bottom wraps to top"},
+ {3, 3, 2, "Monitor 3 left moves to monitor 2"}
+ }
+ });
+
+ // Test Case 6: Three monitors vertical (top + center + bottom)
+ testCases.push_back({
+ "Triple_Vertical",
+ "Three monitors vertical: top + center + bottom",
+ {
+ {0, 1, 0},
+ {0, 2, 0},
+ {0, 3, 0}
+ },
+ {
+ {1, 0, -1, "Monitor 1 top wraps to bottom"},
+ {1, 1, -1, "Monitor 1 right wraps to left"},
+ {1, 2, 2, "Monitor 1 bottom moves to monitor 2"},
+ {1, 3, -1, "Monitor 1 left wraps to right"},
+ {2, 0, 1, "Monitor 2 top moves to monitor 1"},
+ {2, 1, -1, "Monitor 2 right wraps to left"},
+ {2, 2, 3, "Monitor 2 bottom moves to monitor 3"},
+ {2, 3, -1, "Monitor 2 left wraps to right"},
+ {3, 0, 2, "Monitor 3 top moves to monitor 2"},
+ {3, 1, -1, "Monitor 3 right wraps to left"},
+ {3, 2, -1, "Monitor 3 bottom wraps to top"},
+ {3, 3, -1, "Monitor 3 left wraps to right"}
+ }
+ });
+
+ return testCases;
+ }
+
+ // Helper function to print test case in a readable format
+ static std::string FormatTestCase(const MonitorTestCase& testCase)
+ {
+ std::string result = "Test Case: " + testCase.name + "\n";
+ result += "Description: " + testCase.description + "\n";
+ result += "Layout:\n";
+
+ for (int row = 0; row < 3; row++)
+ {
+ result += " ";
+ for (int col = 0; col < 3; col++)
+ {
+ if (testCase.grid[row][col] == 0)
+ {
+ result += ". ";
+ }
+ else
+ {
+ result += std::to_string(testCase.grid[row][col]) + " ";
+ }
+ }
+ result += "\n";
+ }
+
+ result += "Test Scenarios:\n";
+ for (const auto& scenario : testCase.scenarios)
+ {
+ result += " - " + scenario.description + "\n";
+ }
+
+ return result;
+ }
+
+ // Helper function to validate a specific test case against actual behavior
+ static bool ValidateTestCase(const MonitorTestCase& testCase)
+ {
+ // This would be called with actual CursorWrap instance to validate behavior
+ // For now, just return true - this would need actual implementation
+ return true;
+ }
+};
\ No newline at end of file
diff --git a/src/modules/MouseUtils/CursorWrap/dllmain.cpp b/src/modules/MouseUtils/CursorWrap/dllmain.cpp
new file mode 100644
index 0000000000..ece1948d01
--- /dev/null
+++ b/src/modules/MouseUtils/CursorWrap/dllmain.cpp
@@ -0,0 +1,1045 @@
+#include "pch.h"
+#include "../../../interface/powertoy_module_interface.h"
+#include "../../../common/SettingsAPI/settings_objects.h"
+#include "trace.h"
+#include "../../../common/utils/process_path.h"
+#include "../../../common/utils/resources.h"
+#include "../../../common/logger/logger.h"
+#include "../../../common/utils/logger_helper.h"
+#include
+#include
+#include
+#include
@@ -82,7 +81,6 @@
true
NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- MultiThreaded
stdcpplatest
diff --git a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj
index df0df021da..ecd6ea3ec4 100644
--- a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj
+++ b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj
@@ -48,7 +48,6 @@
true
_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- MultiThreadedDebug
stdcpplatest
@@ -66,7 +65,6 @@
true
NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- MultiThreaded
stdcpplatest
diff --git a/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj
index 29e8f444bf..89abed873a 100644
--- a/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj
+++ b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj
@@ -48,7 +48,6 @@
true
_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- MultiThreadedDebug
stdcpplatest
@@ -66,7 +65,6 @@
true
NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- MultiThreaded
stdcpplatest
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj b/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj
index 58668c663f..7fef06e960 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj
@@ -49,7 +49,6 @@
true
_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- MultiThreadedDebug
stdcpplatest
@@ -67,7 +66,6 @@
true
NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
true
- MultiThreaded
stdcpplatest
diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
index fd144e807b..b460e29643 100644
--- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
+++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp
@@ -14,6 +14,9 @@ extern void InclusiveCrosshairsRequestUpdatePosition();
extern void InclusiveCrosshairsEnsureOn();
extern void InclusiveCrosshairsEnsureOff();
extern void InclusiveCrosshairsSetExternalControl(bool enabled);
+extern void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation);
+extern bool InclusiveCrosshairsIsEnabled();
+extern void InclusiveCrosshairsSwitch();
// Non-Localizable strings
namespace
@@ -244,12 +247,19 @@ public:
return false;
}
- if (hotkeyId == 0)
+ if (hotkeyId == 0) // Crosshairs activation
{
+ // If gliding cursor is active, cancel it and activate crosshairs
+ if (m_glideState.load() != 0)
+ {
+ CancelGliding(true /*activateCrosshairs*/);
+ return true;
+ }
+ // Otherwise, normal crosshairs toggle
InclusiveCrosshairsSwitch();
return true;
}
- if (hotkeyId == 1)
+ if (hotkeyId == 1) // Gliding cursor activation
{
HandleGlidingHotkey();
return true;
@@ -268,25 +278,44 @@ private:
SendInput(2, inputs, sizeof(INPUT));
}
- // Cancel gliding without performing the final click (Escape handling)
- void CancelGliding()
+ // Cancel gliding with option to activate crosshairs in user's preferred orientation
+ void CancelGliding(bool activateCrosshairs)
{
int state = m_glideState.load();
if (state == 0)
{
return; // nothing to cancel
}
+
+ // Stop all gliding operations
StopXTimer();
StopYTimer();
m_glideState = 0;
- InclusiveCrosshairsEnsureOff();
+ UninstallKeyboardHook();
+
+ // Reset crosshairs control and restore user settings
InclusiveCrosshairsSetExternalControl(false);
+ InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
+
+ if (activateCrosshairs)
+ {
+ // User is switching to crosshairs mode - enable with their settings
+ InclusiveCrosshairsEnsureOn();
+ }
+ else
+ {
+ // User canceled (Escape) - turn off crosshairs completely
+ InclusiveCrosshairsEnsureOff();
+ }
+
+ // Reset gliding state
if (auto s = m_state)
{
s->xFraction = 0.0;
s->yFraction = 0.0;
}
- Logger::debug("Gliding cursor cancelled via Escape key");
+
+ Logger::debug("Gliding cursor cancelled (activateCrosshairs={})", activateCrosshairs ? 1 : 0);
}
// Stateless helpers operating on shared State
@@ -425,21 +454,22 @@ private:
{
return;
}
- // Simulate the AHK state machine
+
int state = m_glideState.load();
switch (state)
{
- case 0:
+ case 0: // Starting gliding
{
- // For detect for cancel key
+ // Install keyboard hook for Escape cancellation
InstallKeyboardHook();
- // Ensure crosshairs on (do not toggle off if already on)
- InclusiveCrosshairsEnsureOn();
- // Disable internal mouse hook so we control position updates explicitly
+
+ // Force crosshairs visible in BOTH orientation for gliding, regardless of user setting
+ // Set external control before enabling to prevent internal movement hook from attaching
InclusiveCrosshairsSetExternalControl(true);
- // Override crosshairs to show both for Gliding Cursor
InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both);
+ InclusiveCrosshairsEnsureOn(); // Always ensure they are visible
+ // Initialize gliding state
s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed;
s->xFraction = 0.0;
@@ -447,20 +477,17 @@ private:
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
SetCursorPos(0, y);
InclusiveCrosshairsRequestUpdatePosition();
+
m_glideState = 1;
StartXTimer();
break;
}
- case 1:
- {
- // Slow horizontal
+ case 1: // Slow horizontal
s->currentXSpeed = s->slowHSpeed;
m_glideState = 2;
break;
- }
- case 2:
+ case 2: // Switch to vertical fast
{
- // Stop horizontal, start vertical (fast)
StopXTimer();
s->currentYSpeed = s->fastVSpeed;
s->currentYPos = 0;
@@ -471,33 +498,37 @@ private:
StartYTimer();
break;
}
- case 3:
- {
- // Slow vertical
+ case 3: // Slow vertical
s->currentYSpeed = s->slowVSpeed;
m_glideState = 4;
break;
- }
- case 4:
+ case 4: // Finalize (click and end)
default:
{
- UninstallKeyboardHook();
- // Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
+ // Complete the gliding sequence
StopYTimer();
m_glideState = 0;
LeftClick();
- InclusiveCrosshairsEnsureOff();
+
+ // Restore normal crosshairs operation and turn them off
InclusiveCrosshairsSetExternalControl(false);
- // Restore original crosshairs orientation setting
InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation);
- s->xFraction = 0.0;
- s->yFraction = 0.0;
+ InclusiveCrosshairsEnsureOff();
+
+ UninstallKeyboardHook();
+
+ // Reset state
+ if (auto sp = m_state)
+ {
+ sp->xFraction = 0.0;
+ sp->yFraction = 0.0;
+ }
break;
}
}
}
- // Low-level keyboard hook procedures
+ // Low-level keyboard hook for Escape cancellation
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION)
@@ -509,14 +540,11 @@ private:
{
if (inst->m_enabled && inst->m_glideState.load() != 0)
{
- inst->UninstallKeyboardHook();
- inst->CancelGliding();
+ inst->CancelGliding(false); // Escape cancels without activating crosshairs
}
}
}
}
-
- // Do not swallow Escape; pass it through
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
diff --git a/src/modules/MouseWithoutBorders/App/Class/Common.VK.cs b/src/modules/MouseWithoutBorders/App/Class/Common.VK.cs
index 79aa50c6dc..3f54a0281d 100644
--- a/src/modules/MouseWithoutBorders/App/Class/Common.VK.cs
+++ b/src/modules/MouseWithoutBorders/App/Class/Common.VK.cs
@@ -112,6 +112,7 @@ namespace MouseWithoutBorders
internal const int WM_RBUTTONDBLCLK = 0x206;
internal const int WM_MBUTTONDBLCLK = 0x209;
internal const int WM_MOUSEWHEEL = 0x020A;
+ internal const int WM_MOUSEHWHEEL = 0x020E;
internal const int WM_KEYDOWN = 0x100;
internal const int WM_KEYUP = 0x101;
diff --git a/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs b/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs
index 0bbd8014ae..e735db814c 100644
--- a/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs
+++ b/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs
@@ -204,6 +204,9 @@ namespace MouseWithoutBorders.Class
case Common.WM_MOUSEWHEEL:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.WHEEL;
break;
+ case Common.WM_MOUSEHWHEEL:
+ mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.HWHEEL;
+ break;
case Common.WM_XBUTTONUP:
mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XUP;
break;
diff --git a/src/modules/MouseWithoutBorders/App/Class/NativeMethods.cs b/src/modules/MouseWithoutBorders/App/Class/NativeMethods.cs
index 539e0267bd..831144f377 100644
--- a/src/modules/MouseWithoutBorders/App/Class/NativeMethods.cs
+++ b/src/modules/MouseWithoutBorders/App/Class/NativeMethods.cs
@@ -556,6 +556,7 @@ namespace MouseWithoutBorders.Class
XDOWN = 0x0080,
XUP = 0x0100,
WHEEL = 0x0800,
+ HWHEEL = 0x1000,
VIRTUALDESK = 0x4000,
ABSOLUTE = 0x8000,
}
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj
index 16c6b4efbc..74d87c5623 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj
@@ -92,6 +92,7 @@
+
@@ -99,7 +100,7 @@
-
+
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h
index 5018653070..711063d0cb 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h
@@ -3,6 +3,7 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
+#define NOMINMAX
#define NOMCX
#define NOHELP
#define NOCOMM
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp
new file mode 100644
index 0000000000..945a516ca4
--- /dev/null
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp
@@ -0,0 +1,118 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#include "pch.h"
+#include "Helpers.h"
+#include
+
+// Minimal subset of PowerRename Helpers used by NewPlus
+// This is a copy from PowerRename main branch to avoid cross-module dependencies
+
+HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime)
+{
+ std::locale::global(std::locale(""));
+ HRESULT hr = E_INVALIDARG;
+ if (source && wcslen(source) > 0)
+ {
+ std::wstring res(source);
+ wchar_t replaceTerm[MAX_PATH] = { 0 };
+ wchar_t formattedDate[MAX_PATH] = { 0 };
+
+ wchar_t localeName[LOCALE_NAME_MAX_LENGTH];
+ if (GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH) == 0)
+ {
+ StringCchCopy(localeName, LOCALE_NAME_MAX_LENGTH, L"en_US");
+ }
+
+ int hour12 = (fileTime.wHour % 12);
+ if (hour12 == 0)
+ {
+ hour12 = 12;
+ }
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%04d"), L"$01", fileTime.wYear);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100));
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10));
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm);
+
+ GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL);
+ formattedDate[0] = towupper(formattedDate[0]);
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMMM"), replaceTerm);
+
+ GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL);
+ formattedDate[0] = towupper(formattedDate[0]);
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm);
+
+ GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL);
+ formattedDate[0] = towupper(formattedDate[0]);
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDDD"), replaceTerm);
+
+ GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL);
+ formattedDate[0] = towupper(formattedDate[0]);
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM");
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"am" : L"pm");
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm);
+
+ StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100);
+ res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm);
+
+ hr = StringCchCopy(result, cchMax, res.c_str());
+ }
+
+ return hr;
+}
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h
new file mode 100644
index 0000000000..540478856e
--- /dev/null
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#pragma once
+
+// Minimal subset of PowerRename Helpers used by NewPlus
+// This is a copy from PowerRename's main branch to avoid cross-module dependencies
+HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime);
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
index 90058a503e..c650439b65 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
@@ -67,8 +67,6 @@
false
dll.def
runtimeobject.lib;$(CoreLibraryDependencies)
-
-
del $(OutDir)\NewPlusPackage.msix /q
@@ -100,8 +98,6 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv
false
dll.def
runtimeobject.lib;$(CoreLibraryDependencies)
-
-
del $(OutDir)\NewPlusPackage.msix /q
@@ -114,6 +110,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv
+
@@ -131,7 +128,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv
-
+
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h
index 63f23c3e86..1d511f2afe 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h
@@ -1,7 +1,7 @@
#pragma once
#include
-#include "..\..\powerrename\lib\Helpers.h"
+#include "Helpers.h"
#include "helpers_filesystem.h"
#pragma comment(lib, "Pathcch.lib")
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h
index 50f92562d2..02874ab8f3 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h
@@ -302,9 +302,9 @@ namespace newplus::utilities
POINT mouse_position;
GetCursorPos(&mouse_position);
mouse_position.x -= GetSystemMetrics(SM_CXMENUSIZE);
- mouse_position.x = max(mouse_position.x, 20);
+ mouse_position.x = (std::max)(mouse_position.x, 20L);
mouse_position.y -= GetSystemMetrics(SM_CXMENUSIZE)/2;
- mouse_position.y = max(mouse_position.y, 20);
+ mouse_position.y = (std::max)(mouse_position.y, 20L);
POINT position[] = { mouse_position };
folder_view->SelectAndPositionItems(1, shell_item_to_select_and_position, position, common_select_flags | SVSI_POSITIONITEM);
}
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h
index b766a837d5..13093e1d08 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h
@@ -3,6 +3,7 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
+#define NOMINMAX
#define NOMCX
#define NOHELP
#define NOCOMM
@@ -13,6 +14,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp
index a7ddfe835f..a178e00195 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp
@@ -60,8 +60,8 @@ std::wstring template_item::get_target_filename(const bool include_starting_digi
std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const
{
- filename.erase(0, min(filename.find_first_not_of(L"0123456789"), filename.size()));
- filename.erase(0, min(filename.find_first_not_of(L" ."), filename.size()));
+ filename.erase(0, std::min(filename.find_first_not_of(L"0123456789"), filename.size()));
+ filename.erase(0, std::min(filename.find_first_not_of(L" ."), filename.size()));
return filename;
}
diff --git a/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs b/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs
index 926729542a..18702eaaf6 100644
--- a/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs
+++ b/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs
@@ -2,8 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OpenQA.Selenium.Interactions;
using static Microsoft.PowerToys.UITest.UITestBase;
namespace PowerOCR.UITests;
@@ -19,41 +21,274 @@ public class PowerOCRTests : UITestBase
[TestInitialize]
public void TestInitialize()
{
- if (FindAll("Text Extractor").Count == 0)
+ if (FindAll(By.AccessibilityId("TextExtractorNavItem")).Count == 0)
{
- // Expand Advanced list-group if needed
- Find("System Tools").Click();
+ // Expand System Tools list-group if needed
+ Find(By.AccessibilityId("SystemToolsNavItem")).Click();
}
- Find("Text Extractor").Click();
+ Find(By.AccessibilityId("TextExtractorNavItem")).Click();
- Find("Enable Text Extractor").Toggle(true);
+ Find(By.AccessibilityId("EnableTextExtractorToggleSwitch")).Toggle(true);
- SendKeys(Key.Win, Key.D);
+ // Reset activation shortcut to default (Win+Shift+T)
+ var shortcutControl = Find(By.AccessibilityId("TextExtractorActivationShortcut"), 5000);
+ if (shortcutControl != null)
+ {
+ shortcutControl.Click();
+ Thread.Sleep(500);
+
+ // Set default shortcut Win+Shift+T
+ SendKeys(Key.Win, Key.Shift, Key.T);
+ Thread.Sleep(1000);
+
+ // Click Save to confirm
+ var saveButton = Find