Compare commits
132 Commits
v0.96.0
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbb6f3ca63 | ||
|
|
a878208053 | ||
|
|
46781b1574 | ||
|
|
8ff1c40a6b | ||
|
|
5320a8fbaa | ||
|
|
1a283f3348 | ||
|
|
c75c9bf7b3 | ||
|
|
455820c837 | ||
|
|
1e3b05ccb8 | ||
|
|
115bef7897 | ||
|
|
afc30b4fad | ||
|
|
d1405a0da5 | ||
|
|
6c2fbf37e5 | ||
|
|
b77305e7c9 | ||
|
|
b599cf40e8 | ||
|
|
de5f571cda | ||
|
|
aae521b229 | ||
|
|
c8c50ec25d | ||
|
|
df1c5e8eb3 | ||
|
|
ae541c03ff | ||
|
|
84b8fcc78b | ||
|
|
4820606ed2 | ||
|
|
fd904d2032 | ||
|
|
e7a2a699a9 | ||
|
|
693487dd27 | ||
|
|
eb6f230ca8 | ||
|
|
f3f9ef4c6f | ||
|
|
b47aa0a71c | ||
|
|
c17f417945 | ||
|
|
f4243376f9 | ||
|
|
c2e8b2f5ec | ||
|
|
c0effa4d4a | ||
|
|
93541f1c51 | ||
|
|
1099bd24ee | ||
|
|
70717a45cb | ||
|
|
031aaa1e59 | ||
|
|
a44b2b6355 | ||
|
|
1542a9ec62 | ||
|
|
774f0491f7 | ||
|
|
a461206dae | ||
|
|
16b889c019 | ||
|
|
211aedbda8 | ||
|
|
d4fe2d7cec | ||
|
|
7a0648eb5f | ||
|
|
29d1bc4198 | ||
|
|
d4e4bd7014 | ||
|
|
3b9d676705 | ||
|
|
76bbe3bdd6 | ||
|
|
5079de58f7 | ||
|
|
01a7f2c64e | ||
|
|
5d456c85c1 | ||
|
|
192cfe8a98 | ||
|
|
30d4961b73 | ||
|
|
b4133cfaa4 | ||
|
|
91ed56028a | ||
|
|
04a4874187 | ||
|
|
53a515a66f | ||
|
|
9b7aaccdb8 | ||
|
|
50d89a0a0f | ||
|
|
ab382855af | ||
|
|
6e667541a2 | ||
|
|
37a206f3d8 | ||
|
|
c8be3ed450 | ||
|
|
4b8ca7982e | ||
|
|
e126840d83 | ||
|
|
4d90e0d7a8 | ||
|
|
b5f593ac44 | ||
|
|
fcc0304f8a | ||
|
|
eb65888421 | ||
|
|
8dc65d2bb3 | ||
|
|
f85141c3ae | ||
|
|
c1bbfa6035 | ||
|
|
bcbacb6101 | ||
|
|
0b108efccf | ||
|
|
f798e3ff7a | ||
|
|
a4548dd32f | ||
|
|
0e0a831b06 | ||
|
|
5c95ceedc8 | ||
|
|
3541d1f6eb | ||
|
|
22f28c40a8 | ||
|
|
567c63e81b | ||
|
|
860bb8cba2 | ||
|
|
66f8d68a8a | ||
|
|
cbc05b4cfc | ||
|
|
cd06b73431 | ||
|
|
1c3b09d6a0 | ||
|
|
89864af06e | ||
|
|
58e4a6c83c | ||
|
|
8eb9ef2c14 | ||
|
|
d89198d26e | ||
|
|
86e013df7e | ||
|
|
00cd91344d | ||
|
|
72a6057859 | ||
|
|
be07e97cf6 | ||
|
|
02a60bd098 | ||
|
|
207f294e1e | ||
|
|
03f3206a44 | ||
|
|
5171b78eae | ||
|
|
f36e8747fd | ||
|
|
211cbc6438 | ||
|
|
0e30d2705c | ||
|
|
4ece86c4d1 | ||
|
|
55d8e726a0 | ||
|
|
b3c1170040 | ||
|
|
ef1717c97f | ||
|
|
bfff31e657 | ||
|
|
b761f95ba3 | ||
|
|
dda9cd8e18 | ||
|
|
14282b9df1 | ||
|
|
81a6e3bfdc | ||
|
|
06279a2102 | ||
|
|
6fb3ee5e3a | ||
|
|
4df18234a7 | ||
|
|
571cb3cb22 | ||
|
|
9c76d8a667 | ||
|
|
e8cbb1bd66 | ||
|
|
5734afcf89 | ||
|
|
c63b29a777 | ||
|
|
4309006a92 | ||
|
|
d24a1d99ad | ||
|
|
439023af68 | ||
|
|
4e5a2db985 | ||
|
|
8a7c944ec9 | ||
|
|
49cfcb1349 | ||
|
|
320b7eca7c | ||
|
|
d60923bc9a | ||
|
|
a5097c7525 | ||
|
|
7ccbef0298 | ||
|
|
665d7ca535 | ||
|
|
debbc72825 | ||
|
|
46a4e32fb6 | ||
|
|
c832862b9a |
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -7,13 +7,6 @@ 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:
|
||||
|
||||
7
.github/actions/spell-check/allow/code.txt
vendored
@@ -321,10 +321,3 @@ REGSTR
|
||||
|
||||
# Misc Win32 APIs and PInvokes
|
||||
INVOKEIDLIST
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
FFF
|
||||
HHH
|
||||
riday
|
||||
YYY
|
||||
|
||||
1
.github/actions/spell-check/excludes.txt
vendored
@@ -105,7 +105,6 @@
|
||||
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
|
||||
^src/common/sysinternals/Eula/
|
||||
^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$
|
||||
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
|
||||
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
|
||||
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
||||
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
|
||||
|
||||
41
.github/actions/spell-check/expect.txt
vendored
@@ -22,7 +22,6 @@ ADate
|
||||
ADDSTRING
|
||||
ADDUNDORECORD
|
||||
ADifferent
|
||||
adjacents
|
||||
ADMINS
|
||||
adml
|
||||
admx
|
||||
@@ -47,6 +46,7 @@ Allmodule
|
||||
ALLOWUNDO
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
amazonbedrock
|
||||
AModifier
|
||||
amr
|
||||
ANDSCANS
|
||||
@@ -65,7 +65,6 @@ APIIs
|
||||
Apm
|
||||
APPBARDATA
|
||||
APPEXECLINK
|
||||
appext
|
||||
APPLICATIONFRAMEHOST
|
||||
appmanifest
|
||||
APPMODEL
|
||||
@@ -101,6 +100,7 @@ ATX
|
||||
ATRIOX
|
||||
aumid
|
||||
authenticode
|
||||
Authenticode
|
||||
AUTOBUDDY
|
||||
AUTOCHECKBOX
|
||||
AUTOHIDE
|
||||
@@ -141,7 +141,7 @@ bla
|
||||
BLACKFRAME
|
||||
BLENDFUNCTION
|
||||
Blockquotes
|
||||
blt
|
||||
Blt
|
||||
BLURBEHIND
|
||||
BLURREGION
|
||||
bmi
|
||||
@@ -176,18 +176,14 @@ BYPOSITION
|
||||
CALCRECT
|
||||
CALG
|
||||
callbackptr
|
||||
cabstr
|
||||
calpwstr
|
||||
caub
|
||||
Cangjie
|
||||
CANRENAME
|
||||
Carlseibert
|
||||
Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
CAtl
|
||||
CBN
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
CCHFORMNAME
|
||||
@@ -258,7 +254,6 @@ cominterop
|
||||
commandnotfound
|
||||
commandpalette
|
||||
compmgmt
|
||||
COMPOSITIONDISABLED
|
||||
COMPOSITIONFULL
|
||||
CONFIGW
|
||||
CONFLICTINGMODIFIERKEY
|
||||
@@ -291,7 +286,6 @@ cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
createdump
|
||||
creativecommons
|
||||
CREATEPROCESS
|
||||
CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
@@ -315,7 +309,6 @@ CURRENTDIR
|
||||
CURSORINFO
|
||||
cursorpos
|
||||
CURSORSHOWING
|
||||
CURSORWRAP
|
||||
customaction
|
||||
CUSTOMACTIONTEST
|
||||
CUSTOMFORMATPLACEHOLDER
|
||||
@@ -355,7 +348,6 @@ Deact
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
dfx
|
||||
Deduplicator
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
@@ -415,9 +407,6 @@ DNLEN
|
||||
DONOTROUND
|
||||
DONTVALIDATEPATH
|
||||
dotnet
|
||||
downsampled
|
||||
downsampling
|
||||
Downsampled
|
||||
downscale
|
||||
DPICHANGED
|
||||
DPIs
|
||||
@@ -434,6 +423,7 @@ DSTINVERT
|
||||
DString
|
||||
DSVG
|
||||
dto
|
||||
DTo
|
||||
DUMMYUNIONNAME
|
||||
dutil
|
||||
DVASPECT
|
||||
@@ -467,6 +457,7 @@ EDITKEYBOARD
|
||||
EDITSHORTCUTS
|
||||
EDITTEXT
|
||||
EFile
|
||||
ekus
|
||||
eku
|
||||
emojis
|
||||
ENABLEDELAYEDEXPANSION
|
||||
@@ -531,7 +522,6 @@ EXTRINSICPROPERTIES
|
||||
eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FNumber
|
||||
FARPROC
|
||||
fdx
|
||||
fesf
|
||||
@@ -602,7 +592,6 @@ getfilesiginforedist
|
||||
geolocator
|
||||
GETHOTKEY
|
||||
GETICON
|
||||
GETLBTEXT
|
||||
GETMINMAXINFO
|
||||
GETNONCLIENTMETRICS
|
||||
GETPROPERTYSTOREFLAGS
|
||||
@@ -610,7 +599,6 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
GIFs
|
||||
gitmodules
|
||||
GHND
|
||||
GMEM
|
||||
@@ -621,7 +609,6 @@ GPOCA
|
||||
gpp
|
||||
gpu
|
||||
gradians
|
||||
grctlext
|
||||
Gridcustomlayout
|
||||
GSM
|
||||
gtm
|
||||
@@ -695,6 +682,7 @@ hmonitor
|
||||
homies
|
||||
homljgmgpmcbpjbnjpfijnhipfkiclkd
|
||||
HOOKPROC
|
||||
huggingface
|
||||
HORZRES
|
||||
HORZSIZE
|
||||
Hostbackdropbrush
|
||||
@@ -721,7 +709,6 @@ HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
HTTRANSPARENT
|
||||
hutchinsoniana
|
||||
HVal
|
||||
HValue
|
||||
Hvci
|
||||
@@ -743,9 +730,7 @@ IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
idl
|
||||
IIM
|
||||
idlist
|
||||
ifd
|
||||
IDOK
|
||||
IDOn
|
||||
IDR
|
||||
@@ -762,7 +747,6 @@ Ijwhost
|
||||
ILD
|
||||
IMAGEHLP
|
||||
IMAGERESIZERCONTEXTMENU
|
||||
IPTC
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
@@ -902,7 +886,6 @@ LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
logon
|
||||
lon
|
||||
LOGMSG
|
||||
LOGPIXELSX
|
||||
LOGPIXELSY
|
||||
@@ -994,7 +977,6 @@ MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metadatas
|
||||
metadatamatters
|
||||
metafile
|
||||
mfc
|
||||
Mgmt
|
||||
@@ -1193,8 +1175,8 @@ ntfs
|
||||
NTSTATUS
|
||||
NTSYSAPI
|
||||
NULLCURSOR
|
||||
nullonfailure
|
||||
nullref
|
||||
nullonfailure
|
||||
numberbox
|
||||
nwc
|
||||
ocr
|
||||
@@ -1314,7 +1296,6 @@ pnid
|
||||
PNMLINK
|
||||
Poc
|
||||
Podcasts
|
||||
Photoshop
|
||||
POINTERID
|
||||
POINTERUPDATE
|
||||
Pokedex
|
||||
@@ -1519,7 +1500,6 @@ sacl
|
||||
safeprojectname
|
||||
SAMEKEYPREVIOUSLYMAPPED
|
||||
SAMESHORTCUTPREVIOUSLYMAPPED
|
||||
samsung
|
||||
sancov
|
||||
SAVEFAILED
|
||||
scanled
|
||||
@@ -1873,7 +1853,6 @@ UPDATENOW
|
||||
UPDATEREGISTRY
|
||||
updown
|
||||
UPGRADINGPRODUCTCODE
|
||||
upscaling
|
||||
Uptool
|
||||
urld
|
||||
Usb
|
||||
@@ -1883,7 +1862,6 @@ USEINSTALLERFORTEST
|
||||
USESHOWWINDOW
|
||||
USESTDHANDLES
|
||||
USRDLL
|
||||
utm
|
||||
UType
|
||||
uuidv
|
||||
uwp
|
||||
@@ -1974,7 +1952,6 @@ wgpocpl
|
||||
WHEREID
|
||||
wic
|
||||
wifi
|
||||
wikimedia
|
||||
wikipedia
|
||||
WIL
|
||||
winapi
|
||||
@@ -2071,9 +2048,7 @@ XAxis
|
||||
XButton
|
||||
xclip
|
||||
xcopy
|
||||
xap
|
||||
XDeployment
|
||||
XDimension
|
||||
xdf
|
||||
XDocument
|
||||
XElement
|
||||
@@ -2091,7 +2066,6 @@ xsi
|
||||
XSpeed
|
||||
XStr
|
||||
xstyler
|
||||
xmp
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
@@ -2099,7 +2073,6 @@ xxxxxx
|
||||
YAxis
|
||||
ycombinator
|
||||
YIncrement
|
||||
YDimension
|
||||
yinle
|
||||
yinyue
|
||||
YPels
|
||||
|
||||
2
.github/actions/spell-check/patterns.txt
vendored
@@ -253,7 +253,7 @@ _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
|
||||
|
||||
# hit-count: 1 file-count: 1
|
||||
# Amazon
|
||||
\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)
|
||||
\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)[^"'\s]+
|
||||
|
||||
# hit-count: 3 file-count: 3
|
||||
# imgur
|
||||
|
||||
3
.gitignore
vendored
@@ -349,7 +349,10 @@ 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/
|
||||
|
||||
@@ -181,7 +181,6 @@
|
||||
"PowerToys.MousePointerCrosshairs.dll",
|
||||
"PowerToys.MouseJumpUI.dll",
|
||||
"PowerToys.MouseJumpUI.exe",
|
||||
"PowerToys.CursorWrap.dll",
|
||||
|
||||
"PowerToys.MouseWithoutBorders.dll",
|
||||
"PowerToys.MouseWithoutBorders.exe",
|
||||
@@ -291,7 +290,6 @@
|
||||
"Mono.Cecil.Rocks.dll",
|
||||
"Newtonsoft.Json.dll",
|
||||
"CommunityToolkit.WinUI.Controls.TitleBar.dll",
|
||||
"CommunityToolkit.WinUI.Controls.OpacityMaskView.dll",
|
||||
|
||||
"NLog.dll",
|
||||
"HtmlAgilityPack.dll",
|
||||
|
||||
@@ -65,28 +65,21 @@ if (-not (Test-Path $outputDir)) {
|
||||
New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
# 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 "DSC manifests will be generated to: '$outputDir'"
|
||||
|
||||
Write-Host "DSC manifests will be generated to: '$dscOutputDir'"
|
||||
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 "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)
|
||||
$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir)
|
||||
Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')"
|
||||
& $exePath @arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "PowerToys.DSC.exe exited with code $LASTEXITCODE"
|
||||
}
|
||||
|
||||
$generatedFiles = Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
|
||||
$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop
|
||||
if ($generatedFiles.Count -eq 0) {
|
||||
throw "No DSC manifest files were generated in '$dscOutputDir'."
|
||||
throw "No DSC manifest files were generated in '$outputDir'."
|
||||
}
|
||||
|
||||
Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):"
|
||||
|
||||
@@ -19,7 +19,7 @@ parameters:
|
||||
- name: enableMsBuildCaching
|
||||
type: boolean
|
||||
displayName: "Enable MSBuild Caching"
|
||||
default: true
|
||||
default: false
|
||||
- name: runTests
|
||||
type: boolean
|
||||
displayName: "Run Tests"
|
||||
|
||||
@@ -52,6 +52,8 @@ extends:
|
||||
name: SHINE-INT-S
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
${{ else }}:
|
||||
image: SHINE-VS17-Latest
|
||||
os: windows
|
||||
sdl:
|
||||
tsa:
|
||||
@@ -73,6 +75,7 @@ extends:
|
||||
name: SHINE-INT-L
|
||||
demands:
|
||||
# Our INT agents have a large disk mounted at P:\
|
||||
- WorkFolder -equals P:\_work
|
||||
- ${{ if eq(parameters.useVSPreview, true) }}:
|
||||
- ImageOverride -equals SHINE-VS17-Preview
|
||||
os: windows
|
||||
@@ -123,6 +126,7 @@ extends:
|
||||
parameters:
|
||||
pool:
|
||||
name: SHINE-INT-L
|
||||
image: SHINE-VS17-Latest
|
||||
os: windows
|
||||
official: true
|
||||
codeSign: true
|
||||
|
||||
@@ -111,7 +111,6 @@ jobs:
|
||||
${{ else }}:
|
||||
OutputBuildPlatform: ${{ platform }}
|
||||
variables:
|
||||
NUGET_PACKAGES: 'C:\NuGetPackages' # Some of our build steps cache these here... and it was apparently part of the global environment
|
||||
MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\MakeAppx.exe'
|
||||
# Azure DevOps abhors a vacuum
|
||||
# If these are blank, expansion will fail later on... which will result in direct substitution of the variable *names*
|
||||
@@ -140,10 +139,6 @@ jobs:
|
||||
- output: pipelineArtifact
|
||||
artifactName: $(JobOutputArtifactName)
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- output: pipelineArtifact
|
||||
artifactName: $(JobOutputArtifactName)-failure-$(System.JobAttempt)
|
||||
targetPath: $(LogOutputDirectory)
|
||||
condition: or(failed(), canceled())
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
@@ -400,7 +395,7 @@ jobs:
|
||||
### HACK: On ARM64 builds, building an app with Windows App SDK copies the x64 WebView2 dll instead of the ARM64 one. This task makes sure the right dll is used.
|
||||
- task: CopyFiles@2
|
||||
displayName: HACK Copy core WebView2 ARM64 dll to output directory
|
||||
condition: and(succeeded(), eq(variables['BuildPlatform'], 'arm64'))
|
||||
condition: eq(variables['BuildPlatform'],'arm64')
|
||||
inputs:
|
||||
contents: packages/Microsoft.Web.WebView2.1.0.2903.40/runtimes/win-ARM64/native_uap/Microsoft.Web.WebView2.Core.dll
|
||||
targetFolder: $(Build.SourcesDirectory)/ARM64/Release/WinUI3Apps/
|
||||
@@ -439,11 +434,11 @@ jobs:
|
||||
inputs:
|
||||
testResultsFormat: VSTest
|
||||
testResultsFiles: '**/*.trx'
|
||||
condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64'))
|
||||
condition: ne(variables['BuildPlatform'],'arm64')
|
||||
|
||||
# Native dlls
|
||||
- task: VSTest@2
|
||||
condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) # No arm64 agents to run the tests.
|
||||
condition: ne(variables['BuildPlatform'],'arm64') # No arm64 agents to run the tests.
|
||||
displayName: 'Native Tests'
|
||||
inputs:
|
||||
platform: '$(BuildPlatform)'
|
||||
|
||||
@@ -1,6 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<!-- Hybrid CRT configuration -->
|
||||
<PropertyGroup Condition="'$(HybridCrtConfiguration)'==''">
|
||||
<HybridCrtConfiguration>$(Configuration)</HybridCrtConfiguration>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Skip Hybrid CRT for AppContainer/UWP projects as they require MultiThreadedDLL -->
|
||||
<PropertyGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop'">
|
||||
<HybridCrtConfiguration></HybridCrtConfiguration>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemDefinitionGroup Condition="'$(HybridCrtConfiguration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(HybridCrtConfiguration)'=='Release'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- AppContainer/UWP projects must use MultiThreadedDLL -->
|
||||
<ItemDefinitionGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop' And '$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(AppContainerApplication)'=='true' and '$(_VC_Target_Library_Platform)'!='Desktop' And '$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- Project configurations -->
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
@@ -73,7 +123,6 @@
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -83,7 +132,6 @@
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
|
||||
@@ -147,18 +147,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.AdvancedPasteSemanticKernelFormatEvent</td>
|
||||
<td>Triggered when Advanced Paste leverages the Semantic Kernel.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.AdvancedPasteSemanticKernelErrorEvent</td>
|
||||
<td>Occurs when the Semantic Kernel workflow encounters an error.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.AdvancedPasteEndpointUsageEvent</td>
|
||||
<td>Logs the AI provider, model, and processing duration for each endpoint call.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.AdvancedPasteCustomActionErrorEvent</td>
|
||||
<td>Records provider, model, and status details when a custom action fails.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Always on Top
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
@@ -35,33 +34,35 @@
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.8" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.AI.Foundry.Local" Version="0.3.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.66.0" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Amazon" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.66.0-beta" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.HuggingFace" Version="1.66.0-preview" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.10" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
|
||||
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
|
||||
<!--
|
||||
@@ -94,29 +95,29 @@
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.8" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.10" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.8" />
|
||||
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
|
||||
<!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.8" />
|
||||
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.8" />
|
||||
<PackageVersion Include="System.ClientModel" Version="1.7.0" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.8" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.8" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
|
||||
@@ -1498,7 +1498,6 @@ SOFTWARE.
|
||||
- CoenM.ImageSharp.ImageHash
|
||||
- CommunityToolkit.Common
|
||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
||||
- CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView
|
||||
- CommunityToolkit.Mvvm
|
||||
- CommunityToolkit.WinUI.Animations
|
||||
- CommunityToolkit.WinUI.Collections
|
||||
|
||||
@@ -822,8 +822,6 @@ 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}"
|
||||
@@ -2992,14 +2990,6 @@ 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
|
||||
@@ -3361,7 +3351,6 @@ 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}
|
||||
|
||||
46
README.md
@@ -7,23 +7,19 @@
|
||||
<h1 align="center">
|
||||
<span>Microsoft PowerToys</span>
|
||||
</h1>
|
||||
<p align="center">
|
||||
<span align="center">Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.</span>
|
||||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="#-installation">Installation</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="https://aka.ms/powertoys-docs">Documentation</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="https://aka.ms/powertoys-releaseblog">Blog</a>
|
||||
<span> · </span>
|
||||
<span> . </span>
|
||||
<a href="#-whats-new">Release notes</a>
|
||||
</h3>
|
||||
<br/><br/>
|
||||
|
||||
## 🔨 Utilities
|
||||
|
||||
PowerToys includes over 25 utilities to help you customize and optimize your Windows experience:
|
||||
Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks.
|
||||
<br/><br/>
|
||||
|
||||
| | | |
|
||||
|---|---|---|
|
||||
@@ -41,13 +37,20 @@ PowerToys includes over 25 utilities to help you customize and optimize your Win
|
||||
|
||||
## 📋 Installation
|
||||
|
||||
For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
|
||||
For detailed installation instructions, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install).
|
||||
|
||||
Before you begin, make sure your device meets the system requirements:
|
||||
|
||||
> [!NOTE]
|
||||
> - Windows 11 or Windows 10 version 2004 (20H1 / build 19041) or newer
|
||||
> - 64-bit processor: x64 or ARM64
|
||||
> - Latest stable version of [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) is installed via the bootstrapper during setup
|
||||
|
||||
Choose one of the installation methods below:
|
||||
|
||||
But to get started quickly, choose one of the installation methods below:
|
||||
<br/><br/>
|
||||
<details open>
|
||||
<summary><strong>Download .exe from GitHub</strong></summary>
|
||||
<br/>
|
||||
<summary>Download .exe from GitHub</summary>
|
||||
|
||||
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.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
@@ -64,11 +67,11 @@ Go to the [PowerToys GitHub releases][github-release-link], click Assets to reve
|
||||
| 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] |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Microsoft Store</strong></summary>
|
||||
<br/>
|
||||
<summary>Microsoft Store</summary>
|
||||
You can easily install PowerToys from the Microsoft Store:
|
||||
<p>
|
||||
<a style="text-decoration:none" href="https://aka.ms/getPowertoys">
|
||||
@@ -79,9 +82,10 @@ You can easily install PowerToys from the Microsoft Store:
|
||||
</p>
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary><strong>WinGet</strong></summary>
|
||||
<br/>
|
||||
<summary>WinGet</summary>
|
||||
|
||||
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
*User scope installer [default]*
|
||||
@@ -96,8 +100,8 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Other methods</strong></summary>
|
||||
<br/>
|
||||
<summary>Other methods</summary>
|
||||
|
||||
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
@@ -4,7 +4,6 @@
|
||||
#include <ProjectTelemetry.h>
|
||||
#include <spdlog/sinks/base_sink.h>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
#include "../../src/common/logger/logger.h"
|
||||
#include "../../src/common/utils/gpo.h"
|
||||
@@ -857,69 +856,14 @@ UINT __stdcall UnsetAdvancedPasteAPIKeyCA(MSIHANDLE hInstall)
|
||||
|
||||
try
|
||||
{
|
||||
winrt::Windows::Security::Credentials::PasswordVault vault;
|
||||
winrt::Windows::Security::Credentials::PasswordCredential cred;
|
||||
|
||||
hr = WcaInitialize(hInstall, "UnsetAdvancedPasteAPIKey");
|
||||
ExitOnFailure(hr, "Failed to initialize");
|
||||
|
||||
winrt::Windows::Security::Credentials::PasswordVault vault;
|
||||
|
||||
auto hasPrefix = [](std::wstring_view value, wchar_t const* prefix) {
|
||||
std::wstring_view prefixView{ prefix };
|
||||
return value.compare(0, prefixView.size(), prefixView) == 0;
|
||||
};
|
||||
|
||||
const wchar_t* resourcePrefixes[] = {
|
||||
L"https://platform.openai.com/api-keys",
|
||||
L"https://azure.microsoft.com/products/ai-services/openai-service",
|
||||
L"https://azure.microsoft.com/products/ai-services/ai-inference",
|
||||
L"https://console.mistral.ai/account/api-keys",
|
||||
L"https://ai.google.dev/",
|
||||
};
|
||||
|
||||
const wchar_t* usernamePrefixes[] = {
|
||||
L"PowerToys_AdvancedPaste_",
|
||||
};
|
||||
|
||||
auto credentials = vault.RetrieveAll();
|
||||
for (auto const& credential : credentials)
|
||||
{
|
||||
bool shouldRemove = false;
|
||||
|
||||
std::wstring resource{ credential.Resource() };
|
||||
for (auto const prefix : resourcePrefixes)
|
||||
{
|
||||
if (hasPrefix(resource, prefix))
|
||||
{
|
||||
shouldRemove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRemove)
|
||||
{
|
||||
std::wstring username{ credential.UserName() };
|
||||
for (auto const prefix : usernamePrefixes)
|
||||
{
|
||||
if (hasPrefix(username, prefix))
|
||||
{
|
||||
shouldRemove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRemove)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
vault.Remove(credential);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
cred = vault.Retrieve(L"https://platform.openai.com/api-keys", L"PowerToys_AdvancedPaste_OpenAIKey");
|
||||
vault.Remove(cred);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
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""""
|
||||
@@ -110,7 +109,6 @@
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>_DEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
@@ -123,7 +121,6 @@
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>NDEBUG;_WINDOWS;_USRDLL;CUSTOMACTIONTEST_EXPORTS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
|
||||
@@ -4,13 +4,6 @@
|
||||
<Fragment>
|
||||
<DirectoryRef Id="WinUI3AppsInstallFolder">
|
||||
<Directory Id="CmdPalInstallFolder" Name="CmdPal">
|
||||
<Directory Id="CmdPalDepsInstallFolder" Name="Dependencies">
|
||||
<?if $(sys.BUILDARCH) = x64 ?>
|
||||
<Directory Id="CmdPalDepsX64InstallFolder" Name="x64" />
|
||||
<?else?>
|
||||
<Directory Id="CmdPalDepsArm64InstallFolder" Name="arm64" />
|
||||
<?endif?>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</DirectoryRef>
|
||||
<DirectoryRef Id="CmdPalInstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test">
|
||||
@@ -25,40 +18,14 @@
|
||||
<?endif?>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
<?if $(sys.BUILDARCH) = x64 ?>
|
||||
<DirectoryRef Id="CmdPalDepsX64InstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\x64">
|
||||
<Component Id="Module_CmdPal_Deps" Guid="C2790FC4-0665-4462-947A-D942A2AABFF0" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="Module_CmdPal_Deps" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="Microsoft.VCLibs.x64.14.00.Desktop.appx" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\x64\Microsoft.VCLibs.x64.14.00.Desktop.appx" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
<?else?>
|
||||
<DirectoryRef Id="CmdPalDepsArm64InstallFolder" FileSource="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\arm64">
|
||||
<Component Id="Module_CmdPal_Deps" Guid="C2790FC4-0665-4462-947A-D942A2AABFF0" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="Module_CmdPal_Deps" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="Microsoft.VCLibs.ARM64.14.00.Desktop.appx" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Dependencies\arm64\Microsoft.VCLibs.ARM64.14.00.Desktop.appx" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
<?endif?>
|
||||
<ComponentGroup Id="CmdPalComponentGroup">
|
||||
<Component Id="RemoveCmdPalFolder" Guid="2DF90C08-CC75-4245-A14E-B82904636C53" Directory="INSTALLFOLDER">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemoveCmdPalFolder" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveCmdPalInstallDirFolder" Directory="CmdPalInstallFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveCmdPalDepsInstallDirFolder" Directory="CmdPalDepsInstallFolder" On="uninstall" />
|
||||
<?if $(sys.BUILDARCH) = x64 ?>
|
||||
<RemoveFolder Id="RemoveCmdPalDepsX64InstallDirFolder" Directory="CmdPalDepsX64InstallFolder" On="uninstall" />
|
||||
<?else?>
|
||||
<RemoveFolder Id="RemoveCmdPalDepsArm64InstallDirFolder" Directory="CmdPalDepsArm64InstallFolder" On="uninstall" />
|
||||
<?endif?>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_CmdPal" />
|
||||
<ComponentRef Id="Module_CmdPal_Deps" />
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="powertoys_env_path_user" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<!-- Append DSCModules folder to current user's PATH for DSC v3 usage -->
|
||||
<Environment Id="AddPowerToysToUserPath" Name="PATH" Action="set" Part="last" System="no" Value="[DSCModulesReferenceFolder]" />
|
||||
<!-- Append install folder to current user's PATH -->
|
||||
<Environment Id="AddPowerToysToUserPath" Name="PATH" Action="set" Part="last" System="no" Value="[INSTALLFOLDER]" />
|
||||
</Component>
|
||||
<?else?>
|
||||
<Component Id="powertoys_env_path_machine" Bitness="always64">
|
||||
@@ -24,8 +24,8 @@
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="powertoys_env_path_machine" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<!-- Append DSCModules folder to machine PATH for DSC v3 usage -->
|
||||
<Environment Id="AddPowerToysToMachinePath" Name="PATH" Action="set" Part="last" System="yes" Value="[DSCModulesReferenceFolder]" />
|
||||
<!-- Append install folder to machine PATH -->
|
||||
<Environment Id="AddPowerToysToMachinePath" Name="PATH" Action="set" Part="last" System="yes" Value="[INSTALLFOLDER]" />
|
||||
</Component>
|
||||
<?endif?>
|
||||
<Component Id="powertoys_toast_clsid" Bitness="always64">
|
||||
@@ -63,6 +63,16 @@
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="DSCModulesReferenceFolder">
|
||||
<Component Id="PowerToysDSCReference" Guid="40869ACB-0BEB-4911-AE41-5E73BC1586A9" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="DSCModulesReference" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psd1" Id="PTConfReference.psd1" />
|
||||
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psm1" Id="PTConfReference.psm1" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<!-- DSC module files for PerUser handled in InstallDSCModule custom action. -->
|
||||
<?else?>
|
||||
@@ -110,6 +120,7 @@
|
||||
<RegistryValue Type="string" Name="RemoveCoreFolder" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveBaseApplicationsAssetsFolder" Directory="BaseApplicationsAssetsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveDSCModulesReferenceFolder" Directory="DSCModulesReferenceFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveWinUI3AppsInstallFolder" Directory="WinUI3AppsInstallFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveWinUI3AppsAssetsFolder" Directory="WinUI3AppsAssetsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall" />
|
||||
@@ -117,15 +128,16 @@
|
||||
<ComponentRef Id="powertoys_exe" />
|
||||
<ComponentRef Id="PowerToysStartMenuShortcut" />
|
||||
<ComponentRef Id="powertoys_per_machine_comp" />
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<ComponentRef Id="powertoys_env_path_user" />
|
||||
<?else?>
|
||||
<ComponentRef Id="powertoys_env_path_machine" />
|
||||
<?endif?>
|
||||
<ComponentRef Id="powertoys_toast_clsid" />
|
||||
<ComponentRef Id="License_rtf" />
|
||||
<ComponentRef Id="Notice_md" />
|
||||
<ComponentRef Id="DesktopShortcut" />
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<ComponentRef Id="powertoys_env_path_user" />
|
||||
<?else?>
|
||||
<ComponentRef Id="powertoys_env_path_machine" />
|
||||
<?endif?>
|
||||
<ComponentRef Id="PowerToysDSCReference" />
|
||||
<?if $(var.PerUser) = "false" ?>
|
||||
<ComponentRef Id="PowerToysDSC" />
|
||||
<?endif?>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<Project>
|
||||
<Import Project="..\..\Directory.Build.props" Condition="Exists('..\..\Directory.Build.props')" />
|
||||
<Import Project="..\..\src\Version.props" Condition="Exists('..\..\src\Version.props')" />
|
||||
<PropertyGroup>
|
||||
<!-- Set BaseIntermediateOutputPath for each project to avoid conflicts -->
|
||||
@@ -8,4 +9,4 @@
|
||||
<!-- Set MSBuildProjectExtensionsPath to use the BaseIntermediateOutputPath -->
|
||||
<MSBuildProjectExtensionsPath Condition="'$(BaseIntermediateOutputPath)' != ''">$(BaseIntermediateOutputPath)</MSBuildProjectExtensionsPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,33 +0,0 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
|
||||
<?include $(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<?define DscJsonFiles=?>
|
||||
<?define DscJsonFilesPath=$(var.BinDir)\DSCModules?>
|
||||
|
||||
<Fragment>
|
||||
<DirectoryRef Id="DSCModulesReferenceFolder" FileSource="$(var.DscJsonFilesPath)">
|
||||
<!-- DSC v2 PowerShell module files -->
|
||||
<Component Id="PowerToysDSCReference" Guid="40869ACB-0BEB-4911-AE41-5E73BC1586A9" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="DSCModulesReference" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psd1" Id="PTConfReference.psd1" />
|
||||
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version).0\Microsoft.PowerToys.Configure.psm1" Id="PTConfReference.psm1" />
|
||||
</Component>
|
||||
|
||||
<!-- DSC v3 JSON manifest files - Generated by generateAllFileComponents.ps1 -->
|
||||
<!--DscJsonFiles_Component_Def-->
|
||||
</DirectoryRef>
|
||||
|
||||
<ComponentGroup Id="DscResourcesComponentGroup">
|
||||
<ComponentRef Id="PowerToysDSCReference" />
|
||||
<Component Id="RemoveDSCModulesFolder" Guid="A3C77D92-4E97-4C1A-9F2E-8B3C5D6E7F80" Directory="DSCModulesReferenceFolder">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemoveDSCModulesFolder" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveDSCModulesReferenceFolder" Directory="DSCModulesReferenceFolder" On="uninstall" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -14,6 +14,7 @@ SET PTRoot=$(SolutionDir)\..
|
||||
call "..\..\..\publish.cmd" x64
|
||||
)
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
|
||||
</PreBuildEvent>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Platform)' != 'x64'">
|
||||
@@ -24,6 +25,7 @@ SET PTRoot=$(SolutionDir)\..
|
||||
call "..\..\..\publish.cmd" arm64
|
||||
)
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2"
|
||||
call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)"
|
||||
</PreBuildEvent>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
@@ -35,7 +37,6 @@ 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
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
|
||||
<Custom Action="SetUnApplyModulesRegistryChangeSetsParam" Before="UnApplyModulesRegistryChangeSets" />
|
||||
<Custom Action="CheckGPO" After="InstallInitialize" Condition="NOT Installed" />
|
||||
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
|
||||
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed" />
|
||||
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed" />
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
||||
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}</ProjectGuid>
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<TargetName>SilentFilesInUseBAFunction</TargetName>
|
||||
<ProjectName>PowerToysSetupCustomActionsVNext</ProjectName>
|
||||
<ProjectModuleDefinitionFile>bafunctions.def</ProjectModuleDefinitionFile>
|
||||
@@ -33,7 +10,6 @@
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
|
||||
<!-- Configuration-specific property groups -->
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
@@ -65,7 +41,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClCompile Include="SilentFilesInUseBAFunctions.cpp" />
|
||||
<ClCompile Include="SilentFilesInUseBAFunctions.cpp">
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ClCompile Include="bafunctions.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
|
||||
@@ -92,31 +71,5 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<!-- C++ source compile-specific things for Debug/Release configurations -->
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<Optimization>MaxSpeed</Optimization>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -317,7 +317,3 @@ Generate-FileComponents -fileListName "SettingsV2IconsModelsFiles" -wxsFilePath
|
||||
#Workspaces
|
||||
Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\"
|
||||
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
|
||||
|
||||
102
installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1
Normal file
@@ -0,0 +1,102 @@
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory = $True)]
|
||||
[string]$dscWxsFile,
|
||||
[Parameter(Mandatory = $True)]
|
||||
[string]$Platform,
|
||||
[Parameter(Mandatory = $True)]
|
||||
[string]$Configuration
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
# Find build output directory
|
||||
$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration"
|
||||
|
||||
if (-not (Test-Path $buildOutputDir)) {
|
||||
Write-Error "Build output directory not found: '$buildOutputDir'"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Find all DSC manifest JSON files
|
||||
$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File
|
||||
|
||||
if (-not $dscFiles) {
|
||||
Write-Warning "No DSC manifest files found in '$buildOutputDir'"
|
||||
# Create empty component group
|
||||
$wxsContent = @"
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
|
||||
<?include `$(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<Fragment>
|
||||
<ComponentGroup Id="DscResourcesComponentGroup">
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
"@
|
||||
Set-Content -Path $dscWxsFile -Value $wxsContent
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Found $($dscFiles.Count) DSC manifest file(s)"
|
||||
|
||||
# Generate WiX fragment
|
||||
$wxsContent = @"
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
|
||||
<?include `$(sys.CURRENTDIR)\Common.wxi?>
|
||||
|
||||
<Fragment>
|
||||
<DirectoryRef Id="DSCModulesReferenceFolder">
|
||||
"@
|
||||
|
||||
$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 += @"
|
||||
|
||||
<Component Id="$componentId" Guid="{$guid}" Directory="DSCModulesReferenceFolder">
|
||||
<RegistryKey Root="`$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="$componentId" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<File Id="$fileId" Source="`$(var.BinDir)$($file.Name)" Vital="no"/>
|
||||
</Component>
|
||||
"@
|
||||
}
|
||||
|
||||
$wxsContent += @"
|
||||
|
||||
</DirectoryRef>
|
||||
</Fragment>
|
||||
|
||||
<Fragment>
|
||||
<ComponentGroup Id="DscResourcesComponentGroup">
|
||||
"@
|
||||
|
||||
foreach ($componentId in $componentRefs) {
|
||||
$wxsContent += @"
|
||||
|
||||
<ComponentRef Id="$componentId"/>
|
||||
"@
|
||||
}
|
||||
|
||||
$wxsContent += @"
|
||||
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
"@
|
||||
|
||||
# Write the WiX file
|
||||
Set-Content -Path $dscWxsFile -Value $wxsContent
|
||||
|
||||
Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'"
|
||||
@@ -42,8 +42,7 @@
|
||||
Description="PowerToys OCR Module"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
@@ -52,8 +51,7 @@
|
||||
Description="PowerToys Settings UI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
@@ -62,8 +60,7 @@
|
||||
Description="PowerToys Image Resizer UI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
@@ -9,12 +9,6 @@
|
||||
<RootNamespace>CalculatorEngineCommon</RootNamespace>
|
||||
<AppxPackage>false</AppxPackage>
|
||||
</PropertyGroup>
|
||||
<!-- BEGIN common.build.pre.props -->
|
||||
<PropertyGroup Label="Configuration">
|
||||
<EnableHybridCRT>true</EnableHybridCRT>
|
||||
<UseCrtSDKReferenceStaticWarning Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReferenceStaticWarning>
|
||||
</PropertyGroup>
|
||||
<!-- END common.build.pre.props -->
|
||||
<!-- BEGIN cppwinrt.build.pre.props -->
|
||||
<PropertyGroup Label="Globals">
|
||||
<CppWinRTEnabled>true</CppWinRTEnabled>
|
||||
@@ -25,11 +19,9 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<MinimalCoreWin>true</MinimalCoreWin>
|
||||
<AppContainerApplication>true</AppContainerApplication>
|
||||
<AppContainerApplication>false</AppContainerApplication>
|
||||
<WindowsStoreApp>true</WindowsStoreApp>
|
||||
<ApplicationType>Windows Store</ApplicationType>
|
||||
<UseCrtSDKReference Condition="'$(EnableHybridCRT)'=='true'">false</UseCrtSDKReference>
|
||||
<!-- The SDK reference breaks the Hybrid CRT -->
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- We have to use the Desktop platform for Hybrid CRT to work. -->
|
||||
@@ -148,43 +140,5 @@
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
</Target>
|
||||
<!-- BEGIN common.build.post.props -->
|
||||
<!--
|
||||
The Hybrid CRT model statically links the runtime and STL and dynamically
|
||||
links the UCRT instead of the VC++ CRT. The UCRT ships with Windows.
|
||||
WinAppSDK asserts that this is "supported according to the CRT maintainer."
|
||||
|
||||
This must come before Microsoft.Cpp.targets because it manipulates ClCompile.RuntimeLibrary.
|
||||
-->
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and '$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">stdcpp17</LanguageStandard>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrtd.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrtd.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(EnableHybridCRT)'=='true' and ('$(Configuration)'=='Release' or '$(Configuration)'=='AuditMode')">
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreaded, rather than MultiThreadedDLL, to avoid DLL dependencies on VCRUNTIME140.dll and MSVCP140.dll. -->
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Link statically against the runtime and STL, but link dynamically against the CRT by ignoring the static CRT
|
||||
lib and instead linking against the Universal CRT DLL import library. This "hybrid" linking mechanism is
|
||||
supported according to the CRT maintainer. Dynamic linking against the CRT makes the binaries a bit smaller
|
||||
than they would otherwise be if the CRT, runtime, and STL were all statically linked in. -->
|
||||
<IgnoreSpecificDefaultLibraries>%(IgnoreSpecificDefaultLibraries);libucrt.lib</IgnoreSpecificDefaultLibraries>
|
||||
<AdditionalOptions>%(AdditionalOptions) /defaultlib:ucrt.lib</AdditionalOptions>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<!-- END common.build.post.props -->
|
||||
</Target> <!-- END common.build.post.props -->
|
||||
</Project>
|
||||
@@ -112,10 +112,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredMousePointerCrosshairsEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredCursorWrapEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredCursorWrapEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetConfiguredPowerRenameEnabledValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredPowerRenameEnabledValue());
|
||||
@@ -216,6 +212,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteGoogleValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteAnthropicValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteAnthropicValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteOllamaValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteOllamaValue());
|
||||
|
||||
@@ -35,7 +35,6 @@ 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();
|
||||
@@ -60,6 +59,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteAzureAIInferenceValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteMistralValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteGoogleValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteAnthropicValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteOllamaValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteFoundryLocalValue();
|
||||
static GpoRuleConfigured GetConfiguredNewPlusEnabledValue();
|
||||
|
||||
@@ -38,7 +38,6 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredCursorWrapEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredMouseWithoutBordersEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue();
|
||||
@@ -64,6 +63,7 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteAzureAIInferenceValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteMistralValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteGoogleValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteAnthropicValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteOllamaValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteFoundryLocalValue();
|
||||
static GpoRuleConfigured GetConfiguredNewPlusEnabledValue();
|
||||
|
||||
14
src/common/LanguageModelProvider/AppUtils.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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;
|
||||
|
||||
internal static class AppUtils
|
||||
{
|
||||
public static string GetThemeAssetSuffix()
|
||||
{
|
||||
// Default suffix for assets that are theme-agnostic today.
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -10,42 +10,32 @@ namespace LanguageModelProvider.FoundryLocal;
|
||||
internal sealed class FoundryClient
|
||||
{
|
||||
public static async Task<FoundryClient?> CreateAsync()
|
||||
{
|
||||
// First attempt with current environment
|
||||
var client = await TryCreateClientAsync().ConfigureAwait(false);
|
||||
if (client != null)
|
||||
{
|
||||
return client;
|
||||
}
|
||||
|
||||
// If failed, refresh PATH from registry and retry once
|
||||
// This handles cases where PowerToys was launched by MSI installer.
|
||||
Logger.LogInfo("[FoundryClient] First attempt failed, refreshing PATH and retrying");
|
||||
RefreshEnvironmentPath();
|
||||
|
||||
return await TryCreateClientAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<FoundryClient?> TryCreateClientAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] Creating Foundry Local client");
|
||||
|
||||
// Workaround for SDK issue: FoundryLocalManager.StartServiceAsync() uses UseShellExecute=false
|
||||
// which cannot handle Windows App Execution Aliases (foundry.exe in WindowsApps)
|
||||
// Pre-start the service using UseShellExecute=true
|
||||
await EnsureFoundryServiceStarted().ConfigureAwait(false);
|
||||
|
||||
var manager = new FoundryLocalManager();
|
||||
|
||||
// Check if service is already running
|
||||
if (manager.IsServiceRunning)
|
||||
// Check if service is running
|
||||
if (!manager.IsServiceRunning)
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] Foundry service is already running");
|
||||
return new FoundryClient(manager);
|
||||
Logger.LogInfo("[FoundryClient] Service not running, attempting to start via SDK");
|
||||
await manager.StartServiceAsync().ConfigureAwait(false);
|
||||
|
||||
if (!manager.IsServiceRunning)
|
||||
{
|
||||
Logger.LogError("[FoundryClient] Failed to start Foundry Local service");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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");
|
||||
Logger.LogInfo("[FoundryClient] Foundry Local service is running");
|
||||
return new FoundryClient(manager);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -60,6 +50,33 @@ internal sealed class FoundryClient
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task EnsureFoundryServiceStarted()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] Pre-starting foundry service with UseShellExecute=true");
|
||||
|
||||
using var process = new System.Diagnostics.Process();
|
||||
process.StartInfo.FileName = "foundry";
|
||||
process.StartInfo.Arguments = "service start";
|
||||
process.StartInfo.UseShellExecute = true; // Critical: allows App Execution Alias to work
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
process.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
|
||||
|
||||
process.Start();
|
||||
|
||||
// Give the service a moment to start, but don't wait for completion
|
||||
// as the service runs in the background
|
||||
await Task.Delay(2000).ConfigureAwait(false);
|
||||
|
||||
Logger.LogInfo("[FoundryClient] Foundry service start command completed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"[FoundryClient] Failed to pre-start foundry service: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly FoundryLocalManager _foundryManager;
|
||||
private readonly List<FoundryCatalogModel> _catalogModels = [];
|
||||
|
||||
@@ -185,95 +202,93 @@ internal sealed class FoundryClient
|
||||
}
|
||||
|
||||
public async Task<bool> EnsureModelLoaded(string modelId)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
public async Task EnsureRunning()
|
||||
{
|
||||
if (!_foundryManager.IsServiceRunning)
|
||||
{
|
||||
await _foundryManager.StartServiceAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the PATH environment variable from the system registry.
|
||||
/// This is necessary when tools are installed while PowerToys is running,
|
||||
/// as the installer updates the system PATH but running processes don't see the change.
|
||||
/// </summary>
|
||||
private static void RefreshEnvironmentPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] Refreshing PATH environment variable from system");
|
||||
Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}");
|
||||
|
||||
var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process) ?? string.Empty;
|
||||
var machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty;
|
||||
var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty;
|
||||
|
||||
var pathsToAdd = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(currentPath))
|
||||
// Check if already loaded
|
||||
if (await IsModelLoaded(modelId).ConfigureAwait(false))
|
||||
{
|
||||
pathsToAdd.AddRange(currentPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries));
|
||||
Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userPath))
|
||||
// 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))
|
||||
{
|
||||
var userPaths = userPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var path in userPaths)
|
||||
{
|
||||
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
pathsToAdd.Add(path);
|
||||
}
|
||||
}
|
||||
Logger.LogWarning($"[FoundryClient] Model not found in cache: {modelId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(machinePath))
|
||||
{
|
||||
var machinePaths = machinePath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var path in machinePaths)
|
||||
{
|
||||
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
pathsToAdd.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load the model
|
||||
Logger.LogInfo($"[FoundryClient] Loading model: {modelId}");
|
||||
await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false);
|
||||
|
||||
var newPath = string.Join(Path.PathSeparator.ToString(), pathsToAdd);
|
||||
|
||||
if (currentPath != newPath)
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] Updating process PATH with latest system values");
|
||||
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] PATH is already up to date");
|
||||
}
|
||||
// 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] Failed to refresh PATH: {ex.Message}");
|
||||
Logger.LogError($"[FoundryClient] EnsureModelLoaded exception: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FoundryDownloadResult> DownloadModel(FoundryCatalogModel model, IProgress<float>? progress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] Downloading model: {model.Name}");
|
||||
var models = await ListCachedModels().ConfigureAwait(false);
|
||||
|
||||
if (models.Any(m => m.Name == model.Name))
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] Model already downloaded: {model.Name}");
|
||||
return new(true, "Model already downloaded");
|
||||
}
|
||||
|
||||
// Use the SDK's download with progress
|
||||
// CA2016: The cancellationToken is properly forwarded via WithCancellation extension method
|
||||
#pragma warning disable CA2016
|
||||
await foreach (var downloadProgress in _foundryManager.DownloadModelWithProgressAsync(model.Name).WithCancellation(cancellationToken).ConfigureAwait(false))
|
||||
#pragma warning restore CA2016
|
||||
{
|
||||
if (downloadProgress.Percentage >= 0 && downloadProgress.Percentage <= 100)
|
||||
{
|
||||
progress?.Report((float)(downloadProgress.Percentage / 100));
|
||||
}
|
||||
|
||||
if (downloadProgress.IsCompleted)
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] Download completed: {model.Name}");
|
||||
return new FoundryDownloadResult(true, "Download completed successfully");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(downloadProgress.ErrorMessage))
|
||||
{
|
||||
Logger.LogError($"[FoundryClient] Download error: {downloadProgress.ErrorMessage}");
|
||||
return new FoundryDownloadResult(false, downloadProgress.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[FoundryClient] Download completed: {model.Name}");
|
||||
return new FoundryDownloadResult(true, "Download completed successfully");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] Download cancelled: {model.Name}");
|
||||
return new FoundryDownloadResult(false, "Download was cancelled");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"[FoundryClient] Download exception: {e.Message}");
|
||||
return new FoundryDownloadResult(false, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,64 +13,80 @@ namespace LanguageModelProvider;
|
||||
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
{
|
||||
private IEnumerable<ModelDetails>? _downloadedModels;
|
||||
private IEnumerable<FoundryCatalogModel>? _catalogModels;
|
||||
private FoundryClient? _foundryClient;
|
||||
private IEnumerable<ModelDetails>? _catalogModels;
|
||||
private FoundryClient? _foundryManager;
|
||||
private string? _serviceUrl;
|
||||
|
||||
public static FoundryLocalModelProvider Instance { get; } = new();
|
||||
|
||||
public string Name => "FoundryLocal";
|
||||
|
||||
public HardwareAccelerator ModelHardwareAccelerator => HardwareAccelerator.FOUNDRYLOCAL;
|
||||
|
||||
public string ProviderDescription => "The model will run locally via Foundry Local";
|
||||
|
||||
public IChatClient? GetIChatClient(string modelId)
|
||||
{
|
||||
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}");
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
public string UrlPrefix => "fl://";
|
||||
|
||||
public string Icon => $"fl{AppUtils.GetThemeAssetSuffix()}.svg";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Check if model is in catalog
|
||||
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
|
||||
if (!isInCatalog)
|
||||
{
|
||||
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
|
||||
Logger.LogError($"[FoundryLocal] {errorMessage}");
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
|
||||
// Check if model is cached
|
||||
var isInCache = _downloadedModels?.Any(m => m.ProviderModelDetails is FoundryCachedModel cached && cached.Name == modelId) ?? false;
|
||||
if (!isInCache)
|
||||
{
|
||||
var errorMessage = $"The requested model '{modelId}' is not cached. Please download it using Foundry Local.";
|
||||
Logger.LogError($"[FoundryLocal] {errorMessage}");
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
Logger.LogInfo($"[FoundryLocal] Extracted model ID: {modelId}");
|
||||
|
||||
// Ensure the model is loaded before returning chat client
|
||||
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
if (!isLoaded)
|
||||
try
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
|
||||
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
|
||||
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 = _foundryClient.GetServiceUri();
|
||||
var baseUri = _foundryManager.GetServiceUri();
|
||||
if (baseUri == null)
|
||||
{
|
||||
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
|
||||
Logger.LogError($"[FoundryLocal] {message}");
|
||||
throw new InvalidOperationException(message);
|
||||
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"),
|
||||
@@ -114,43 +130,84 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
return _downloadedModels ?? [];
|
||||
}
|
||||
|
||||
public IEnumerable<ModelDetails> GetAllModelsInCatalog()
|
||||
{
|
||||
Logger.LogInfo($"[FoundryLocal] Returning {_catalogModels?.Count() ?? 0} catalog models");
|
||||
return _catalogModels ?? [];
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadModel(ModelDetails modelDetails, IProgress<float>? progress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_foundryManager == null)
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Cannot download model: manager is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (modelDetails.ProviderModelDetails is not FoundryCatalogModel model)
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Cannot download model: invalid model details type");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[FoundryLocal] Starting download for model: {model.Name}");
|
||||
var result = await _foundryManager.DownloadModel(model, progress, cancellationToken);
|
||||
Logger.LogInfo($"[FoundryLocal] Download result: {result.Success}, error: {result.ErrorMessage ?? "none"}");
|
||||
return result.Success;
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_downloadedModels = null;
|
||||
_catalogModels = null;
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (_foundryClient != null && _downloadedModels != null && _downloadedModels.Any() && _catalogModels != null && _catalogModels.Any())
|
||||
if (_foundryManager != null && _downloadedModels != null && _downloadedModels.Any())
|
||||
{
|
||||
await _foundryClient.EnsureRunning().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo("[FoundryLocal] Initializing provider");
|
||||
_foundryClient ??= await FoundryClient.CreateAsync();
|
||||
_foundryManager ??= await FoundryClient.CreateAsync();
|
||||
|
||||
if (_foundryClient == null)
|
||||
if (_foundryManager == null)
|
||||
{
|
||||
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
|
||||
Logger.LogError($"[FoundryLocal] {message}");
|
||||
throw new InvalidOperationException(message);
|
||||
Logger.LogError("[FoundryLocal] Failed to create Foundry client");
|
||||
return;
|
||||
}
|
||||
|
||||
_serviceUrl ??= await _foundryClient.GetServiceUrl();
|
||||
_serviceUrl ??= await _foundryManager.GetServiceUrl();
|
||||
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
|
||||
|
||||
var catalogModels = await _foundryClient.ListCatalogModels();
|
||||
Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models");
|
||||
_catalogModels = catalogModels;
|
||||
if (_catalogModels == null || !_catalogModels.Any())
|
||||
{
|
||||
Logger.LogInfo("[FoundryLocal] Loading catalog models");
|
||||
_catalogModels = (await _foundryManager.ListCatalogModels()).Select(ToModelDetails).ToArray();
|
||||
Logger.LogInfo($"[FoundryLocal] Loaded {_catalogModels.Count()} catalog models");
|
||||
}
|
||||
|
||||
var cachedModels = await _foundryClient.ListCachedModels();
|
||||
var cachedModels = await _foundryManager.ListCachedModels();
|
||||
Logger.LogInfo($"[FoundryLocal] Found {cachedModels.Count} cached models");
|
||||
|
||||
List<ModelDetails> downloadedModels = [];
|
||||
|
||||
foreach (var model in _catalogModels)
|
||||
{
|
||||
var cachedModel = cachedModels.FirstOrDefault(m => m.Name == model.Name);
|
||||
|
||||
if (cachedModel != default)
|
||||
{
|
||||
// Use the actual model Name (ModelId), not the alias (Id)
|
||||
model.Id = $"fl-{model.Name}";
|
||||
model.Url = $"{UrlPrefix}{cachedModel.Name}";
|
||||
Logger.LogInfo($"[FoundryLocal] Adding cached model: {model.Name}, URL: {model.Url}");
|
||||
downloadedModels.Add(model);
|
||||
cachedModels.Remove(cachedModel);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var model in cachedModels)
|
||||
{
|
||||
Logger.LogInfo($"[FoundryLocal] Adding unmatched cached model: {model.Name}");
|
||||
@@ -158,7 +215,7 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
{
|
||||
Id = $"fl-{model.Name}",
|
||||
Name = model.Name,
|
||||
Url = $"fl://{model.Name}",
|
||||
Url = $"{UrlPrefix}{model.Name}",
|
||||
Description = $"{model.Name} running locally with Foundry Local",
|
||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||
SupportedOnQualcomm = true,
|
||||
@@ -170,11 +227,27 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
Logger.LogInfo($"[FoundryLocal] Initialization complete. Total downloaded models: {downloadedModels.Count}");
|
||||
}
|
||||
|
||||
private ModelDetails ToModelDetails(FoundryCatalogModel model)
|
||||
{
|
||||
return new ModelDetails
|
||||
{
|
||||
Id = $"fl-{model.Name}",
|
||||
Name = model.Name,
|
||||
Url = $"{UrlPrefix}{model.Name}",
|
||||
Description = $"{model.Alias} running locally with Foundry Local",
|
||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||
Size = model.FileSizeMb * 1024 * 1024,
|
||||
SupportedOnQualcomm = true,
|
||||
License = model.License?.ToLowerInvariant() ?? string.Empty,
|
||||
ProviderModelDetails = model,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> IsAvailable()
|
||||
{
|
||||
Logger.LogInfo("[FoundryLocal] Checking availability");
|
||||
await InitializeAsync();
|
||||
var available = _foundryClient != null;
|
||||
var available = _foundryManager != null;
|
||||
Logger.LogInfo($"[FoundryLocal] Available: {available}");
|
||||
return available;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,17 @@ public interface ILanguageModelProvider
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
string UrlPrefix { get; }
|
||||
|
||||
string Icon { get; }
|
||||
|
||||
HardwareAccelerator ModelHardwareAccelerator { get; }
|
||||
|
||||
string ProviderDescription { get; }
|
||||
|
||||
Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
|
||||
|
||||
IChatClient? GetIChatClient(string modelId);
|
||||
IChatClient? GetIChatClient(string url);
|
||||
|
||||
string GetIChatClientString(string url);
|
||||
}
|
||||
|
||||
106
src/common/LanguageModelProvider/LanguageModelService.cs
Normal file
@@ -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<string, ILanguageModelProvider> _providersByPrefix;
|
||||
|
||||
public LanguageModelService(IEnumerable<ILanguageModelProvider> providers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(providers);
|
||||
|
||||
_providersByPrefix = new ConcurrentDictionary<string, ILanguageModelProvider>(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<ILanguageModelProvider> 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<IReadOnlyList<ModelDetails>> GetModelsAsync(bool refresh = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
List<ModelDetails> 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);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ namespace ManagedCommon
|
||||
ColorPicker,
|
||||
CmdPal,
|
||||
CropAndLock,
|
||||
CursorWrap,
|
||||
EnvironmentVariables,
|
||||
FancyZones,
|
||||
FileLocksmith,
|
||||
|
||||
@@ -1,175 +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.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
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for configuring PowerToys settings for UI tests.
|
||||
/// </summary>
|
||||
public class SettingsConfigHelper
|
||||
{
|
||||
private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true };
|
||||
private static readonly SettingsUtils SettingsUtils = new SettingsUtils();
|
||||
|
||||
/// <summary>
|
||||
/// Configures global PowerToys settings to enable only specified modules and disable all others.
|
||||
/// </summary>
|
||||
/// <param name="modulesToEnable">Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when modulesToEnable is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
|
||||
[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<GeneralSettings>();
|
||||
}
|
||||
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<string, bool>();
|
||||
|
||||
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<Dictionary<string, object>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="moduleName">The name of the module (e.g., "Peek", "FancyZones").</param>
|
||||
/// <param name="defaultSettingsContent">The default JSON content to use if the settings file doesn't exist.</param>
|
||||
/// <param name="updateSettingsAction">
|
||||
/// 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; }
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when moduleName or updateSettingsAction is null.</exception>
|
||||
/// <exception cref="InvalidOperationException">Thrown when settings file operations fail.</exception>
|
||||
[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<Dictionary<string, object>> 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<string, object>? 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<Dictionary<string, object>>(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<Dictionary<string, object>>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<PublishAot>true</PublishAot>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
|
||||
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<PublishTrimmed>false</PublishTrimmed>
|
||||
</PropertyGroup>
|
||||
@@ -21,8 +21,4 @@
|
||||
<PackageReference Include="CoenM.ImageSharp.ImageHash" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -63,14 +63,12 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -59,7 +59,6 @@ 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";
|
||||
|
||||
@@ -46,16 +46,6 @@
|
||||
<PropertyGroup>
|
||||
<TargetName>notifications</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <Windows.h>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace powertoys_gpo
|
||||
{
|
||||
@@ -52,7 +51,6 @@ 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";
|
||||
@@ -89,6 +87,7 @@ namespace powertoys_gpo
|
||||
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";
|
||||
@@ -410,11 +409,6 @@ 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);
|
||||
@@ -614,6 +608,11 @@ namespace powertoys_gpo
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,11 +40,9 @@
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Generate the DSC resource JSON files to DSCModules subfolder -->
|
||||
<!-- Skip generation in CI/CD builds (CIBuild=true) to avoid unnecessary work during pipeline -->
|
||||
<Target Name="GenerateDscResourceJsonFiles" AfterTargets="Build" Condition="'$(CIBuild)' != 'true'">
|
||||
<Message Text="Generating DSC resource JSON files to DSCModules subfolder..." Importance="high" />
|
||||
<MakeDir Directories="$(TargetDir)DSCModules" />
|
||||
<Exec Command="dotnet "$(TargetPath)" manifest --resource settings --outputDir "$(TargetDir)DSCModules"" />
|
||||
<!-- In debug mode, generate the DSC resource JSON files -->
|
||||
<Target Name="GenerateDscResourceJsonFiles" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
|
||||
<Message Text="Generating DSC resource JSON files inside ..." Importance="high" />
|
||||
<Exec Command="dotnet "$(TargetPath)" manifest --resource settings --outputDir "$(TargetDir)\"" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -49,8 +49,6 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
|
||||
|
||||
public bool CloseAfterLosingFocus => false;
|
||||
|
||||
public bool EnableClipboardPreview => true;
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;
|
||||
|
||||
@@ -59,8 +59,10 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageReference Include="MessagePack" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Amazon" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Google" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.HuggingFace" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.MistralAI" />
|
||||
<PackageReference Include="Microsoft.SemanticKernel.Connectors.Ollama" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace AdvancedPaste
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
protected async override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
if (cmdArgs?.Length > 1)
|
||||
@@ -134,6 +134,8 @@ namespace AdvancedPaste
|
||||
{
|
||||
ProcessNamedPipe(cmdArgs[2]);
|
||||
}
|
||||
|
||||
await ShowWindow(); // This allows for direct access without using PowerToys Runner, not all functionality might work
|
||||
}
|
||||
|
||||
private void ProcessNamedPipe(string pipeName)
|
||||
|
||||
@@ -14,13 +14,28 @@
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
|
||||
<LinearGradientBrush x:Name="IntelligentUnderlineGradient" StartPoint="0,0.5" EndPoint="1,0.5">
|
||||
<GradientStop Offset="0.0" Color="#FF0078D4" />
|
||||
<GradientStop Offset="0.42" Color="#FF464FEB" />
|
||||
<GradientStop Offset="0.87" Color="#FFD660FF" />
|
||||
<GradientStop Offset="1.0" Color="#FFFEA874" />
|
||||
</LinearGradientBrush>
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<Color x:Key="AccentGradientColor">#65C8F2</Color>
|
||||
<LinearGradientBrush x:Key="AccentGradientBrush" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Offset="0.0" Color="#98EFFE" />
|
||||
<GradientStop Offset="0.25" Color="#48B1E9" />
|
||||
<GradientStop Offset="1.0" Color="{StaticResource AccentGradientColor}" />
|
||||
</LinearGradientBrush>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<Color x:Key="AccentGradientColor">#005FB8</Color>
|
||||
<LinearGradientBrush x:Key="AccentGradientBrush" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Offset="0.0" Color="#4992C7" />
|
||||
<GradientStop Offset="0.25" Color="#1353A0" />
|
||||
<GradientStop Offset="1.0" Color="{StaticResource AccentGradientColor}" />
|
||||
</LinearGradientBrush>
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<Color x:Key="AccentGradientColor">#48B1E9</Color>
|
||||
<SolidColorBrush x:Key="AccentGradientBrush" Color="{StaticResource AccentGradientColor}" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
<x:Double x:Key="ModelSelectorButtonWidth">44</x:Double>
|
||||
<Style x:Key="CustomTextBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="Foreground" Value="{ThemeResource TextControlForeground}" />
|
||||
@@ -156,19 +171,6 @@
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Control.IsTemplateFocusTarget="True"
|
||||
CornerRadius="{TemplateBinding CornerRadius}" />
|
||||
<Rectangle
|
||||
x:Name="FocusHighlighter"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="1"
|
||||
Grid.ColumnSpan="4"
|
||||
Height="2"
|
||||
Margin="12,0,12,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Bottom"
|
||||
Fill="{StaticResource IntelligentUnderlineGradient}"
|
||||
RadiusX="1"
|
||||
RadiusY="1"
|
||||
Visibility="Collapsed" />
|
||||
<Grid Grid.Row="1" Width="{StaticResource ModelSelectorButtonWidth}">
|
||||
<ProgressRing
|
||||
Width="20"
|
||||
@@ -280,10 +282,7 @@
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBackgroundFocused}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusHighlighter" Storyboard.TargetProperty="Visibility">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderBrush">
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0">
|
||||
<DiscreteObjectKeyFrame.Value>
|
||||
<LinearGradientBrush MappingMode="Absolute" StartPoint="0,0" EndPoint="0,2">
|
||||
@@ -300,7 +299,7 @@
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderThickness">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBorderThemeThicknessFocused}" />
|
||||
</ObjectAnimationUsingKeyFrames>-->
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentElement" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlForegroundFocused}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
@@ -542,10 +541,7 @@
|
||||
Source="{x:Bind ViewModel.ActiveAIProvider?.ServiceType, Mode=OneWay, Converter={StaticResource ServiceTypeToIconConverter}}" />
|
||||
</DropDownButton.Content>
|
||||
<DropDownButton.Flyout>
|
||||
<Flyout
|
||||
Opened="AIProviderFlyout_Opened"
|
||||
Placement="Bottom"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
<Flyout Placement="Bottom" ShouldConstrainToRootBounds="False">
|
||||
<Grid
|
||||
Width="386"
|
||||
Margin="-4"
|
||||
@@ -558,7 +554,7 @@
|
||||
<TextBlock
|
||||
x:Uid="AIProvidersFlyoutHeader"
|
||||
Grid.Row="0"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
<ListView
|
||||
x:Name="AIProviderListView"
|
||||
Grid.Row="1"
|
||||
@@ -611,10 +607,10 @@
|
||||
CornerRadius="{StaticResource ControlCornerRadius}"
|
||||
Visibility="{x:Bind IsLocalModel, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Uid="LocalModelBadge"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="Local" />
|
||||
</Border>
|
||||
<!--<Border
|
||||
Grid.Column="2"
|
||||
@@ -744,8 +740,8 @@
|
||||
x:Name="LoadingText"
|
||||
x:Uid="LoadingText"
|
||||
Grid.Row="1"
|
||||
Margin="4,4,0,0"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource AccentGradientBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Visibility="Collapsed">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
|
||||
@@ -22,8 +22,6 @@ namespace AdvancedPaste.Controls
|
||||
{
|
||||
public OptionsViewModel ViewModel { get; private set; }
|
||||
|
||||
private bool _syncingProviderSelection;
|
||||
|
||||
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
|
||||
nameof(PlaceholderText),
|
||||
typeof(string),
|
||||
@@ -76,11 +74,6 @@ namespace AdvancedPaste.Controls
|
||||
var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
|
||||
VisualStateManager.GoToState(this, state, true);
|
||||
}
|
||||
|
||||
if (e.PropertyName is nameof(ViewModel.ActiveAIProvider) or nameof(ViewModel.AIProviders))
|
||||
{
|
||||
SyncProviderSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_PreviewRequested(object sender, EventArgs e)
|
||||
@@ -94,7 +87,6 @@ namespace AdvancedPaste.Controls
|
||||
private void Grid_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
InputTxtBox.Focus(FocusState.Programmatic);
|
||||
SyncProviderSelection();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -134,56 +126,18 @@ namespace AdvancedPaste.Controls
|
||||
Loader.IsLoading = loading;
|
||||
}
|
||||
|
||||
private void SyncProviderSelection()
|
||||
{
|
||||
if (AIProviderListView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_syncingProviderSelection = true;
|
||||
AIProviderListView.SelectedItem = ViewModel.ActiveAIProvider;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_syncingProviderSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AIProviderFlyout_Opened(object sender, object e)
|
||||
{
|
||||
SyncProviderSelection();
|
||||
}
|
||||
|
||||
private async void AIProviderListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_syncingProviderSelection)
|
||||
if (AIProviderListView.SelectedItem is PasteAIProviderDefinition provider)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (ViewModel.SetActiveProviderCommand.CanExecute(provider))
|
||||
{
|
||||
await ViewModel.SetActiveProviderCommand.ExecuteAsync(provider);
|
||||
}
|
||||
|
||||
var flyout = FlyoutBase.GetAttachedFlyout(AIProviderButton);
|
||||
|
||||
if (AIProviderListView.SelectedItem is not PasteAIProviderDefinition provider)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(ViewModel.ActiveAIProvider?.Id, provider.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var flyout = FlyoutBase.GetAttachedFlyout(AIProviderButton);
|
||||
flyout?.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ViewModel.SetActiveProviderCommand.CanExecute(provider))
|
||||
{
|
||||
await ViewModel.SetActiveProviderCommand.ExecuteAsync(provider);
|
||||
SyncProviderSelection();
|
||||
}
|
||||
|
||||
flyout?.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="20"
|
||||
Visibility="{x:Bind ViewModel.ShowClipboardPreview, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
Visibility="{x:Bind ViewModel.ClipboardHasData, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
@@ -168,8 +168,7 @@
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{x:Bind ViewModel.ClipboardHistoryEnabled, Mode=TwoWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind ViewModel.ShowClipboardHistoryButton, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="ClipboardHistoryButtonToolTip" />
|
||||
</ToolTipService.ToolTip>
|
||||
@@ -264,17 +263,16 @@
|
||||
<Button
|
||||
Padding="0"
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind ViewModel.HasLegalLinks, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
Style="{StaticResource SubtleButtonStyle}">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
<Button.Flyout>
|
||||
<Flyout>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run x:Uid="AIMistakeNote" /><LineBreak /><Run
|
||||
x:Uid="CustomEndpointWarning"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="You are using a custom endpoint. Verify all answers." />
|
||||
</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<HyperlinkButton
|
||||
@@ -282,15 +280,13 @@
|
||||
x:Uid="TermsLink"
|
||||
Padding="0"
|
||||
FontSize="12"
|
||||
NavigateUri="{x:Bind ViewModel.TermsLinkUri, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.HasTermsLink, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
NavigateUri="https://openai.com/policies/terms-of-use" />
|
||||
<HyperlinkButton
|
||||
x:Name="PrivacyHyperLink"
|
||||
x:Uid="PrivacyLink"
|
||||
Padding="0"
|
||||
FontSize="12"
|
||||
NavigateUri="{x:Bind ViewModel.PrivacyLinkUri, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.HasPrivacyLink, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
NavigateUri="https://openai.com/policies/privacy-policy" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
@@ -299,49 +295,47 @@
|
||||
</StackPanel>
|
||||
</controls:PromptBox.Footer>
|
||||
</controls:PromptBox>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<Grid RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListView
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None"
|
||||
TabIndex="1" />
|
||||
<Rectangle
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
<Grid Grid.Row="2" RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListView
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="1" />
|
||||
<Rectangle
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
|
||||
<ListView
|
||||
x:Name="CustomActionsListView"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<ListView
|
||||
x:Name="CustomActionsListView"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace AdvancedPaste.Helpers
|
||||
PromptTokens = semanticKernelFormatEvent.PromptTokens;
|
||||
CompletionTokens = semanticKernelFormatEvent.CompletionTokens;
|
||||
ModelName = semanticKernelFormatEvent.ModelName;
|
||||
ProviderType = semanticKernelFormatEvent.ProviderType;
|
||||
ActionChain = semanticKernelFormatEvent.ActionChain;
|
||||
}
|
||||
|
||||
@@ -39,8 +38,6 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
public string ModelName { get; set; }
|
||||
|
||||
public string ProviderType { get; set; }
|
||||
|
||||
public string ActionChain { get; set; }
|
||||
|
||||
public string ToJsonString() => JsonSerializer.Serialize(this, SourceGenerationContext.Default.AIServiceFormatEvent);
|
||||
|
||||
@@ -19,8 +19,6 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
|
||||
public bool EnableClipboardPreview { get; }
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions { get; }
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions { get; }
|
||||
|
||||
@@ -40,8 +40,6 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool CloseAfterLosingFocus { get; private set; }
|
||||
|
||||
public bool EnableClipboardPreview { get; private set; }
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
@@ -55,7 +53,6 @@ namespace AdvancedPaste.Settings
|
||||
IsAIEnabled = false;
|
||||
ShowCustomPreview = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
EnableClipboardPreview = true;
|
||||
PasteAIConfiguration = new PasteAIConfiguration();
|
||||
_additionalActions = [];
|
||||
_customActions = [];
|
||||
@@ -110,7 +107,6 @@ namespace AdvancedPaste.Settings
|
||||
IsAIEnabled = properties.IsAIEnabled;
|
||||
ShowCustomPreview = properties.ShowCustomPreview;
|
||||
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
|
||||
EnableClipboardPreview = properties.EnableClipboardPreview;
|
||||
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
|
||||
|
||||
var sourceAdditionalActions = properties.AdditionalActions;
|
||||
@@ -167,134 +163,28 @@ namespace AdvancedPaste.Settings
|
||||
return false;
|
||||
}
|
||||
|
||||
var properties = settings.Properties;
|
||||
bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
|
||||
bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
|
||||
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
|
||||
|
||||
if (legacyCredential is null)
|
||||
if (settings.Properties.IsAIEnabled || !LegacyOpenAIKeyExists())
|
||||
{
|
||||
return legacyAdvancedAIConsumed;
|
||||
return false;
|
||||
}
|
||||
|
||||
var configuration = properties.PasteAIConfiguration;
|
||||
|
||||
if (configuration is null)
|
||||
{
|
||||
configuration = new PasteAIConfiguration();
|
||||
properties.PasteAIConfiguration = configuration;
|
||||
}
|
||||
|
||||
bool configurationUpdated = false;
|
||||
|
||||
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
|
||||
PasteAIProviderDefinition openAIProvider = ensureResult.Provider;
|
||||
configurationUpdated |= ensureResult.Updated;
|
||||
|
||||
if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
|
||||
{
|
||||
openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled;
|
||||
configurationUpdated = true;
|
||||
}
|
||||
|
||||
if (openAIProvider is not null)
|
||||
{
|
||||
StoreMigratedOpenAICredential(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
|
||||
RemoveLegacyOpenAICredential();
|
||||
}
|
||||
|
||||
const bool shouldEnableAI = true;
|
||||
bool enabledUpdated = false;
|
||||
if (properties.IsAIEnabled != shouldEnableAI)
|
||||
{
|
||||
properties.IsAIEnabled = shouldEnableAI;
|
||||
enabledUpdated = true;
|
||||
}
|
||||
|
||||
return configurationUpdated || enabledUpdated || legacyAdvancedAIConsumed;
|
||||
settings.Properties.IsAIEnabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static PasswordCredential TryGetLegacyOpenAICredential()
|
||||
private static bool LegacyOpenAIKeyExists()
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordVault vault = new();
|
||||
var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
||||
credential?.RetrievePassword();
|
||||
return credential;
|
||||
return vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey") is not null;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveLegacyOpenAICredential()
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordVault vault = new();
|
||||
TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void StoreMigratedOpenAICredential(string providerId, string serviceType, string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var serviceKind = serviceType.ToAIServiceType();
|
||||
if (serviceKind != AIServiceType.OpenAI)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string resource = "https://platform.openai.com/api-keys";
|
||||
string username = $"PowerToys_AdvancedPaste_PasteAI_openai_{NormalizeProviderIdentifier(providerId)}";
|
||||
|
||||
PasswordVault vault = new();
|
||||
TryRemoveCredential(vault, resource, username);
|
||||
|
||||
PasswordCredential credential = new(resource, username, password);
|
||||
vault.Add(credential);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to migrate legacy OpenAI credential", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName)
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName);
|
||||
vault.Remove(existingCred);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Credential doesn't exist, which is fine
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public async Task SetActiveAIProviderAsync(string providerId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerId))
|
||||
|
||||
@@ -11,6 +11,12 @@ 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;
|
||||
@@ -214,7 +220,7 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
|
||||
var serviceType = GetRuntimeConfiguration().ServiceType;
|
||||
return new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Required(),
|
||||
Temperature = 0.01,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,10 +11,8 @@ 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;
|
||||
|
||||
@@ -83,39 +81,28 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
SystemPrompt = systemPrompt,
|
||||
};
|
||||
|
||||
var operationStart = DateTime.UtcNow;
|
||||
|
||||
var providerContent = await provider.ProcessPasteAsync(
|
||||
request,
|
||||
cancellationToken,
|
||||
progress);
|
||||
|
||||
var durationMs = (int)Math.Round((DateTime.UtcNow - operationStart).TotalMilliseconds);
|
||||
|
||||
var usage = request.Usage;
|
||||
var content = providerContent ?? string.Empty;
|
||||
|
||||
// Log endpoint usage (custom action pipeline is not the advanced SK flow)
|
||||
var endpointEvent = new AdvancedPasteEndpointUsageEvent(providerConfig.ProviderType, providerConfig.Model ?? string.Empty, isAdvanced: false, durationMs: durationMs);
|
||||
PowerToysTelemetry.Log.WriteEvent(endpointEvent);
|
||||
|
||||
Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}, DurationMs={durationMs}");
|
||||
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);
|
||||
var statusCode = ExtractStatusCode(ex);
|
||||
var modelName = providerConfig.Model ?? string.Empty;
|
||||
AdvancedPasteCustomActionErrorEvent errorEvent = new(providerConfig.ProviderType, modelName, statusCode, ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
var statusCode = ExtractStatusCode(ex);
|
||||
var failureMessage = providerConfig.ProviderType switch
|
||||
{
|
||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => ErrorHelpers.TranslateErrorText(statusCode),
|
||||
@@ -188,6 +175,8 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
AIServiceType.Onnx => false,
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using LanguageModelProvider;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -24,7 +23,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
|
||||
public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new FoundryLocalPasteProvider(config));
|
||||
|
||||
private static readonly FoundryLocalModelProvider _modelProvider = FoundryLocalModelProvider.Instance;
|
||||
private static readonly LanguageModelService LanguageModels = LanguageModelService.CreateDefault();
|
||||
|
||||
private readonly PasteAIConfig _config;
|
||||
|
||||
@@ -34,6 +33,10 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey();
|
||||
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model;
|
||||
|
||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -69,26 +72,22 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
throw new PasteActionException(
|
||||
"No Foundry Local model selected",
|
||||
new InvalidOperationException("Model identifier is required"),
|
||||
aiServiceMessage: "Please select a model in the AI provider settings.");
|
||||
aiServiceMessage: "Please select a model in the AI provider settings. Model identifier should be in the format 'fl://model-name'.");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
IChatClient chatClient;
|
||||
try
|
||||
var chatClient = LanguageModels.GetClient(modelReference);
|
||||
if (chatClient is null)
|
||||
{
|
||||
chatClient = _modelProvider.GetIChatClient(modelReference);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// GetIChatClient throws InvalidOperationException for user-facing errors
|
||||
var errorMessage = string.Format(System.Globalization.CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("FoundryLocal_UnableToLoadModel"), modelReference);
|
||||
throw new PasteActionException(
|
||||
errorMessage,
|
||||
ex,
|
||||
aiServiceMessage: ex.Message);
|
||||
$"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}
|
||||
@@ -105,7 +104,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
new(ChatRole.User, userMessageContent),
|
||||
};
|
||||
|
||||
var chatOptions = CreateChatOptions(_config?.SystemPrompt, modelReference);
|
||||
var chatOptions = CreateChatOptions(_config?.SystemPrompt, actualModelId);
|
||||
|
||||
progress?.Report(0.1);
|
||||
|
||||
|
||||
@@ -11,8 +11,10 @@ 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;
|
||||
@@ -27,8 +29,11 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
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));
|
||||
@@ -137,12 +142,21 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
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)}");
|
||||
@@ -170,6 +184,8 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
return serviceType switch
|
||||
{
|
||||
AIServiceType.Ollama => false,
|
||||
AIServiceType.Anthropic => false,
|
||||
AIServiceType.AmazonBedrock => false,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -156,10 +156,16 @@ public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider
|
||||
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;
|
||||
|
||||
@@ -29,7 +29,6 @@ public abstract class KernelServiceBase(
|
||||
ICustomActionTransformService customActionTransformService) : IKernelService
|
||||
{
|
||||
private const string PromptParameterName = "prompt";
|
||||
private const string DefaultSystemPrompt = "You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task. Call function when necessary to help user finish the transformation 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. Do not output anything else besides the reformatted clipboard content.";
|
||||
|
||||
private readonly IKernelQueryCacheService _queryCacheService = queryCacheService;
|
||||
private readonly IPromptModerationService _promptModerationService = promptModerationService;
|
||||
@@ -145,8 +144,7 @@ public abstract class KernelServiceBase(
|
||||
|
||||
ChatHistory chatHistory = [];
|
||||
|
||||
var systemPrompt = string.IsNullOrWhiteSpace(runtimeConfig.SystemPrompt) ? DefaultSystemPrompt : runtimeConfig.SystemPrompt;
|
||||
chatHistory.AddSystemMessage(systemPrompt);
|
||||
chatHistory.AddSystemMessage(runtimeConfig.SystemPrompt);
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
@@ -188,22 +186,8 @@ public abstract class KernelServiceBase(
|
||||
|
||||
private void LogResult(bool cacheUsed, bool isSavedQuery, IEnumerable<ActionChainItem> actionChain, AIServiceUsage usage)
|
||||
{
|
||||
var runtimeConfig = GetRuntimeConfiguration();
|
||||
|
||||
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(
|
||||
cacheUsed,
|
||||
isSavedQuery,
|
||||
usage.PromptTokens,
|
||||
usage.CompletionTokens,
|
||||
AdvancedAIModelName,
|
||||
runtimeConfig.ServiceType.ToString(),
|
||||
AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
|
||||
AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, AdvancedAIModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain));
|
||||
PowerToysTelemetry.Log.WriteEvent(telemetryEvent);
|
||||
|
||||
// Log endpoint usage
|
||||
var endpointEvent = new AdvancedPasteEndpointUsageEvent(runtimeConfig.ServiceType, AdvancedAIModelName, isAdvanced: true);
|
||||
PowerToysTelemetry.Log.WriteEvent(endpointEvent);
|
||||
|
||||
var logEvent = new AIServiceFormatEvent(telemetryEvent);
|
||||
Logger.LogDebug($"{nameof(TransformClipboardAsync)} complete; {logEvent.ToJsonString()}");
|
||||
}
|
||||
|
||||
@@ -160,10 +160,10 @@
|
||||
<value>Active provider: {0}</value>
|
||||
</data>
|
||||
<data name="AIProvidersFlyoutHeader.Text" xml:space="preserve">
|
||||
<value>Configured models</value>
|
||||
<value>AI providers</value>
|
||||
</data>
|
||||
<data name="AIProvidersEmptyText.Text" xml:space="preserve">
|
||||
<value>No models configured</value>
|
||||
<value>No AI providers configured</value>
|
||||
</data>
|
||||
<data name="AIProvidersManageButtonContent.Content" xml:space="preserve">
|
||||
<value>Configure models in Settings</value>
|
||||
@@ -359,17 +359,6 @@
|
||||
</data>
|
||||
<data name="Relative_Date_TimeFormat" xml:space="preserve">
|
||||
<value>{0}, {1}</value>
|
||||
<comment>(e.g., "10/20/2025, 17:05" in the user's locale)</comment>
|
||||
</data>
|
||||
<data name="CustomEndpointWarning" xml:space="preserve">
|
||||
<value>You are using a custom endpoint. Verify all answers.</value>
|
||||
</data>
|
||||
<data name="LocalModelBadge.Text" xml:space="preserve">
|
||||
<value>Local</value>
|
||||
<comment>Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally</comment>
|
||||
</data>
|
||||
<data name="FoundryLocal_UnableToLoadModel" xml:space="preserve">
|
||||
<value>Unable to load Foundry Local model: {0}</value>
|
||||
<comment>{0} is the model identifier. Do not translate {0}.</comment>
|
||||
<comment>(e.g., “10/20/2025, 17:05” in the user’s locale)</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -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.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 sealed class AdvancedPasteCustomActionErrorEvent : EventBase, IEvent
|
||||
{
|
||||
public AdvancedPasteCustomActionErrorEvent(AIServiceType providerType, string modelName, int statusCode, string error)
|
||||
{
|
||||
ProviderType = providerType.ToString();
|
||||
ModelName = modelName;
|
||||
StatusCode = statusCode;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public string ProviderType { get; set; }
|
||||
|
||||
public string ModelName { get; set; }
|
||||
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
public string Error { get; set; }
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
}
|
||||
@@ -1,46 +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.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
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the AI provider type (e.g., OpenAI, AzureOpenAI, Google).
|
||||
/// </summary>
|
||||
public string ProviderType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the configured model name.
|
||||
/// </summary>
|
||||
public string ModelName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the advanced AI pipeline was used.
|
||||
/// </summary>
|
||||
public bool IsAdvanced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total duration in milliseconds, or -1 if unavailable.
|
||||
/// </summary>
|
||||
public int DurationMs { get; set; }
|
||||
|
||||
public AdvancedPasteEndpointUsageEvent(AIServiceType providerType, string modelName, bool isAdvanced, int durationMs = -1)
|
||||
{
|
||||
ProviderType = providerType.ToString();
|
||||
ModelName = modelName;
|
||||
IsAdvanced = isAdvanced;
|
||||
DurationMs = durationMs;
|
||||
}
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace AdvancedPaste.Telemetry;
|
||||
|
||||
[EventData]
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSavedQuery, int promptTokens, int completionTokens, string modelName, string providerType, string actionChain) : EventBase, IEvent
|
||||
public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSavedQuery, int promptTokens, int completionTokens, string modelName, string actionChain) : EventBase, IEvent
|
||||
{
|
||||
public static string FormatActionChain(IEnumerable<ActionChainItem> actionChain) => FormatActionChain(actionChain.Select(item => item.Format));
|
||||
|
||||
@@ -30,8 +30,6 @@ public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSaved
|
||||
|
||||
public string ModelName { get; set; } = modelName;
|
||||
|
||||
public string ProviderType { get; set; } = providerType;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a comma-separated list of paste formats used - in the same order they were executed.
|
||||
/// Conceptually an array but formatted this way to work around https://github.com/dotnet/runtime/issues/10428
|
||||
|
||||
@@ -63,7 +63,6 @@ namespace AdvancedPaste.ViewModels
|
||||
private ClipboardFormat _availableClipboardFormats;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowClipboardHistoryButton))]
|
||||
private bool _clipboardHistoryEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -72,12 +71,6 @@ namespace AdvancedPaste.ViewModels
|
||||
[NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))]
|
||||
[NotifyPropertyChangedFor(nameof(AllowedAIProviders))]
|
||||
[NotifyPropertyChangedFor(nameof(ActiveAIProvider))]
|
||||
[NotifyPropertyChangedFor(nameof(ActiveAIProviderTooltip))]
|
||||
[NotifyPropertyChangedFor(nameof(TermsLinkUri))]
|
||||
[NotifyPropertyChangedFor(nameof(PrivacyLinkUri))]
|
||||
[NotifyPropertyChangedFor(nameof(HasTermsLink))]
|
||||
[NotifyPropertyChangedFor(nameof(HasPrivacyLink))]
|
||||
[NotifyPropertyChangedFor(nameof(HasLegalLinks))]
|
||||
private bool _isAllowedByGPO;
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -194,45 +187,10 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
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 HasLegalLinks => HasTermsLink || HasPrivacyLink;
|
||||
|
||||
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
|
||||
|
||||
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
|
||||
|
||||
public bool ShowClipboardPreview => _userSettings.EnableClipboardPreview;
|
||||
|
||||
public bool ShowClipboardHistoryButton => ClipboardHistoryEnabled;
|
||||
|
||||
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
|
||||
|
||||
private PasteFormats CustomAIFormat =>
|
||||
@@ -318,9 +276,8 @@ namespace AdvancedPaste.ViewModels
|
||||
OnPropertyChanged(nameof(IsAdvancedAIEnabled));
|
||||
OnPropertyChanged(nameof(AIProviders));
|
||||
OnPropertyChanged(nameof(AllowedAIProviders));
|
||||
OnPropertyChanged(nameof(ShowClipboardPreview));
|
||||
|
||||
NotifyActiveProviderChanged();
|
||||
OnPropertyChanged(nameof(ActiveAIProvider));
|
||||
OnPropertyChanged(nameof(ActiveAIProviderTooltip));
|
||||
|
||||
EnqueueRefreshPasteFormats();
|
||||
}
|
||||
@@ -359,18 +316,8 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
NotifyActiveProviderChanged();
|
||||
}
|
||||
|
||||
private void NotifyActiveProviderChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(ActiveAIProvider));
|
||||
OnPropertyChanged(nameof(ActiveAIProviderTooltip));
|
||||
OnPropertyChanged(nameof(TermsLinkUri));
|
||||
OnPropertyChanged(nameof(PrivacyLinkUri));
|
||||
OnPropertyChanged(nameof(HasTermsLink));
|
||||
OnPropertyChanged(nameof(HasPrivacyLink));
|
||||
OnPropertyChanged(nameof(HasLegalLinks));
|
||||
}
|
||||
|
||||
private void RefreshPasteFormats()
|
||||
@@ -808,6 +755,7 @@ namespace AdvancedPaste.ViewModels
|
||||
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
|
||||
@@ -889,7 +837,6 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
UpdateAIProviderActiveFlags();
|
||||
OnPropertyChanged(nameof(AIProviders));
|
||||
NotifyActiveProviderChanged();
|
||||
EnqueueRefreshPasteFormats();
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,6 @@
|
||||
<ClCompile>
|
||||
<Optimization>Disabled</Optimization>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">MultiThreadedDebug</RuntimeLibrary>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
|
||||
@@ -95,8 +93,6 @@
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">MultiThreaded</RuntimeLibrary>
|
||||
<RuntimeLibrary Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</ClCompile>
|
||||
@@ -49,7 +48,6 @@
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
|
||||
@@ -1,7 +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.
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Hosts.Tests.Mocks;
|
||||
using HostsUILib.Helpers;
|
||||
@@ -15,7 +19,6 @@ namespace Hosts.FuzzTests
|
||||
{
|
||||
private static Mock<IUserSettings> _userSettings;
|
||||
private static Mock<IElevationHelper> _elevationHelper;
|
||||
private static Mock<IBackupManager> _backupManager;
|
||||
|
||||
// Case1: Fuzzing method for ValidIPv4
|
||||
public static void FuzzValidIPv4(ReadOnlySpan<byte> input)
|
||||
@@ -70,10 +73,9 @@ namespace Hosts.FuzzTests
|
||||
_userSettings = new Mock<IUserSettings>();
|
||||
_elevationHelper = new Mock<IElevationHelper>();
|
||||
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
|
||||
_backupManager = new Mock<IBackupManager>();
|
||||
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
|
||||
string input = System.Text.Encoding.UTF8.GetString(data);
|
||||
|
||||
|
||||
@@ -30,11 +30,8 @@
|
||||
<Compile Include="..\HostsUILib\Models\Entry.cs" Link="Entry.cs" />
|
||||
<Compile Include="..\HostsUILib\Models\HostsData.cs" Link="HostsData.cs" />
|
||||
<Compile Include="..\HostsUILib\Settings\HostsAdditionalLinesPosition.cs" Link="HostsAdditionalLinesPosition.cs" />
|
||||
<Compile Include="..\HostsUILib\Settings\HostsDeleteBackupMode.cs" Link="HostsDeleteBackupMode.cs" />
|
||||
<Compile Include="..\HostsUILib\Settings\HostsEncoding.cs" Link="HostsEncoding.cs" />
|
||||
<Compile Include="..\HostsUILib\Settings\IUserSettings.cs" Link="IUserSettings.cs" />
|
||||
<Compile Include="..\HostsUILib\Helpers\IBackupManager.cs" Link="IBackupManager.cs" />
|
||||
<Compile Include="..\HostsUILib\Helpers\BackupManager.cs" Link="BackupManager.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,156 +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.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<IUserSettings>();
|
||||
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<IUserSettings>();
|
||||
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<IUserSettings>();
|
||||
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<IUserSettings>();
|
||||
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<IUserSettings>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,8 @@ namespace Hosts.Tests
|
||||
[TestClass]
|
||||
public class HostsServiceTest
|
||||
{
|
||||
private const string BackupPath = @"C:\Backup\hosts";
|
||||
private static Mock<IUserSettings> _userSettings;
|
||||
private static Mock<IElevationHelper> _elevationHelper;
|
||||
private static Mock<IBackupManager> _backupManager;
|
||||
|
||||
[ClassInitialize]
|
||||
public static void ClassInitialize(TestContext context)
|
||||
@@ -31,7 +29,27 @@ namespace Hosts.Tests
|
||||
_userSettings = new Mock<IUserSettings>();
|
||||
_elevationHelper = new Mock<IElevationHelper>();
|
||||
_elevationHelper.Setup(m => m.IsElevated).Returns(true);
|
||||
_backupManager = new Mock<IBackupManager>();
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -49,7 +67,7 @@ namespace Hosts.Tests
|
||||
";
|
||||
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var data = await service.ReadAsync();
|
||||
@@ -74,7 +92,7 @@ namespace Hosts.Tests
|
||||
";
|
||||
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var data = await service.ReadAsync();
|
||||
@@ -100,7 +118,7 @@ namespace Hosts.Tests
|
||||
";
|
||||
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var data = await service.ReadAsync();
|
||||
@@ -119,7 +137,7 @@ namespace Hosts.Tests
|
||||
public async Task Empty_Hosts()
|
||||
{
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty));
|
||||
|
||||
await service.WriteAsync(string.Empty, Enumerable.Empty<Entry>());
|
||||
@@ -150,7 +168,7 @@ namespace Hosts.Tests
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Top);
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var data = await service.ReadAsync();
|
||||
@@ -182,7 +200,7 @@ namespace Hosts.Tests
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var userSettings = new Mock<IUserSettings>();
|
||||
userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom);
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var data = await service.ReadAsync();
|
||||
@@ -206,7 +224,7 @@ namespace Hosts.Tests
|
||||
";
|
||||
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
fileSystem.AddFile(service.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var data = await service.ReadAsync();
|
||||
@@ -223,7 +241,7 @@ namespace Hosts.Tests
|
||||
var elevationHelper = new Mock<IElevationHelper>();
|
||||
elevationHelper.Setup(m => m.IsElevated).Returns(false);
|
||||
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object);
|
||||
await Assert.ThrowsExceptionAsync<NotRunningElevatedException>(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty<Entry>()));
|
||||
}
|
||||
|
||||
@@ -231,7 +249,7 @@ namespace Hosts.Tests
|
||||
public async Task Save_ReadOnlyHostsException()
|
||||
{
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
|
||||
var hostsFile = new MockFileData(string.Empty)
|
||||
{
|
||||
@@ -247,7 +265,7 @@ namespace Hosts.Tests
|
||||
public void Remove_ReadOnly_Attribute()
|
||||
{
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
|
||||
var hostsFile = new MockFileData(string.Empty)
|
||||
{
|
||||
@@ -266,7 +284,7 @@ namespace Hosts.Tests
|
||||
public async Task Save_Hidden_Hosts()
|
||||
{
|
||||
var fileSystem = new CustomMockFileSystem();
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object);
|
||||
|
||||
var hostsFile = new MockFileData(string.Empty)
|
||||
{
|
||||
@@ -298,7 +316,7 @@ namespace Hosts.Tests
|
||||
var fs = new CustomMockFileSystem();
|
||||
var settings = new Mock<IUserSettings>();
|
||||
settings.Setup(s => s.NoLeadingSpaces).Returns(true);
|
||||
var svc = new HostsService(fs, settings.Object, _elevationHelper.Object, _backupManager.Object);
|
||||
var svc = new HostsService(fs, settings.Object, _elevationHelper.Object);
|
||||
fs.AddFile(svc.HostsFilePath, new MockFileData(content));
|
||||
|
||||
var data = await svc.ReadAsync();
|
||||
@@ -309,57 +327,5 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ namespace Hosts
|
||||
{
|
||||
// Core Services
|
||||
services.AddSingleton<IFileSystem, FileSystem>();
|
||||
services.AddSingleton<IBackupManager, BackupManager>();
|
||||
services.AddSingleton<IHostsService, HostsService>();
|
||||
services.AddSingleton<IUserSettings, Hosts.Settings.UserSettings>();
|
||||
services.AddSingleton<IElevationHelper, ElevationHelper>();
|
||||
@@ -75,7 +74,7 @@ namespace Hosts
|
||||
}).
|
||||
Build();
|
||||
|
||||
var deleteBackupThread = new Thread(() =>
|
||||
var cleanupBackupThread = new Thread(() =>
|
||||
{
|
||||
// Delete old backups only if running elevated
|
||||
if (!Host.GetService<IElevationHelper>().IsElevated)
|
||||
@@ -85,7 +84,7 @@ namespace Hosts
|
||||
|
||||
try
|
||||
{
|
||||
Host.GetService<IBackupManager>().Delete();
|
||||
Host.GetService<IHostsService>().CleanupBackup();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -93,8 +92,8 @@ namespace Hosts
|
||||
}
|
||||
});
|
||||
|
||||
deleteBackupThread.IsBackground = true;
|
||||
deleteBackupThread.Start();
|
||||
cleanupBackupThread.IsBackground = true;
|
||||
cleanupBackupThread.Start();
|
||||
|
||||
UnhandledException += App_UnhandledException;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -45,34 +45,17 @@ namespace Hosts.Settings
|
||||
public HostsAdditionalLinesPosition AdditionalLinesPosition { get; private set; }
|
||||
|
||||
// Moved from Settings.UI.Library
|
||||
public HostsEncoding Encoding { get; private set; }
|
||||
|
||||
public bool BackupHosts { get; private set; }
|
||||
|
||||
public string BackupPath { get; private set; }
|
||||
|
||||
// Moved from Settings.UI.Library
|
||||
public HostsDeleteBackupMode DeleteBackupsMode { get; private set; }
|
||||
|
||||
public int DeleteBackupsDays { get; private set; }
|
||||
|
||||
public int DeleteBackupsCount { get; private set; }
|
||||
public HostsEncoding Encoding { get; set; }
|
||||
|
||||
public event EventHandler LoopbackDuplicatesChanged;
|
||||
|
||||
public UserSettings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
var defaultSettings = new HostsProperties();
|
||||
ShowStartupWarning = defaultSettings.ShowStartupWarning;
|
||||
LoopbackDuplicates = defaultSettings.LoopbackDuplicates;
|
||||
AdditionalLinesPosition = (HostsAdditionalLinesPosition)defaultSettings.AdditionalLinesPosition;
|
||||
Encoding = (HostsEncoding)defaultSettings.Encoding;
|
||||
BackupHosts = defaultSettings.BackupHosts;
|
||||
BackupPath = defaultSettings.BackupPath;
|
||||
DeleteBackupsMode = (HostsDeleteBackupMode)defaultSettings.DeleteBackupsMode;
|
||||
DeleteBackupsDays = defaultSettings.DeleteBackupsDays;
|
||||
DeleteBackupsCount = defaultSettings.DeleteBackupsCount;
|
||||
ShowStartupWarning = true;
|
||||
LoopbackDuplicates = false;
|
||||
AdditionalLinesPosition = HostsAdditionalLinesPosition.Top;
|
||||
Encoding = HostsEncoding.Utf8;
|
||||
|
||||
LoadSettingsFromJson();
|
||||
|
||||
@@ -108,11 +91,6 @@ namespace Hosts.Settings
|
||||
Encoding = (HostsEncoding)settings.Properties.Encoding;
|
||||
LoopbackDuplicates = settings.Properties.LoopbackDuplicates;
|
||||
NoLeadingSpaces = settings.Properties.NoLeadingSpaces;
|
||||
BackupHosts = settings.Properties.BackupHosts;
|
||||
BackupPath = settings.Properties.BackupPath;
|
||||
DeleteBackupsMode = (HostsDeleteBackupMode)settings.Properties.DeleteBackupsMode;
|
||||
DeleteBackupsDays = settings.Properties.DeleteBackupsDays;
|
||||
DeleteBackupsCount = settings.Properties.DeleteBackupsCount;
|
||||
}
|
||||
|
||||
retry = false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted ..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h HostsModuleInterface.base.rc HostsModuleInterface.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h HostsModuleInterface.base.rc HostsModuleInterface.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
@@ -46,7 +46,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories>$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,112 +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.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<IFileInfo> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
@@ -22,15 +23,16 @@ namespace HostsUILib.Helpers
|
||||
{
|
||||
public partial class HostsService : IHostsService, IDisposable
|
||||
{
|
||||
private const int DefaultBufferSize = 4096; // From System.IO.File source code
|
||||
private const string _backupSuffix = $"_PowerToysBackup_";
|
||||
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;
|
||||
@@ -42,13 +44,11 @@ namespace HostsUILib.Helpers
|
||||
public HostsService(
|
||||
IFileSystem fileSystem,
|
||||
IUserSettings userSettings,
|
||||
IElevationHelper elevationHelper,
|
||||
IBackupManager backupManager)
|
||||
IElevationHelper elevationHelper)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_userSettings = userSettings;
|
||||
_elevationHelper = elevationHelper;
|
||||
_backupManager = backupManager;
|
||||
|
||||
_hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc\hosts");
|
||||
|
||||
@@ -60,13 +60,18 @@ namespace HostsUILib.Helpers
|
||||
_fileSystemWatcher.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
public bool Exists()
|
||||
{
|
||||
return _fileSystem.File.Exists(HostsFilePath);
|
||||
}
|
||||
|
||||
public async Task<HostsData> ReadAsync()
|
||||
{
|
||||
var entries = new List<Entry>();
|
||||
var unparsedBuilder = new StringBuilder();
|
||||
var splittedEntries = false;
|
||||
|
||||
if (!_fileSystem.File.Exists(HostsFilePath))
|
||||
if (!Exists())
|
||||
{
|
||||
return new HostsData(entries, unparsedBuilder.ToString(), false);
|
||||
}
|
||||
@@ -187,10 +192,15 @@ namespace HostsUILib.Helpers
|
||||
{
|
||||
await _asyncLock.WaitAsync();
|
||||
_fileSystemWatcher.EnableRaisingEvents = false;
|
||||
_backupManager.Create(HostsFilePath);
|
||||
|
||||
if (!_backupDone && Exists())
|
||||
{
|
||||
_fileSystem.File.Copy(HostsFilePath, HostsFilePath + _backupSuffix + DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture));
|
||||
_backupDone = true;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
@@ -221,6 +231,15 @@ 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;
|
||||
|
||||
@@ -1,13 +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.
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
public interface IBackupManager
|
||||
{
|
||||
void Create(string hostsFilePath);
|
||||
|
||||
void Delete();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ namespace HostsUILib.Helpers
|
||||
|
||||
Task<bool> PingAsync(string address);
|
||||
|
||||
void CleanupBackup();
|
||||
|
||||
void OpenHostsFile();
|
||||
|
||||
void RemoveReadOnlyAttribute();
|
||||
|
||||
@@ -1,13 +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.
|
||||
|
||||
namespace HostsUILib.Settings
|
||||
{
|
||||
public enum HostsDeleteBackupMode
|
||||
{
|
||||
Never = 0,
|
||||
Count = 1,
|
||||
Age = 2,
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,6 @@ 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();
|
||||
|
||||
@@ -11,17 +11,19 @@
|
||||
#include <logger/logger_settings.h>
|
||||
#include <logger/logger.h>
|
||||
#include <utils/logger_helper.h>
|
||||
#include "LightSwitchStateManager.h"
|
||||
#include <LightSwitchUtils.h>
|
||||
#include <LightSwitchServiceObserver.h>
|
||||
|
||||
SERVICE_STATUS g_ServiceStatus = {};
|
||||
SERVICE_STATUS_HANDLE g_StatusHandle = nullptr;
|
||||
HANDLE g_ServiceStopEvent = nullptr;
|
||||
extern int g_lastUpdatedDay = -1;
|
||||
static ScheduleMode prevMode = ScheduleMode::Off;
|
||||
static std::wstring prevLat, prevLon;
|
||||
static int prevMinutes = -1;
|
||||
|
||||
VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
|
||||
VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl);
|
||||
DWORD WINAPI ServiceWorkerThread(LPVOID lpParam);
|
||||
void ApplyTheme(bool shouldBeLight);
|
||||
|
||||
// Entry point for the executable
|
||||
int _tmain(int argc, TCHAR* argv[])
|
||||
@@ -122,66 +124,31 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl)
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyTheme(bool shouldBeLight)
|
||||
static void update_sun_times(auto& settings)
|
||||
{
|
||||
const auto& s = LightSwitchSettings::settings();
|
||||
|
||||
if (s.changeSystem)
|
||||
{
|
||||
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
|
||||
if (shouldBeLight != isSystemCurrentlyLight)
|
||||
{
|
||||
SetSystemTheme(shouldBeLight);
|
||||
Logger::info(L"[LightSwitchService] Changed system theme to {}.", shouldBeLight ? L"light" : L"dark");
|
||||
}
|
||||
}
|
||||
|
||||
if (s.changeApps)
|
||||
{
|
||||
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
|
||||
if (shouldBeLight != isAppsCurrentlyLight)
|
||||
{
|
||||
SetAppsTheme(shouldBeLight);
|
||||
Logger::info(L"[LightSwitchService] Changed apps theme to {}.", shouldBeLight ? L"light" : L"dark");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateManager)
|
||||
{
|
||||
const auto& s = LightSwitchSettings::settings();
|
||||
if (s.scheduleMode == ScheduleMode::Off)
|
||||
return;
|
||||
double latitude = std::stod(settings.latitude);
|
||||
double longitude = std::stod(settings.longitude);
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
// Compute effective boundaries (with offsets if needed)
|
||||
int effectiveLight = s.lightTime;
|
||||
int effectiveDark = s.darkTime;
|
||||
SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay);
|
||||
|
||||
if (s.scheduleMode == ScheduleMode::SunsetToSunrise)
|
||||
int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
|
||||
int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
|
||||
try
|
||||
{
|
||||
effectiveLight = (s.lightTime + s.sunrise_offset) % 1440;
|
||||
effectiveDark = (s.darkTime + s.sunset_offset) % 1440;
|
||||
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
|
||||
values.add_property(L"lightTime", newLightTime);
|
||||
values.add_property(L"darkTime", newDarkTime);
|
||||
values.save_to_settings_file();
|
||||
|
||||
Logger::info(L"[LightSwitchService] Updated sun times and saved to config.");
|
||||
}
|
||||
|
||||
// Use shared helper (handles wraparound logic)
|
||||
bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark);
|
||||
|
||||
// Compare current system/apps theme
|
||||
bool currentSystemLight = GetCurrentSystemTheme();
|
||||
bool currentAppsLight = GetCurrentAppsTheme();
|
||||
|
||||
bool systemMismatch = s.changeSystem && (currentSystemLight != shouldBeLight);
|
||||
bool appsMismatch = s.changeApps && (currentAppsLight != shouldBeLight);
|
||||
|
||||
// Trigger manual override only if mismatch and not already active
|
||||
if ((systemMismatch || appsMismatch) && !stateManager.GetState().isManualOverride)
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] External theme change detected (Windows Settings). Entering manual override mode.");
|
||||
stateManager.OnManualOverride();
|
||||
std::wstring wmsg(e.what(), e.what() + strlen(e.what()));
|
||||
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,16 +162,326 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
Logger::info(L"[LightSwitchService] Worker thread starting...");
|
||||
Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Initialization
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
static LightSwitchStateManager stateManager;
|
||||
|
||||
LightSwitchSettings::instance().InitFileWatcher();
|
||||
|
||||
HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent();
|
||||
LightSwitchServiceObserver observer({ SettingId::LightTime,
|
||||
SettingId::DarkTime,
|
||||
SettingId::ScheduleMode,
|
||||
SettingId::Sunrise_Offset,
|
||||
SettingId::Sunset_Offset });
|
||||
|
||||
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 = (lightMinutes < darkMinutes) ? (nowMinutes >= lightMinutes && nowMinutes < darkMinutes) : (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.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
if (settings.scheduleMode != ScheduleMode::Off)
|
||||
{
|
||||
applyTheme(nowMinutes,
|
||||
settings.lightTime + settings.sunrise_offset,
|
||||
settings.darkTime + settings.sunset_offset,
|
||||
settings);
|
||||
Logger::trace(L"[LightSwitchService] Initialized g_lastUpdatedDay = {}", g_lastUpdatedDay);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change.");
|
||||
}
|
||||
|
||||
g_lastUpdatedDay = st.wDay;
|
||||
ULONGLONG lastSettingsReload = 0;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
HANDLE waits[2] = { g_ServiceStopEvent, hParent };
|
||||
DWORD count = hParent ? 2 : 1;
|
||||
bool skipRest = false;
|
||||
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
bool scheduleJustEnabled = (prevMode == ScheduleMode::Off && settings.scheduleMode != ScheduleMode::Off);
|
||||
prevMode = settings.scheduleMode;
|
||||
|
||||
// ─── Handle "Schedule Off" Mode ─────────────────────────────────────────────
|
||||
if (settings.scheduleMode == ScheduleMode::Off)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive.");
|
||||
|
||||
if (!hManualOverride)
|
||||
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
|
||||
HANDLE waitsOff[4];
|
||||
DWORD countOff = 0;
|
||||
waitsOff[countOff++] = g_ServiceStopEvent;
|
||||
if (hParent)
|
||||
waitsOff[countOff++] = hParent;
|
||||
if (hManualOverride)
|
||||
waitsOff[countOff++] = hManualOverride;
|
||||
waitsOff[countOff++] = LightSwitchSettings::instance().GetSettingsChangedEvent();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
DWORD wait = WaitForMultipleObjects(countOff, waitsOff, FALSE, INFINITE);
|
||||
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
|
||||
goto cleanup;
|
||||
}
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent exited - stopping service.");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Manual override received while schedule OFF.");
|
||||
ResetEvent(hManualOverride);
|
||||
continue;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Schedule re-enabled, resuming normal loop.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ─── Normal Schedule Loop ───────────────────────────────────────────────────
|
||||
ULONGLONG nowTick = GetTickCount64();
|
||||
bool recentSettingsReload = (nowTick - lastSettingsReload < 5000);
|
||||
|
||||
if (g_lastUpdatedDay != -1)
|
||||
{
|
||||
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
|
||||
|
||||
if (settings.scheduleMode != ScheduleMode::Off && !recentSettingsReload && !scheduleJustEnabled)
|
||||
{
|
||||
Logger::debug(L"[LightSwitchService] Checking if manual override is active...");
|
||||
bool manualOverrideActive = (hManualOverride && WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
|
||||
Logger::debug(L"[LightSwitchService] Manual override active = {}", manualOverrideActive);
|
||||
|
||||
if (!manualOverrideActive)
|
||||
{
|
||||
bool currentSystemTheme = GetCurrentSystemTheme();
|
||||
bool currentAppsTheme = GetCurrentAppsTheme();
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
bool shouldBeLight = (settings.lightTime < settings.darkTime) ? (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime) : (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime);
|
||||
|
||||
Logger::debug(L"[LightSwitchService] shouldBeLight = {}", shouldBeLight);
|
||||
|
||||
if ((settings.changeSystem && (currentSystemTheme != shouldBeLight)) ||
|
||||
(settings.changeApps && (currentAppsTheme != shouldBeLight)))
|
||||
{
|
||||
Logger::debug(L"[LightSwitchService] External theme change detected - enabling manual override");
|
||||
|
||||
if (!hManualOverride)
|
||||
{
|
||||
hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
if (!hManualOverride)
|
||||
hManualOverride = CreateEventW(nullptr, TRUE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE");
|
||||
}
|
||||
|
||||
if (hManualOverride)
|
||||
{
|
||||
SetEvent(hManualOverride);
|
||||
Logger::info(L"[LightSwitchService] Detected manual theme change outside of LightSwitch. Triggering manual override.");
|
||||
skipRest = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"[LightSwitchService] Skipping external-change detection (schedule off, recent reload, or just enabled).");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Apply Schedule Logic ───────────────────────────────────────────────────
|
||||
if (!skipRest)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
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<int>(currentSettings.scheduleMode));
|
||||
Logger::info(msg);
|
||||
|
||||
bool manualOverrideActive = false;
|
||||
if (hManualOverride)
|
||||
manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0);
|
||||
|
||||
if (manualOverrideActive)
|
||||
{
|
||||
int lightBoundary = (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440;
|
||||
int darkBoundary = (currentSettings.darkTime + currentSettings.sunset_offset) % 1440;
|
||||
|
||||
bool crossedLight = false;
|
||||
bool crossedDark = false;
|
||||
|
||||
if (prevMinutes != -1)
|
||||
{
|
||||
if (nowMinutes < prevMinutes)
|
||||
{
|
||||
crossedLight = (prevMinutes <= lightBoundary || nowMinutes >= lightBoundary);
|
||||
crossedDark = (prevMinutes <= darkBoundary || nowMinutes >= darkBoundary);
|
||||
}
|
||||
else
|
||||
{
|
||||
crossedLight = (prevMinutes < lightBoundary && nowMinutes >= lightBoundary);
|
||||
crossedDark = (prevMinutes < darkBoundary && nowMinutes >= darkBoundary);
|
||||
}
|
||||
}
|
||||
|
||||
Logger::debug(L"[LightSwitchService] prevMinutes={} nowMinutes={} light={} dark={}",
|
||||
prevMinutes,
|
||||
nowMinutes,
|
||||
lightBoundary,
|
||||
darkBoundary);
|
||||
|
||||
if (crossedLight || crossedDark)
|
||||
{
|
||||
ResetEvent(hManualOverride);
|
||||
Logger::info(L"[LightSwitchService] Manual override cleared after crossing schedule boundary.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Skipping schedule due to manual override");
|
||||
skipRest = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipRest)
|
||||
applyTheme(nowMinutes,
|
||||
currentSettings.lightTime + currentSettings.sunrise_offset,
|
||||
currentSettings.darkTime + currentSettings.sunset_offset,
|
||||
currentSettings);
|
||||
}
|
||||
|
||||
// ─── 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)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop.");
|
||||
break;
|
||||
}
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent process exited - stopping service.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (hManualOverride)
|
||||
CloseHandle(hManualOverride);
|
||||
if (hParent)
|
||||
CloseHandle(hParent);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ApplyThemeNow()
|
||||
{
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& settings = LightSwitchSettings::instance().settings();
|
||||
|
||||
@@ -212,82 +489,43 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
GetLocalTime(&st);
|
||||
int nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
|
||||
Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute);
|
||||
stateManager.SyncInitialThemeState();
|
||||
stateManager.OnTick(nowMinutes);
|
||||
bool shouldBeLight = false;
|
||||
if (settings.lightTime < settings.darkTime)
|
||||
shouldBeLight = (nowMinutes >= settings.lightTime && nowMinutes < settings.darkTime);
|
||||
else
|
||||
shouldBeLight = (nowMinutes >= settings.lightTime || nowMinutes < settings.darkTime);
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Worker Loop
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
for (;;)
|
||||
bool isSystemCurrentlyLight = GetCurrentSystemTheme();
|
||||
bool isAppsCurrentlyLight = GetCurrentAppsTheme();
|
||||
|
||||
Logger::info(L"[LightSwitchService] Applying (if needed) theme immediately due to schedule change.");
|
||||
|
||||
if (shouldBeLight)
|
||||
{
|
||||
HANDLE waits[4];
|
||||
DWORD count = 0;
|
||||
waits[count++] = g_ServiceStopEvent;
|
||||
if (hParent)
|
||||
waits[count++] = hParent;
|
||||
if (hManualOverride)
|
||||
waits[count++] = hManualOverride;
|
||||
waits[count++] = hSettingsChanged;
|
||||
|
||||
// Wait for one of these to trigger or for a new minute tick
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds;
|
||||
if (msToNextMinute < 50)
|
||||
msToNextMinute = 50;
|
||||
|
||||
DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute);
|
||||
|
||||
if (wait == WAIT_TIMEOUT)
|
||||
if (settings.changeSystem && !isSystemCurrentlyLight)
|
||||
{
|
||||
// regular minute tick
|
||||
GetLocalTime(&st);
|
||||
nowMinutes = st.wHour * 60 + st.wMinute;
|
||||
DetectAndHandleExternalThemeChange(stateManager);
|
||||
stateManager.OnTick(nowMinutes);
|
||||
continue;
|
||||
SetSystemTheme(true);
|
||||
Logger::info(L"[LightSwitchService] Changing system theme to light mode.");
|
||||
}
|
||||
|
||||
if (wait == WAIT_OBJECT_0)
|
||||
if (settings.changeApps && !isAppsCurrentlyLight)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Stop event triggered — exiting.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (hParent && wait == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Parent process exited — stopping service.");
|
||||
break;
|
||||
}
|
||||
|
||||
if (hManualOverride && wait == WAIT_OBJECT_0 + (hParent ? 2 : 1))
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Manual override event detected.");
|
||||
stateManager.OnManualOverride();
|
||||
ResetEvent(hManualOverride);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2))
|
||||
{
|
||||
ResetEvent(hSettingsChanged);
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
stateManager.OnSettingsChanged();
|
||||
continue;
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Cleanup
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
if (hManualOverride)
|
||||
CloseHandle(hManualOverride);
|
||||
if (hParent)
|
||||
CloseHandle(hParent);
|
||||
|
||||
Logger::info(L"[LightSwitchService] Worker thread exiting cleanly.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
|
||||
|
||||
@@ -74,8 +74,8 @@
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="LightSwitchService.cpp" />
|
||||
<ClCompile Include="LightSwitchServiceObserver.cpp" />
|
||||
<ClCompile Include="LightSwitchSettings.cpp" />
|
||||
<ClCompile Include="LightSwitchStateManager.cpp" />
|
||||
<ClCompile Include="SettingsConstants.cpp" />
|
||||
<ClCompile Include="ThemeHelper.cpp" />
|
||||
<ClCompile Include="ThemeScheduler.cpp" />
|
||||
@@ -85,9 +85,8 @@
|
||||
<ResourceCompile Include="LightSwitchService.rc" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="LightSwitchServiceObserver.h" />
|
||||
<ClInclude Include="LightSwitchSettings.h" />
|
||||
<ClInclude Include="LightSwitchStateManager.h" />
|
||||
<ClInclude Include="LightSwitchUtils.h" />
|
||||
<ClInclude Include="SettingsConstants.h" />
|
||||
<ClInclude Include="SettingsObserver.h" />
|
||||
<ClInclude Include="ThemeHelper.h" />
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<ClCompile Include="WinHookEventIDs.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="LightSwitchStateManager.cpp">
|
||||
<ClCompile Include="LightSwitchServiceObserver.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
@@ -56,10 +56,7 @@
|
||||
<ClInclude Include="WinHookEventIDs.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LightSwitchStateManager.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="LightSwitchUtils.h">
|
||||
<ClInclude Include="LightSwitchServiceObserver.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#include "LightSwitchServiceObserver.h"
|
||||
#include <logger.h>
|
||||
#include "LightSwitchSettings.h"
|
||||
|
||||
// These are defined elsewhere in your service module (ServiceWorkerThread.cpp)
|
||||
extern int g_lastUpdatedDay;
|
||||
void ApplyThemeNow();
|
||||
|
||||
void LightSwitchServiceObserver::SettingsUpdate(SettingId id)
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Setting changed: {}", static_cast<int>(id));
|
||||
g_lastUpdatedDay = -1;
|
||||
ApplyThemeNow();
|
||||
}
|
||||
|
||||
bool LightSwitchServiceObserver::WantsToBeNotified(SettingId id) const noexcept
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case SettingId::LightTime:
|
||||
case SettingId::DarkTime:
|
||||
case SettingId::ScheduleMode:
|
||||
case SettingId::Sunrise_Offset:
|
||||
case SettingId::Sunset_Offset:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "SettingsObserver.h"
|
||||
|
||||
// The LightSwitchServiceObserver reacts when LightSwitchSettings changes.
|
||||
class LightSwitchServiceObserver : public SettingsObserver
|
||||
{
|
||||
public:
|
||||
explicit LightSwitchServiceObserver(std::unordered_set<SettingId> observedSettings) :
|
||||
SettingsObserver(std::move(observedSettings))
|
||||
{
|
||||
}
|
||||
|
||||
void SettingsUpdate(SettingId id) override;
|
||||
bool WantsToBeNotified(SettingId id) const noexcept override;
|
||||
};
|
||||
@@ -2,8 +2,10 @@
|
||||
#include <common/utils/json.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
#include "SettingsObserver.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <WinHookEventIDs.h>
|
||||
#include <logger.h>
|
||||
|
||||
using namespace std;
|
||||
@@ -36,79 +38,13 @@ void LightSwitchSettings::InitFileWatcher()
|
||||
m_settingsFileWatcher = std::make_unique<FileWatcher>(
|
||||
GetSettingsFileName(),
|
||||
[this]() {
|
||||
using namespace std::chrono;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> lock(m_debounceMutex);
|
||||
m_debouncePending = false;
|
||||
}
|
||||
|
||||
Logger::info(L"[LightSwitchSettings] Settings file stabilized, reloading.");
|
||||
|
||||
try
|
||||
{
|
||||
LoadSettings();
|
||||
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);
|
||||
}
|
||||
});
|
||||
Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event.");
|
||||
LoadSettings();
|
||||
SetEvent(m_settingsChangedEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -137,7 +73,6 @@ HANDLE LightSwitchSettings::GetSettingsChangedEvent() const
|
||||
|
||||
void LightSwitchSettings::LoadSettings()
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m_settingsMutex);
|
||||
try
|
||||
{
|
||||
PowerToysSettings::PowerToyValues values =
|
||||
@@ -246,4 +181,4 @@ void LightSwitchSettings::LoadSettings()
|
||||
{
|
||||
// Keeps defaults if load fails
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,7 @@
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <windows.h>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include <common/SettingsAPI/FileWatcher.h>
|
||||
#include <common/SettingsAPI/settings_objects.h>
|
||||
#include <SettingsConstants.h>
|
||||
@@ -86,7 +83,7 @@ public:
|
||||
|
||||
private:
|
||||
LightSwitchSettings();
|
||||
~LightSwitchSettings();
|
||||
~LightSwitchSettings() = default;
|
||||
|
||||
LightSwitchConfig m_settings;
|
||||
std::unique_ptr<FileWatcher> m_settingsFileWatcher;
|
||||
@@ -95,11 +92,4 @@ 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;
|
||||
};
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
#include "pch.h"
|
||||
#include "LightSwitchStateManager.h"
|
||||
#include <logger.h>
|
||||
#include <LightSwitchUtils.h>
|
||||
#include "ThemeScheduler.h"
|
||||
#include <ThemeHelper.h>
|
||||
|
||||
void ApplyTheme(bool shouldBeLight);
|
||||
|
||||
// Constructor
|
||||
LightSwitchStateManager::LightSwitchStateManager()
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Initialized");
|
||||
}
|
||||
|
||||
// Called when settings.json changes
|
||||
void LightSwitchStateManager::OnSettingsChanged()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
|
||||
// If manual override was active, clear it so new settings take effect
|
||||
if (_state.isManualOverride)
|
||||
{
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Called once per minute
|
||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Called when manual override is triggered
|
||||
void LightSwitchStateManager::OnManualOverride()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
Logger::info(L"[LightSwitchStateManager] Manual override triggered");
|
||||
_state.isManualOverride = !_state.isManualOverride;
|
||||
|
||||
// When entering manual override, sync internal theme state to match the current system
|
||||
if (_state.isManualOverride)
|
||||
{
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
}
|
||||
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
// Helpers
|
||||
bool LightSwitchStateManager::CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon)
|
||||
{
|
||||
try
|
||||
{
|
||||
double latVal = std::stod(lat);
|
||||
double lonVal = std::stod(lon);
|
||||
return !(latVal == 0 && lonVal == 0) && (latVal >= -90.0 && latVal <= 90.0) && (lonVal >= -180.0 && lonVal <= 180.0);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void LightSwitchStateManager::SyncInitialThemeState()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
}
|
||||
|
||||
static std::pair<int, int> update_sun_times(auto& settings)
|
||||
{
|
||||
double latitude = std::stod(settings.latitude);
|
||||
double longitude = std::stod(settings.longitude);
|
||||
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
|
||||
SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay);
|
||||
|
||||
int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute;
|
||||
int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute;
|
||||
|
||||
try
|
||||
{
|
||||
auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch");
|
||||
values.add_property(L"lightTime", newLightTime);
|
||||
values.add_property(L"darkTime", newDarkTime);
|
||||
values.save_to_settings_file();
|
||||
|
||||
Logger::info(L"[LightSwitchService] Updated sun times and saved to config.");
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
std::string msg = e.what();
|
||||
std::wstring wmsg(msg.begin(), msg.end());
|
||||
Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg);
|
||||
}
|
||||
|
||||
return { newLightTime, newDarkTime };
|
||||
}
|
||||
|
||||
// Internal: decide what should happen now
|
||||
void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
{
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
const auto& _currentSettings = LightSwitchSettings::settings();
|
||||
auto now = GetNowMinutes();
|
||||
|
||||
// Early exit: OFF mode just pauses activity
|
||||
if (_currentSettings.scheduleMode == ScheduleMode::Off)
|
||||
{
|
||||
_state.lastTickMinutes = now;
|
||||
return;
|
||||
}
|
||||
|
||||
bool coordsValid = CoordinatesAreValid(_currentSettings.latitude, _currentSettings.longitude);
|
||||
|
||||
// Handle Sun Mode recalculation
|
||||
if (_currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise && coordsValid)
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
bool newDay = (_state.lastEvaluatedDay != st.wDay);
|
||||
bool modeChangedToSun = (_state.lastAppliedMode != ScheduleMode::SunsetToSunrise &&
|
||||
_currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise);
|
||||
|
||||
if (newDay || modeChangedToSun)
|
||||
{
|
||||
auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings);
|
||||
_state.lastEvaluatedDay = st.wDay;
|
||||
_state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset;
|
||||
_state.effectiveDarkMinutes = newDarkTime + _currentSettings.sunset_offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.effectiveLightMinutes = _currentSettings.lightTime + _currentSettings.sunrise_offset;
|
||||
_state.effectiveDarkMinutes = _currentSettings.darkTime + _currentSettings.sunset_offset;
|
||||
}
|
||||
}
|
||||
else if (_currentSettings.scheduleMode == ScheduleMode::FixedHours)
|
||||
{
|
||||
_state.effectiveLightMinutes = _currentSettings.lightTime;
|
||||
_state.effectiveDarkMinutes = _currentSettings.darkTime;
|
||||
}
|
||||
|
||||
// Handle manual override logic
|
||||
if (_state.isManualOverride)
|
||||
{
|
||||
bool crossedBoundary = false;
|
||||
if (_state.lastTickMinutes != -1)
|
||||
{
|
||||
int prev = _state.lastTickMinutes;
|
||||
|
||||
// Handle midnight wraparound safely
|
||||
if (now < prev)
|
||||
{
|
||||
crossedBoundary =
|
||||
(prev <= _state.effectiveLightMinutes || now >= _state.effectiveLightMinutes) ||
|
||||
(prev <= _state.effectiveDarkMinutes || now >= _state.effectiveDarkMinutes);
|
||||
}
|
||||
else
|
||||
{
|
||||
crossedBoundary =
|
||||
(prev < _state.effectiveLightMinutes && now >= _state.effectiveLightMinutes) ||
|
||||
(prev < _state.effectiveDarkMinutes && now >= _state.effectiveDarkMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
if (crossedBoundary)
|
||||
{
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.lastTickMinutes = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_state.lastAppliedMode = _currentSettings.scheduleMode;
|
||||
|
||||
bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes);
|
||||
|
||||
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
|
||||
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
|
||||
|
||||
/* Logger::debug(
|
||||
L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})",
|
||||
now / 60,
|
||||
now % 60,
|
||||
_state.effectiveLightMinutes / 60,
|
||||
_state.effectiveLightMinutes % 60,
|
||||
_state.effectiveLightMinutes,
|
||||
_state.effectiveDarkMinutes / 60,
|
||||
_state.effectiveDarkMinutes % 60,
|
||||
_state.effectiveDarkMinutes); */
|
||||
|
||||
/* Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
|
||||
shouldBeLight ? "true" : "false",
|
||||
appsNeedsToChange ? "true" : "false",
|
||||
systemNeedsToChange ? "true" : "false"); */
|
||||
|
||||
// Only apply theme if there's a change or no override active
|
||||
if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange))
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark");
|
||||
ApplyTheme(shouldBeLight);
|
||||
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
}
|
||||
|
||||
_state.lastTickMinutes = now;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
#pragma once
|
||||
#include "LightSwitchSettings.h"
|
||||
#include <optional>
|
||||
|
||||
// Represents runtime-only information (not saved in settings.json)
|
||||
struct LightSwitchState
|
||||
{
|
||||
ScheduleMode lastAppliedMode = ScheduleMode::Off;
|
||||
bool isManualOverride = false;
|
||||
bool isSystemLightActive = false;
|
||||
bool isAppsLightActive = false;
|
||||
int lastEvaluatedDay = -1;
|
||||
int lastTickMinutes = -1;
|
||||
|
||||
// Derived, runtime-resolved times
|
||||
int effectiveLightMinutes = 0; // the boundary we actually act on
|
||||
int effectiveDarkMinutes = 0; // includes offsets if needed
|
||||
};
|
||||
|
||||
// The controller that reacts to settings changes, time ticks, and manual overrides.
|
||||
class LightSwitchStateManager
|
||||
{
|
||||
public:
|
||||
LightSwitchStateManager();
|
||||
|
||||
// Called when settings.json changes or stabilizes.
|
||||
void OnSettingsChanged();
|
||||
|
||||
// Called every minute (from service worker tick).
|
||||
void OnTick(int currentMinutes);
|
||||
|
||||
// Called when manual override is toggled (via shortcut or system change).
|
||||
void OnManualOverride();
|
||||
|
||||
// Initial sync at startup to align internal state with system theme
|
||||
void SyncInitialThemeState();
|
||||
|
||||
// Accessor for current state (optional, for debugging or telemetry)
|
||||
const LightSwitchState& GetState() const { return _state; }
|
||||
|
||||
private:
|
||||
LightSwitchState _state;
|
||||
std::mutex _stateMutex;
|
||||
|
||||
void EvaluateAndApplyIfNeeded();
|
||||
bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
|
||||
constexpr bool ShouldBeLight(int nowMinutes, int lightTime, int darkTime)
|
||||
{
|
||||
// Normalize values into [0, 1439]
|
||||
int normalizedLightTime = (lightTime % 1440 + 1440) % 1440;
|
||||
int normalizedDarkTime = (darkTime % 1440 + 1440) % 1440;
|
||||
int normalizedNowMinutes = (nowMinutes % 1440 + 1440) % 1440;
|
||||
|
||||
// Case 1: Normal range, e.g. light mode comes before dark mode in the same day
|
||||
if (normalizedLightTime < normalizedDarkTime)
|
||||
return normalizedNowMinutes >= normalizedLightTime && normalizedNowMinutes < normalizedDarkTime;
|
||||
|
||||
// Case 2: Wrap-around range, e.g. light mode starts in the evening and dark mode starts in the morning
|
||||
return normalizedNowMinutes >= normalizedLightTime || normalizedNowMinutes < normalizedDarkTime;
|
||||
}
|
||||
|
||||
inline int GetNowMinutes()
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
return st.wHour * 60 + st.wMinute;
|
||||
}
|
||||
@@ -152,16 +152,16 @@ namespace LightSwitch.UITests
|
||||
|
||||
var neededTabs = 6;
|
||||
|
||||
if (modeCombobox.Text != "Fixed hours")
|
||||
if (modeCombobox.Text != "Manual")
|
||||
{
|
||||
modeCombobox.Click();
|
||||
var manualListItem = testBase.Session.Find<Element>(By.AccessibilityId("ManualCBItem_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(manualListItem, "Fixed Hours combobox item not found.");
|
||||
Assert.IsNotNull(manualListItem, "Manual combobox item not found.");
|
||||
manualListItem.Click();
|
||||
neededTabs = 1;
|
||||
}
|
||||
|
||||
Assert.AreEqual("Fixed hours", modeCombobox.Text, "Mode combobox should be set to Fixed hours.");
|
||||
Assert.AreEqual("Manual", modeCombobox.Text, "Mode combobox should be set to Manual.");
|
||||
|
||||
var timeline = testBase.Session.Find<Element>(By.AccessibilityId("Timeline_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(timeline, "Timeline not found.");
|
||||
@@ -198,7 +198,7 @@ namespace LightSwitch.UITests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a update manual location test operation
|
||||
/// Perform a update geolocation test operation
|
||||
/// </summary>
|
||||
public static void PerformUserSelectedLocationTest(UITestBase testBase)
|
||||
{
|
||||
@@ -216,22 +216,19 @@ namespace LightSwitch.UITests
|
||||
|
||||
Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
|
||||
|
||||
// Click the select location button
|
||||
var setLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(setLocationButton, "Set location button not found.");
|
||||
setLocationButton.Click(msPostAction: 1000);
|
||||
setLocationButton.Click();
|
||||
|
||||
var latitudeBox = testBase.Session.Find<Element>(By.AccessibilityId("LatitudeBox_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(latitudeBox, "Latitude text box not found.");
|
||||
latitudeBox.Click();
|
||||
var autoSuggestTextbox = testBase.Session.Find<Element>(By.AccessibilityId("CitySearchBox_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(autoSuggestTextbox, "City search box not found.");
|
||||
autoSuggestTextbox.Click();
|
||||
autoSuggestTextbox.SendKeys("Seattle");
|
||||
autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Down);
|
||||
autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Enter);
|
||||
|
||||
testBase.Session.SendKeys(Key.Up);
|
||||
|
||||
var longitudeBox = testBase.Session.Find<Element>(By.AccessibilityId("LongitudeBox_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(longitudeBox, "Longitude text box not found.");
|
||||
longitudeBox.Click();
|
||||
|
||||
testBase.Session.SendKeys(Key.Down);
|
||||
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
|
||||
|
||||
var sunrise = testBase.Session.Find<Element>(By.AccessibilityId("SunriseText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text));
|
||||
@@ -259,14 +256,13 @@ namespace LightSwitch.UITests
|
||||
|
||||
Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise.");
|
||||
|
||||
// Click the select location button
|
||||
// Click the select city button
|
||||
var setLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(setLocationButton, "Set location button not found.");
|
||||
setLocationButton.Click(msPostAction: 1000);
|
||||
setLocationButton.Click(msPostAction: 8000);
|
||||
|
||||
var syncLocationButton = testBase.Session.Find<Element>(By.AccessibilityId("SyncLocationButton_LightSwitch"), 5000);
|
||||
Assert.IsNotNull(syncLocationButton, "Sync location button not found.");
|
||||
syncLocationButton.Click(msPostAction: 8000);
|
||||
var latLong = testBase.Session.Find<Element>(By.AccessibilityId("LocationResultText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text));
|
||||
|
||||
var sunrise = testBase.Session.Find<Element>(By.AccessibilityId("SunriseText_LightSwitch"), 5000);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text));
|
||||
@@ -367,7 +363,6 @@ namespace LightSwitch.UITests
|
||||
var systemBeforeValue = GetSystemTheme();
|
||||
var appsBeforeValue = GetAppsTheme();
|
||||
|
||||
Task.Delay(1000).Wait();
|
||||
testBase.Session.SendKeys(activationKeys);
|
||||
Task.Delay(5000).Wait();
|
||||
|
||||
@@ -394,7 +389,6 @@ namespace LightSwitch.UITests
|
||||
var noneSystemBeforeValue = GetSystemTheme();
|
||||
var noneAppsBeforeValue = GetAppsTheme();
|
||||
|
||||
Task.Delay(1000).Wait();
|
||||
testBase.Session.SendKeys(activationKeys);
|
||||
Task.Delay(5000).Wait();
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
#include <windows.h>
|
||||
#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
|
||||