Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d11b732b7 | ||
|
|
ab4792a4b8 | ||
|
|
dc3a9da968 | ||
|
|
baee472288 | ||
|
|
142833d49a | ||
|
|
46d594a7f5 | ||
|
|
673a41512c | ||
|
|
f57062c206 | ||
|
|
f339b48324 | ||
|
|
f590871d6d | ||
|
|
bdcedb142e | ||
|
|
7da85cac40 | ||
|
|
750ef385b8 | ||
|
|
7279bfd9ec | ||
|
|
8d6bb43c26 | ||
|
|
b82e3535da | ||
|
|
d81186267d | ||
|
|
d22589bcf6 | ||
|
|
0aed70cc87 | ||
|
|
ba70becdef | ||
|
|
820d98cb43 | ||
|
|
cbeeefcaf9 | ||
|
|
6ceb53336f | ||
|
|
5812b55710 | ||
|
|
1be856c573 | ||
|
|
c356d65e0d | ||
|
|
2dd1d457eb | ||
|
|
bb276d3be9 | ||
|
|
b7aca7c3fc | ||
|
|
8e3a88df2c | ||
|
|
fb028e8fdc | ||
|
|
d980697849 | ||
|
|
d42b15f380 | ||
|
|
b49f722e88 | ||
|
|
c2ea654b3c | ||
|
|
80e0a4d09e | ||
|
|
8c93854bbb | ||
|
|
1aba1e170c | ||
|
|
c5a18aa488 | ||
|
|
df019c09e6 | ||
|
|
d97301f06f | ||
|
|
86ca7c5661 | ||
|
|
e37d1d5d04 | ||
|
|
ac6aa80401 | ||
|
|
c4a83be733 | ||
|
|
34d01e998c | ||
|
|
b1adc8548f | ||
|
|
0e71f616de | ||
|
|
4f0f614f67 | ||
|
|
ddf9815613 | ||
|
|
184ccb75ec | ||
|
|
455dc52430 | ||
|
|
ed1d15f9a1 | ||
|
|
31da4fa112 | ||
|
|
3ee7518850 | ||
|
|
1bfa42b270 | ||
|
|
e0979fce59 | ||
|
|
b2684f74f5 | ||
|
|
b67e11d000 | ||
|
|
a5bc91028f | ||
|
|
1eebd47af0 | ||
|
|
1f840000f5 | ||
|
|
04fe688e7a | ||
|
|
8fc31de489 | ||
|
|
0a510119c8 | ||
|
|
38dfc55a2d | ||
|
|
89b2dfe9ac | ||
|
|
2ef65e7d63 | ||
|
|
3dc5b3a3ed | ||
|
|
fc5b65c5c3 | ||
|
|
da5448b169 | ||
|
|
1ab685ee07 | ||
|
|
cf137ccbbc | ||
|
|
2cc051ee33 | ||
|
|
efc89b01ff | ||
|
|
9717eaac4c | ||
|
|
b5716d0499 | ||
|
|
a4d23b7607 | ||
|
|
5409b1b907 | ||
|
|
fbe952715c | ||
|
|
034759f949 | ||
|
|
934c3bbce9 | ||
|
|
ac548297c9 | ||
|
|
17a215d321 | ||
|
|
1b6a8c54ff | ||
|
|
67518dd754 | ||
|
|
18d1fd568c | ||
|
|
3eae35f356 | ||
|
|
e349779766 | ||
|
|
5466ab6cf8 | ||
|
|
bf19bdc1ee | ||
|
|
2441621b80 | ||
|
|
1ca9d10ff5 | ||
|
|
b438f15f6e | ||
|
|
48de981f50 | ||
|
|
9ab6559fac | ||
|
|
8cc32d3098 | ||
|
|
3b7eedfb67 | ||
|
|
d7e1b18ba4 | ||
|
|
0206fdbec1 | ||
|
|
bdaf644f02 | ||
|
|
329c8c2616 | ||
|
|
b081e413b1 | ||
|
|
290fa01adf |
7
.github/actions/spell-check/allow/code.txt
vendored
@@ -17,8 +17,8 @@ LIGHTTURQUOISE
|
||||
NCol
|
||||
OLIVEGREEN
|
||||
PALEBLUE
|
||||
pargb
|
||||
pbgra
|
||||
PArgb
|
||||
Pbgra
|
||||
SRGBTo
|
||||
WHITEONBLACK
|
||||
|
||||
@@ -432,6 +432,3 @@ SHELLEXPERIENCEHOST
|
||||
SHELLHOST
|
||||
STARTMENUEXPERIENCEHOST
|
||||
WIDGETBOARD
|
||||
|
||||
# URIs
|
||||
actioncenter
|
||||
|
||||
212
.github/actions/spell-check/allow/zoomit.txt
vendored
@@ -1,51 +1,23 @@
|
||||
accelscroll
|
||||
acq
|
||||
ADDTO
|
||||
ADDTOOL
|
||||
adr
|
||||
Adr
|
||||
ALWAYSTIP
|
||||
APPLYTOSUBMENUS
|
||||
ARCHMASK
|
||||
archs
|
||||
AUDCLNT
|
||||
autocorr
|
||||
avx
|
||||
axisdefer
|
||||
axisflip
|
||||
axisstart
|
||||
backlight
|
||||
BEOS
|
||||
bfi
|
||||
BFIN
|
||||
bfly
|
||||
BGRX
|
||||
bitmaps
|
||||
bitrev
|
||||
blits
|
||||
Borgerding
|
||||
Borland
|
||||
breakc
|
||||
BREAKSCR
|
||||
BUFFERFLAGS
|
||||
bugzilla
|
||||
Cands
|
||||
capturepath
|
||||
cbs
|
||||
centiseconds
|
||||
cexp
|
||||
cfx
|
||||
cfy
|
||||
cgem
|
||||
cifx
|
||||
cify
|
||||
CLASSW
|
||||
coeffs
|
||||
colblocks
|
||||
constantbuffer
|
||||
coprime
|
||||
cpuid
|
||||
cpx
|
||||
CREATEDIBSECTION
|
||||
CREATESTRUCTW
|
||||
crossfades
|
||||
@@ -56,216 +28,109 @@ CTLCOLORDLG
|
||||
CTLCOLOREDIT
|
||||
CTLCOLORLISTBOX
|
||||
CTrim
|
||||
CVTEPI
|
||||
DBuffer
|
||||
dcl
|
||||
dct
|
||||
ddx
|
||||
ddy
|
||||
Deinterleave
|
||||
denoise
|
||||
denoised
|
||||
DEVSOURCE
|
||||
DFCS
|
||||
DIVSCALAR
|
||||
DJGPP
|
||||
dlg
|
||||
dlu
|
||||
dnn
|
||||
DONTCARE
|
||||
downsample
|
||||
DRAWITEM
|
||||
DRAWITEMSTRUCT
|
||||
droppedband
|
||||
Droppedband
|
||||
DSPs
|
||||
dsum
|
||||
dupburst
|
||||
dupsegments
|
||||
DWLP
|
||||
eband
|
||||
ebx
|
||||
ECX
|
||||
EDITCONTROL
|
||||
EDSP
|
||||
emmintrin
|
||||
EMX
|
||||
ENABLEHOOK
|
||||
endloop
|
||||
ENDOFSTREAM
|
||||
ener
|
||||
enh
|
||||
ettings
|
||||
expectedlock
|
||||
expf
|
||||
fabs
|
||||
fabsf
|
||||
facbuf
|
||||
fastscroll
|
||||
FDE
|
||||
ffast
|
||||
FIXDIV
|
||||
floorf
|
||||
fmadd
|
||||
fout
|
||||
fstride
|
||||
fxc
|
||||
GETCHANNELRECT
|
||||
GETCHECK
|
||||
GETCOUNT
|
||||
GETDISPINFO
|
||||
GETSCREENSAVEACTIVE
|
||||
GETSCREENSAVETIMEOUT
|
||||
GETTHUMBRECT
|
||||
GIFs
|
||||
glu
|
||||
groupshared
|
||||
gru
|
||||
hcfdark
|
||||
hcfwhitespace
|
||||
hlsl
|
||||
Hsieh
|
||||
hstride
|
||||
HTBOTTOMRIGHT
|
||||
HTHEME
|
||||
htol
|
||||
ICONINFORMATION
|
||||
ICONWARNING
|
||||
idct
|
||||
IDIn
|
||||
IDISHWND
|
||||
ifft
|
||||
igc
|
||||
ilog
|
||||
imad
|
||||
imax
|
||||
imin
|
||||
immintrin
|
||||
Inj
|
||||
interp
|
||||
inttypes
|
||||
ishl
|
||||
itof
|
||||
jumprecover
|
||||
kfft
|
||||
kheight
|
||||
kissfft
|
||||
KSDATAFORMAT
|
||||
ksize
|
||||
ktime
|
||||
lastg
|
||||
latestcapture
|
||||
ldx
|
||||
LEFTNOWORDWRAP
|
||||
legitjumps
|
||||
lenmem
|
||||
letterbox
|
||||
lld
|
||||
lldx
|
||||
llu
|
||||
llums
|
||||
logfont
|
||||
lookback
|
||||
lpc
|
||||
lpcnet
|
||||
LPNMHDR
|
||||
LPNMTTDISPINFO
|
||||
lround
|
||||
lte
|
||||
luma
|
||||
Luma
|
||||
maj
|
||||
manualdrop
|
||||
maskcache
|
||||
maxabs
|
||||
maxcorr
|
||||
MAXFACTORS
|
||||
maxperiod
|
||||
maxstep
|
||||
memalign
|
||||
memid
|
||||
memneeded
|
||||
MENUINFO
|
||||
MFSTARTUP
|
||||
mfxhw
|
||||
mic
|
||||
middledrop
|
||||
minperiod
|
||||
MIPSr
|
||||
MJPEG
|
||||
MMRESULT
|
||||
momentumreversal
|
||||
movc
|
||||
mrate
|
||||
mrt
|
||||
MULBYSCALAR
|
||||
MULC
|
||||
MWERKS
|
||||
mycfg
|
||||
narrowstrip
|
||||
nbak
|
||||
nbytes
|
||||
ncapture
|
||||
nchw
|
||||
ncm
|
||||
nduplicates
|
||||
nfft
|
||||
NHWC
|
||||
niterations
|
||||
nmonitor
|
||||
nnet
|
||||
NONCLIENTMETRICS
|
||||
NONOTIFY
|
||||
nonvle
|
||||
normf
|
||||
nredraw
|
||||
nstop
|
||||
nsubpixel
|
||||
ntorn
|
||||
numthreads
|
||||
nvw
|
||||
Octasic
|
||||
osc
|
||||
OSCE
|
||||
ovflw
|
||||
OWNERDRAW
|
||||
PBGRA
|
||||
periodictrap
|
||||
pfdc
|
||||
pillarbox
|
||||
pfdc
|
||||
playhead
|
||||
pnmh
|
||||
pointerreuse
|
||||
PPW
|
||||
prereq
|
||||
PSHR
|
||||
pstdint
|
||||
PSWA
|
||||
pwfx
|
||||
QCONST
|
||||
qpc
|
||||
Qpc
|
||||
quantums
|
||||
qweight
|
||||
RCSEGMODEL
|
||||
RCZOOMITSCR
|
||||
readback
|
||||
READERF
|
||||
realcapture
|
||||
REFKNOWNFOLDERID
|
||||
relu
|
||||
reposted
|
||||
RETURNCMD
|
||||
rnn
|
||||
rnnoise
|
||||
rotateleft
|
||||
rsqrt
|
||||
rtcd
|
||||
RTEXT
|
||||
RTH
|
||||
rtvs
|
||||
SCALEIN
|
||||
SCALEOUT
|
||||
SCREENSAVE
|
||||
SCRNSAVE
|
||||
SCRNSAVECONFIGURE
|
||||
@@ -273,80 +138,43 @@ scrnsavw
|
||||
Scrnsavw
|
||||
scrollramp
|
||||
SCROLLSIZEGRIP
|
||||
selfie
|
||||
selftest
|
||||
SETBARCOLOR
|
||||
SETBKCOLOR
|
||||
SETDEFID
|
||||
SETRECT
|
||||
SETSCREENSAVETIMEOUT
|
||||
SETTIPSIDE
|
||||
sgem
|
||||
sgemv
|
||||
sgv
|
||||
SHAREMODE
|
||||
SHAREVIOLATION
|
||||
shortlist
|
||||
simde
|
||||
siv
|
||||
slowthenfast
|
||||
smallstart
|
||||
SNIPOCR
|
||||
softmax
|
||||
sqrtf
|
||||
SROUND
|
||||
srvs
|
||||
ssi
|
||||
startuprecovery
|
||||
stdint
|
||||
stf
|
||||
stopafter
|
||||
STREAMFLAGS
|
||||
SUBFROM
|
||||
subias
|
||||
submix
|
||||
sxx
|
||||
sxy
|
||||
symbian
|
||||
synthesising
|
||||
syy
|
||||
tallportal
|
||||
TBTS
|
||||
tci
|
||||
tcsicmp
|
||||
TEXTCALLBACK
|
||||
TEXTMETRIC
|
||||
tgsm
|
||||
THIRDPARTY
|
||||
tinystep
|
||||
tme
|
||||
toolbars
|
||||
TOOLINFO
|
||||
TRACKMOUSEEVENT
|
||||
TRIANGLELIST
|
||||
TTM
|
||||
TTN
|
||||
TWID
|
||||
UADD
|
||||
uav
|
||||
uavs
|
||||
uge
|
||||
Unadvise
|
||||
upscaled
|
||||
upscales
|
||||
USUB
|
||||
utof
|
||||
vad
|
||||
vaddq
|
||||
vaddvq
|
||||
valgrind
|
||||
Valin
|
||||
vandq
|
||||
vblank
|
||||
vcgeq
|
||||
vdup
|
||||
vectorizer
|
||||
VERTID
|
||||
VIDCAP
|
||||
vld
|
||||
vle
|
||||
@@ -356,7 +184,6 @@ vminq
|
||||
vmlal
|
||||
vmull
|
||||
vqaddq
|
||||
VSHR
|
||||
vshrn
|
||||
vsntprintf
|
||||
vsnwprintf
|
||||
@@ -367,9 +194,7 @@ WAVEFORMATEXTENSIBLE
|
||||
webcam
|
||||
Webcam
|
||||
webcams
|
||||
Wextra
|
||||
wfopen
|
||||
WGC
|
||||
wideportal
|
||||
wil
|
||||
WMU
|
||||
@@ -377,46 +202,11 @@ wrapjump
|
||||
wtol
|
||||
WTSSESSION
|
||||
WTSUn
|
||||
wxyz
|
||||
xchg
|
||||
xcorr
|
||||
XEnd
|
||||
Xfl
|
||||
Xiang
|
||||
Xiph
|
||||
xmmintrin
|
||||
xptr
|
||||
xshift
|
||||
XStart
|
||||
XStep
|
||||
xxxy
|
||||
xxyx
|
||||
xxyz
|
||||
xyw
|
||||
xywx
|
||||
xyxx
|
||||
xyxz
|
||||
xyzw
|
||||
xyzx
|
||||
xzwx
|
||||
xzxx
|
||||
Yfl
|
||||
YInternal
|
||||
yshift
|
||||
YUV
|
||||
yyyx
|
||||
yyzw
|
||||
yzw
|
||||
yzwy
|
||||
yzyy
|
||||
Zhou
|
||||
Zhu
|
||||
ZMBS
|
||||
zncc
|
||||
Zncc
|
||||
ZNCC
|
||||
zrh
|
||||
zwzz
|
||||
zyzw
|
||||
zzwz
|
||||
zzzw
|
||||
|
||||
4
.github/actions/spell-check/excludes.txt
vendored
@@ -112,6 +112,8 @@
|
||||
^src/modules/cmdpal/Microsoft\.CmdPal\.UI/Settings/InternalPage\.SampleData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/.*\.TestData\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CmdPal\.Common\.UnitTests/Text/.*\.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
|
||||
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
|
||||
^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$
|
||||
^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/
|
||||
^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$
|
||||
@@ -135,8 +137,6 @@
|
||||
^src/modules/previewpane/SvgPreviewHandler/SvgHTMLPreviewGenerator\.cs$
|
||||
^src/modules/previewpane/UnitTests-MarkdownPreviewHandler/HelperFiles/MarkdownWithHTMLImageTag\.txt$
|
||||
^src/modules/registrypreview/RegistryPreviewUILib/Controls/HexBox/.*$
|
||||
^src/modules/ZoomIt/ZoomIt/rnnoise/
|
||||
^src/modules/ZoomIt/ZoomIt/selfie_segmentation\.onnx$
|
||||
^src/modules/ZoomIt/ZoomIt/ZoomIt\.idc$
|
||||
^src/Monaco/
|
||||
^tools/project_template/ModuleTemplate/resource\.h$
|
||||
|
||||
27
.github/actions/spell-check/expect.txt
vendored
@@ -24,7 +24,7 @@ advancedpaste
|
||||
advapi
|
||||
advfirewall
|
||||
AFeature
|
||||
affordance
|
||||
affordances
|
||||
afterfx
|
||||
AFX
|
||||
agentskills
|
||||
@@ -113,6 +113,7 @@ azman
|
||||
azureaiinference
|
||||
azureinference
|
||||
azureopenai
|
||||
Backlight
|
||||
backticks
|
||||
Badflags
|
||||
Badmode
|
||||
@@ -135,7 +136,6 @@ BITMAPINFO
|
||||
BITMAPINFOHEADER
|
||||
BITSPERPEL
|
||||
BITSPIXEL
|
||||
Blackmagic
|
||||
bla
|
||||
BLENDFUNCTION
|
||||
blittable
|
||||
@@ -465,12 +465,13 @@ DWMNCRENDERINGCHANGED
|
||||
Dwmp
|
||||
DWMSENDICONICLIVEPREVIEWBITMAP
|
||||
DWMSENDICONICTHUMBNAIL
|
||||
dwmwa
|
||||
dwmwcp
|
||||
DWMWA
|
||||
DWMWCP
|
||||
DWMWINDOWATTRIBUTE
|
||||
DWMWINDOWMAXIMIZEDCHANGE
|
||||
DWORDLONG
|
||||
dworigin
|
||||
DWRITE
|
||||
dxgi
|
||||
Dxva
|
||||
eab
|
||||
@@ -540,7 +541,6 @@ EXTRINSICPROPERTIES
|
||||
eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
Fairlight
|
||||
FARPROC
|
||||
fdw
|
||||
fdx
|
||||
@@ -623,9 +623,7 @@ GETPROPERTYSTOREFLAGS
|
||||
GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTASKBARPOS
|
||||
GETTEXTLENGTH
|
||||
GETWORKAREA
|
||||
gfx
|
||||
GHND
|
||||
gitmodules
|
||||
@@ -680,7 +678,6 @@ HDEVNOTIFY
|
||||
hdr
|
||||
HDROP
|
||||
hdwwiz
|
||||
Headered
|
||||
Helpline
|
||||
helptext
|
||||
hgdiobj
|
||||
@@ -998,6 +995,7 @@ LTRREADING
|
||||
luid
|
||||
lusrmgr
|
||||
LWA
|
||||
LWIN
|
||||
LZero
|
||||
MAGTRANSFORM
|
||||
makeappx
|
||||
@@ -1205,6 +1203,7 @@ nonclient
|
||||
NONCLIENTMETRICSW
|
||||
NONELEVATED
|
||||
nonspace
|
||||
nonstd
|
||||
NOOWNERZORDER
|
||||
NOPARENTNOTIFY
|
||||
NOPREFIX
|
||||
@@ -1234,8 +1233,6 @@ NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
Notupdated
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
Nouveaut
|
||||
nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
@@ -1366,7 +1363,6 @@ pii
|
||||
pinfo
|
||||
pinvoke
|
||||
pipename
|
||||
Pitjantjatjara
|
||||
PKBDLLHOOKSTRUCT
|
||||
pkgfamily
|
||||
PKI
|
||||
@@ -1422,6 +1418,7 @@ Prefixer
|
||||
Premul
|
||||
prependpath
|
||||
prepopulate
|
||||
Prereq
|
||||
prevhost
|
||||
previewer
|
||||
PREVIEWHANDLERFRAMEINFO
|
||||
@@ -1510,7 +1507,6 @@ RAWPATH
|
||||
rbhid
|
||||
Rbuttondown
|
||||
rclsid
|
||||
RCW
|
||||
RCZOOMIT
|
||||
rdp
|
||||
RDW
|
||||
@@ -1643,7 +1639,6 @@ SETPOWEROFFACTIVE
|
||||
SETRANGE
|
||||
SETREDRAW
|
||||
SETRULES
|
||||
SETAUTOHIDEBAREX
|
||||
SETSCREENSAVEACTIVE
|
||||
SETSTICKYKEYS
|
||||
SETTEXT
|
||||
@@ -1920,7 +1915,6 @@ tracerpt
|
||||
trackbar
|
||||
trafficmanager
|
||||
transicc
|
||||
transitioning
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
trl
|
||||
@@ -2001,6 +1995,7 @@ valuegenerator
|
||||
VARTYPE
|
||||
vbcscompiler
|
||||
vcamp
|
||||
VCENTER
|
||||
vcgtq
|
||||
VCINSTALLDIR
|
||||
vcp
|
||||
@@ -2038,6 +2033,7 @@ vorrq
|
||||
VOS
|
||||
vpaddlq
|
||||
vqsubq
|
||||
VREDRAW
|
||||
vreinterpretq
|
||||
VSC
|
||||
VSCBD
|
||||
@@ -2086,7 +2082,6 @@ winapi
|
||||
winappsdk
|
||||
windir
|
||||
WINDOWCREATED
|
||||
WINDOWDESTROYED
|
||||
windowedge
|
||||
WINDOWINFO
|
||||
WINDOWNAME
|
||||
@@ -2182,7 +2177,6 @@ xclip
|
||||
xcopy
|
||||
xdf
|
||||
xfd
|
||||
xhair
|
||||
xmp
|
||||
Xoshiro
|
||||
xsi
|
||||
@@ -2191,7 +2185,6 @@ XUP
|
||||
XVIRTUALSCREEN
|
||||
XXL
|
||||
xxxxxx
|
||||
Yankunytjatjara
|
||||
ycombinator
|
||||
yinle
|
||||
yinyue
|
||||
|
||||
6
.github/actions/spell-check/patterns.txt
vendored
@@ -312,9 +312,3 @@ ms-windows-store://\S+
|
||||
|
||||
# ANSI color codes
|
||||
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+)*m
|
||||
|
||||
# Special licenses text from RNNoise (BSD-style disclaimer: ``AS IS'')
|
||||
``AS IS''
|
||||
|
||||
# Old school moniker for macOS from RNNoise
|
||||
MacOS
|
||||
|
||||
6
.github/copilot-instructions.md
vendored
@@ -30,12 +30,6 @@ These are auto-applied based on file location:
|
||||
- [Runner & Settings UI](.github/instructions/runner-settings-ui.instructions.md)
|
||||
- [Common Libraries](.github/instructions/common-libraries.instructions.md)
|
||||
|
||||
## Shortcut Guide V2 Manifests
|
||||
|
||||
When creating or editing Shortcut Guide keyboard shortcut manifest files, follow the schema and naming conventions in the spec:
|
||||
|
||||
- [WinGet Manifest Keyboard Shortcuts schema](<../doc/specs/WinGet Manifest Keyboard Shortcuts schema.md>) – manifest file format, field definitions, file naming, and the `+` prefix convention for apps without a WinGet package
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
- [Architecture](../doc/devdocs/core/architecture.md)
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 Microsoft Corporation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,302 +0,0 @@
|
||||
---
|
||||
name: powertoys-module-verification
|
||||
description: "Verify a single PowerToys module's release checklist items end-to-end. Drive each checkbox via UIA / Named Events / settings.json edits / clipboard inspection / GPO / SendInput. Output a structured PASS / FAIL / BLOCKED verdict per item with evidence (FAIL distinguishes product defects from stale/ambiguous checklist items). Combine standard winapp ui mechanics (see references/winapp-ui-testing.md) with PT-specific recipes and the helper .ps1 files shipped with this skill."
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when you need to **verify every checklist item for a single PowerToys module** for a release sign-off — e.g. "verify all 18 Color Picker items", "verify all 88 Command Palette items". Each item produces a PASS / FAIL / BLOCKED verdict with evidence (UIA enumeration, log line, settings.json diff, screenshot, etc.).
|
||||
|
||||
The **checklist to verify is supplied with the task** (the calling prompt points you at the module's checklist file). This skill is the *how* — the drive techniques, helpers, taxonomy, and reporting format — independent of any specific checklist.
|
||||
|
||||
## Required reads (in order)
|
||||
|
||||
1. **`references/winapp-ui-testing.md`** — the **prerequisite** UIA mechanics doc (winapp ui verbs, scripted batch testing, file pickers, accessibility audits, screenshots, click-vs-invoke, PostMessage, SendInput cb=40, stunted-UIA recovery, settings-mutation safety contract). **Read this first** — this skill assumes you know its content and only adds PT-specific extensions.
|
||||
2. **This `SKILL.md`** — the PT-specific playbook: the 3-bucket drive-technique selector (Step 2), classification taxonomy, critical pitfalls, helper-script catalog.
|
||||
3. **`references/modules/<module>.md` IF IT EXISTS** — per-module entry-paths, item-by-item recipes, common BLOCKED traps, fixture lists, source citations. **Always check `references/modules/` first.** If no profile exists, fall back to this SKILL.md and create one after you finish (template in `references/modules/README.md`).
|
||||
4. **`references/explorer-context-menu-flow.md` IF your module registers an Explorer right-click entry** (PowerRename, File Locksmith, Image Resizer, New+, Preview Pane, RegistryPreview) — shared synthetic-right-click + UIA-invoke + multi-file-selection flow + module-caption table. Helper: `scripts/pt-explorer-contextmenu.ps1`.
|
||||
5. **`references/pre-flight.md`** — pre-flight checks, bootstrap snippet, state-hygiene cleanup, final wrap-up, hard rules.
|
||||
6. **`references/reporting-format.md`** — per-item table template, top-of-report summary, step-table rules, anti-patterns, worked example.
|
||||
7. **`references/environment-setup.md`** — RDP/sleep/screensaver/session-attachment gotchas. Cite in BLK-ENV verdicts.
|
||||
8. **`references/release-checklist/<module>.md`** — the checklist for the module under test (one file per module; see `references/release-checklist/index.md` for the full list). Each item carries `[ADMIN: …]` + `[CLARITY: …]` metadata. **This file IS the set of items to verify.**
|
||||
|
||||
## Helper scripts shipped with this skill
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `scripts/pt-shared-events.ps1` | `Invoke-PtSharedEvent`, `Test-PtSharedEvent`, `Get-PtSharedEventCatalog` — 56-entry friendly-name map for PT Named Events (CmdPal.Show, AOT.Pin, PowerLauncher.Invoke, LightSwitch.Toggle, ZoomIt.Draw, ...). The deterministic, foreground-free, UIPI-immune way to trigger a module. |
|
||||
| `scripts/pt-sendinput-chord.ps1` | `Send-PtChord`, `Wait-PtHotkeyAccepted` — last-resort SendInput hotkey injection with the cb=40 fix. Use only when the module has no Named Event and the hotkey itself is the test subject. |
|
||||
| `scripts/pt-foreground-guard.ps1` | `Test-PtForeground`, `Force-PtForeground`, `Assert-PtForegroundOrAbort` — guard helpers to ensure target window IS foreground before SendInput, so keys don't leak to caller's terminal. |
|
||||
| `scripts/pt-cmdpal-recycle.ps1` | `Reset-CmdPalAppX`, `Reset-CmdPalToHome`, `Test-CmdPalDegraded`, `Invoke-CmdPalQuery` — CmdPal-specific lifecycle (handles TextChanged-broken state, BackButton navigation, AppX recycle). |
|
||||
| `scripts/pt-admin-probe.ps1` | `Test-PtAdmin`, `Test-ProcessElevated`, `Test-PtRunnerAdmin` — TokenElevation probes to verify your session and the PT runner have the right elevation for the test. |
|
||||
| `scripts/pt-clipboard-diff.ps1` | `Get-PtClipboardFormats`, `Compare-PtClipboardFormatDiff`, `Set-PtClipboardRich` — multi-format clipboard inspection for Advanced Paste tests. |
|
||||
| `scripts/pt-explorer-com.ps1` | `Get-PtExplorerWindows`, `Open-PtExplorerAtPath`, `Select-PtExplorerFiles`, `Invoke-PtPeekWithExplorerSelection`, `Test-PtInteractiveDesktop` — drive Explorer via Shell COM to set up multi-file selections, then trigger Peek/FZ/PowerRename/Image Resizer/Workspaces via their hotkeys. **Use this for Peek L706-L709, L719-L720 and any test that needs an Explorer file selection.** |
|
||||
| `scripts/pt-explorer-contextmenu.ps1` | `Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems` — open Win11's real context menu via synthetic right-click (with retry), then UIA-invoke a menu item by name. **Canonical user-flow path for File Locksmith / Image Resizer / PowerRename / New+ menu-presence + launch tests.** Needs an unlocked interactive desktop. See `references/explorer-context-menu-flow.md` for the full write-up, stability notes, and per-module captions. |
|
||||
| `scripts/pt-shell-verbs.ps1` | `Get-PtShellVerbs`, `Invoke-PtShellVerb`, `Reset-PtShellComCache` — enumerate + invoke CLASSIC HKCR shell verbs via Shell.Application COM. **NOT for PT context-menu modules on Win11** (PT registers via IExplorerCommand, not classic — use `pt-explorer-contextmenu.ps1` for those). Useful for non-PT verbs (Open/Edit/Send-to/third-party) and as a negative check that PT verbs are NOT classic-shadowed. |
|
||||
| `scripts/pt-state.ps1` | `Get-PtSettings`, `Get-PtModuleSettings`, `Get-CmdPalSettings`, `Get-PtRunnerLogTail`, `Test-PtModuleEnabled`, `Test-PtModuleProcess`, `Restart-PtRunner`, `Backup-PtModuleSettings`, `Restore-PtModuleSettings` — common state checks. |
|
||||
| `scripts/pt-nonelevated.ps1` | `Start-PtNonElevated`, `Invoke-PtNonElevatedCapture` — launch an exe at **Medium IL (non-elevated)** from an elevated agent shell via a one-shot `RunLevel Limited` scheduled task. Required for elevation-visibility tests (a non-elevated module must NOT see higher-integrity processes; e.g. File Locksmith L649/L650). Verify the result with `Test-ProcessElevated`. |
|
||||
|
||||
Dot-source them **all** at once in your bootstrap (the `Get-ChildItem` loop loads every helper — see **Step 1 — Bootstrap**):
|
||||
```powershell
|
||||
$skill = '<this skill folder>' # the folder containing SKILL.md, e.g. <PT-repo>\.github\skills\powertoys-module-verification
|
||||
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
|
||||
```
|
||||
|
||||
## Step 1 — Bootstrap
|
||||
|
||||
```powershell
|
||||
$module = 'AdvancedPaste' # or 'CmdPal', 'FZ', 'Peek', ...
|
||||
# Work out of %TEMP% during the run (keeps screenshots/scratch off OneDrive); move to the
|
||||
# sign-off archive at the very end (see Step 7).
|
||||
$workspace = "$env:TEMP\verify-$module-$(Get-Date -Format yyyyMMdd-HHmmss)"
|
||||
New-Item -ItemType Directory -Path $workspace, "$workspace\artifacts" -Force | Out-Null
|
||||
$report = "$workspace\verify-$module.md"
|
||||
|
||||
# Dot-source helpers
|
||||
$skill = '<this skill folder>' # set once at top of your script (the folder containing SKILL.md)
|
||||
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
|
||||
|
||||
# Verify environment
|
||||
"=== Environment ===" | Tee-Object $report -Append
|
||||
"IsAdmin: $(Test-PtAdmin)" | Tee-Object $report -Append
|
||||
$rn = Test-PtRunnerAdmin
|
||||
"PT runner: PID=$($rn.Pid) Elevated=$($rn.Elevated)" | Tee-Object $report -Append
|
||||
|
||||
# The checklist items to verify are supplied with the task (see the calling prompt).
|
||||
# Read that module's checklist file and iterate its items (see Step 6 — Verifier loop).
|
||||
```
|
||||
|
||||
## Step 2 — Drive techniques
|
||||
|
||||
Every checklist item boils down to ONE of three intents. **Pick the bucket from the verb in the item, then use the best technique inside it.** Stop at the first technique that works.
|
||||
|
||||
| Intent | Verb-cues in the checklist item | Bucket |
|
||||
|---|---|---|
|
||||
| Change a setting | "default is X", "setting persists", "is enabled/disabled by default", "value Y is accepted" | §2.A |
|
||||
| Interact with a UI element | "click X", "toggle X", "type into Y", "X button is visible", "selecting Z does W" | §2.B |
|
||||
| Trigger a module action | "pressing hotkey X opens Y", "module launches", "Z happens when invoked" | §2.C |
|
||||
|
||||
### §2.A — Change a setting (single technique)
|
||||
|
||||
Edit the JSON file the module reads, wait for the file-watcher debounce, assert, then restore from backup. Zero external tools.
|
||||
|
||||
```powershell
|
||||
$bk = Backup-PtModuleSettings -ModuleDir AdvancedPaste
|
||||
try {
|
||||
$j = Get-PtModuleSettings -ModuleDir AdvancedPaste
|
||||
$j.properties.IsAdvancedAIEnabled.value = $false
|
||||
$j | ConvertTo-Json -Depth 12 | Set-Content "$env:LOCALAPPDATA\Microsoft\PowerToys\AdvancedPaste\settings.json"
|
||||
Start-Sleep -Seconds 4 # debounce — runner re-reads via file-watcher
|
||||
# ... assertion ...
|
||||
} finally {
|
||||
Restore-PtModuleSettings -ModuleDir AdvancedPaste -BackupPath $bk
|
||||
}
|
||||
```
|
||||
|
||||
> For shell-extension modules (PowerRename, File Locksmith, Image Resizer, New+) edit the **module-owned** file under `%LOCALAPPDATA%\Microsoft\PowerToys\<Module>\`, then `Restart-PtRunner` (and on stubborn handlers, restart Explorer). See pitfall #18 below.
|
||||
>
|
||||
> If you need to flip the *enabled* bit for a whole module, debounce isn't enough — call `Restart-PtRunner` after the write.
|
||||
|
||||
### §2.B — Interact with a UI element (2 techniques, most-reliable first)
|
||||
|
||||
#### B1. UIA invoke / set-value — **always try first**
|
||||
```powershell
|
||||
winapp ui invoke 'SubmitButton' -a PowerToys.Settings
|
||||
winapp ui set-value 'QueryTextBox' '=2+3*4' -a PowerToys.PowerLauncher
|
||||
Start-Sleep -Milliseconds 600
|
||||
winapp ui inspect -a PowerToys.PowerLauncher --depth 7 -i 2>$null
|
||||
```
|
||||
Invoke goes through UIA InvokePattern COM IPC — no foreground steal, no UIPI. See references/winapp-ui-testing.md §CRITICAL — invoke vs click.
|
||||
|
||||
#### B2. PostMessage WM_KEYDOWN/CHAR — when UIA can't reach the target
|
||||
For elevated targets, AppX windows with stunted UIA trees, or keystrokes that UIA `set-value` can't dispatch (arrow-key ListView nav, Enter to commit). See references/winapp-ui-testing.md §CRITICAL — Keystroke input that bypasses UIPI (PostMessage). Esc is often filtered by WinUI 3 raw-input hook — use BackButton invoke instead.
|
||||
|
||||
### §2.C — Trigger a module action (2 techniques, most-reliable first)
|
||||
|
||||
| | C1 Named Event | C2 SendInput chord |
|
||||
|---|---|---|
|
||||
| **Proves** | The action fires (the path *downstream* of the hotkey). **Not** that the chord is bound. | The full path: real keys → runner hook → action. The **only** method that proves the chord binding itself. |
|
||||
| **Robustness** | Highest — no foreground, no input desktop, UIPI-immune; works headless / RDP-minimized. | Lowest — needs an attached input desktop (else `BLK-ENV`), steals foreground, can't inject OS-reserved chords (Win+L / Win+Tab). |
|
||||
| **Precondition** | Owning module process is running (the event only exists while it is). | Attached input desktop + foreground. |
|
||||
|
||||
**Pick by what the item asserts:** for "does action Y happen" use C1; for "pressing chord X triggers Y" or "the rebind takes effect", C1 is insufficient (it bypasses the chord) — use C2, or C1 *plus* a runner-log line proving the chord was accepted.
|
||||
|
||||
#### C1. Named Event signal — preferred
|
||||
```powershell
|
||||
Invoke-PtSharedEvent -Name 'CmdPal.Show' # opens CmdPal without keyboard
|
||||
Invoke-PtSharedEvent -Name 'AOT.Pin' # pins foreground window via AOT
|
||||
Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke' # opens PT Run
|
||||
Invoke-PtSharedEvent -Name 'LightSwitch.Toggle' # toggles theme
|
||||
Get-PtSharedEventCatalog | Format-Table # full list
|
||||
```
|
||||
No synthetic input — it's a `SetEvent` on the kernel event the module waits on, the same downstream path the runner's hotkey handler signals. Verify the side effect via UIA (`winapp ui list-windows -a <module>`), a log line (`Get-PtRunnerLogTail`), or settings.json diff (`Get-PtModuleSettings`). The event only exists while the owning process runs, so `Test-PtSharedEvent` doubles as an "is the module alive" check.
|
||||
|
||||
#### C2. SendInput chord — last resort / chord-binding verification
|
||||
Real synthetic keys. Loud (steals foreground) and fragile, but the only way to prove the activation chord is actually bound. The runner's global keyboard hook catches the chord regardless of focus, so the precondition is just an **attached input desktop** (pitfall #13; on a detached desktop `SendInput` returns `ACCESS_DENIED` and the keys vanish → mark `BLK-ENV`).
|
||||
```powershell
|
||||
# Precondition: input desktop attached? 0 = detached → don't bother sending, mark BLK-ENV (pitfall #13)
|
||||
if ([PtFg]::GetForegroundWindow() -eq [IntPtr]::Zero) { throw 'No input desktop — BLK-ENV (pitfall #13)' }
|
||||
|
||||
Send-PtChord -Mods 0x5B,0x10 -Key 0x43 # Win+Shift+C → Color Picker (cb=40 fix is inside the helper)
|
||||
$line = Wait-PtHotkeyAccepted -ModuleHint 'Color' -TimeoutSec 3
|
||||
if (-not $line) { throw 'Runner did not log hotkey invocation' }
|
||||
```
|
||||
|
||||
> **Rare fallback — a module that uses its own `RegisterHotKey` and exposes no Named Event.** Post `WM_HOTKEY` (`0x312`) straight to its message window (find the HWND via `EnumWindows`+`GetClassName` through `Add-Type` — same P/Invoke pattern as `pt-foreground-guard.ps1`). **No current PT module needs this:** ZoomIt — the obvious candidate — also waits on Named Events (`ZoomIt.Zoom`, `ZoomIt.Draw`, …; source: `Zoomit.cpp` `CreateEventW(ZOOMIT_ZOOM_EVENT)`), so drive it with C1.
|
||||
|
||||
> **Different case — sending keys *into* a specific focused window** (e.g. a CmdPal alias like `=` / `<` / `>` that `winapp ui set-value` can't trigger because it bypasses TextChanged; see pitfalls #4 and #6). Here the keystrokes go to whatever currently has focus, so you must bring the target window foreground first:
|
||||
> ```powershell
|
||||
> Assert-PtForegroundOrAbort -AppId Microsoft.CmdPal.UI # -AppId = the window you're typing INTO
|
||||
> Send-PtChord -Key 0xBB # '=' (no modifiers) to trigger the calculator alias
|
||||
> ```
|
||||
> The `-AppId` is whatever window you're targeting — it's **not** CmdPal-specific. CmdPal is just the worst offender: its AppX foreground-lock drops focus after the first `SetForegroundWindow`, so without the guard the keys silently leak to your terminal.
|
||||
|
||||
> Verdict decisions (PASS if behavior matches spec; **FAIL** if the product is wrong *or* the checklist item is stale/ambiguous; BLOCKED if you couldn't run the check after ≥2 entry-paths) live in **Step 3 — Classification taxonomy** below. Don't put verdict logic in Step 2.
|
||||
|
||||
## Step 3 — Classification taxonomy
|
||||
|
||||
### Verdicts (assign exactly ONE per item)
|
||||
|
||||
| Verdict | Meaning |
|
||||
|---|---|
|
||||
| **PASS** | You drove/observed the behavior and it matched the spec. **A pass is a pass — there is no PASS sub-type.** Record *how* you verified in the item's **Category** field as free text, e.g. "full UIA flow + asserted popup", "settings.json round-trip", "runner-log line", "Shell COM / IExplorerCommand", "screenshot pixel-diff", "output matches fixture", "process spawn/exit", "module CLI", "admin GPO write". |
|
||||
| **FAIL** | The item is **red** — something is wrong and action is required. Treat the checklist as test code: a test fails because **the product is wrong** *or* **the test/checklist is wrong**. Record the **cause** in the **Category** field: <br>• **product** — behavior contradicts a valid spec → file a product bug (repro + expected-vs-actual + screenshot/log + build version). <br>• **checklist** — the item itself is broken: *stale* (feature was removed/deprecated — cite the source grep proving it's gone) or *ambiguous* (`[CLARITY: VAGUE-*]`, no definable pass/fail criterion — quote the original wording). Fix the checklist, not the product. |
|
||||
| **BLOCKED** | Couldn't run the check in this environment / with this toolset *after ≥2 entry-paths* — inconclusive, like a skipped test. **Not red against the product.** Tag exactly one concrete reason below. |
|
||||
|
||||
### BLOCKED reasons
|
||||
Different failure reasons stay distinct because each drives a different remediation.
|
||||
|
||||
| Reason | When |
|
||||
|---|---|
|
||||
| `BLK-ENV` | This specific shell can't drive it (non-interactive / Session 0, RDP-minimized, missing Explorer windows) but a normal interactive desktop CAN. Triggers a "re-run on an interactive desktop" recommendation. Cite `references/environment-setup.md`. |
|
||||
| `BLK-HARDWARE` | Needs hardware this session lacks — multi-monitor, 2 physical PCs (MWB), real camera / battery / game-mode, or live screen/device capture. State the specific shortfall in **Category**. |
|
||||
| `BLK-DRAG-REQUIRED` | Needs a real mouse-drag gesture; synthetic drag is insufficient (e.g. FancyZones snap). |
|
||||
| `BLK-DESTRUCTIVE` | Reboot, hibernate, install/uninstall, or mid-session AppX uninstall — would damage the run environment. |
|
||||
| `BLK-VISUAL-RENDER` | The thing to verify is a rendered surface UIA can't see — WinUI3 islands, WebView2, or Explorer-side context-menu rendering/localization. Needs pixel/OCR or a manual eyeball. |
|
||||
| `BLK-OVERLAY-INPUT-BLOCK` | Overlay both blocks input and excludes itself from capture (`BlockInput` + `WDA_EXCLUDEFROMCAPTURE`, e.g. ZoomIt draw mode) — can neither drive nor screenshot it. |
|
||||
| `BLK-EXTERNAL-APP` | Needs a 3rd-party tool, a real API key, or a system locale change. |
|
||||
|
||||
**Rule of thumb**: in your report, separate the two FAIL causes — *product* FAILs are bugs to file; *checklist* FAILs are items to rewrite or prune. `BLOCKED` is only for a concrete, named obstacle (cite it), never a substitute for effort. If a large share of a module's items are checklist-FAILs, the checklist needs an overhaul before re-verifying.
|
||||
|
||||
## Step 4 — Report format
|
||||
|
||||
**See `references/reporting-format.md` for the full template** (per-item table, summary, step-table rules, anti-patterns, worked example). Don't paraphrase; copy the templates literally. This includes a mandatory **§G Retrospective** — a self-reflection on the *run itself*: list every friction encountered (classified by source — `SKILL-UNCLEAR` / `WINAPP-TOOL-BUG` / `WINAPP-DOC-UNCLEAR` / `HELPER-FLAW` / `PT-PRODUCT` / `ENVIRONMENT` — with severity + minutes/attempts cost + a suggested fix), or write `Everything was smooth — no friction encountered.` if there was none. This is how the skill improves run over run, so don't skip it.
|
||||
|
||||
## Step 5 — State hygiene (CRITICAL)
|
||||
|
||||
**See `references/pre-flight.md` §State hygiene** for the backup/restore pattern and cleanup commands. Always wrap mutations in `try { ... } finally { Restore-* }`.
|
||||
|
||||
## Module-specific quick reference
|
||||
|
||||
**Look for `references/modules/<module>.md` FIRST.** Each per-module profile contains paths, entry-paths, item-by-item recipes, common BLOCKED traps, fixture lists, and source citations specific to that module.
|
||||
|
||||
Catalog: see `references/modules/README.md`. Currently authored: `peek.md`, `power-rename.md`, `file-locksmith.md`, `image-resizer.md`.
|
||||
|
||||
If your module has NO profile yet:
|
||||
1. Fall back to the generic drive-stack in §2 above.
|
||||
2. **For Explorer-context-menu modules** (PowerRename / File Locksmith / Image Resizer / New+ / Preview Pane / RegistryPreview): read **`references/explorer-context-menu-flow.md`** first — it has the synthetic-right-click + UIA-invoke pattern with stability rules and module-caption table. Per-module profiles cite it and only document module-specific quirks. The canonical helper is `scripts/pt-explorer-contextmenu.ps1`.
|
||||
3. After finishing the verification, **create the profile** using the template in `references/modules/README.md` so the next agent benefits from what you learned.
|
||||
|
||||
Quick one-liners for modules without dedicated profiles (will be moved to per-module files as they're authored):
|
||||
|
||||
- **Advanced Paste**: `Invoke-PtSharedEvent -Name 'AdvancedPaste.ShowUI'` + `Set-PtClipboardRich` + `Compare-PtClipboardFormatDiff` (see `scripts/pt-clipboard-diff.ps1`).
|
||||
- **Command Palette**: `Invoke-PtSharedEvent -Name 'CmdPal.Show'` + `Invoke-CmdPalQuery` (auto-handles degraded state via `scripts/pt-cmdpal-recycle.ps1`). Settings file via `Get-CmdPalSettings`.
|
||||
- **PowerToys Run**: `Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke'` + `winapp ui set-value QueryTextBox`. Window has 2 HWNDs — filter by width ≥ 800.
|
||||
- **FancyZones**: `Invoke-PtSharedEvent -Name 'FancyZones.ToggleEditor'`. Snap-drag tests are usually `BLK-DRAG-REQUIRED`; settings verify via settings.json round-trip.
|
||||
- **Light Switch**: `Invoke-PtSharedEvent -Name 'LightSwitch.Toggle' | LightSwitch.Light | LightSwitch.Dark`. Verify via `HKCU:\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme`.
|
||||
- **Always on Top**: `Invoke-PtSharedEvent -Name 'AOT.Pin'`. Verify `WS_EX_TOPMOST` on pinned HWND.
|
||||
- **Hosts File Editor** (admin): `Invoke-PtSharedEvent -Name 'Hosts.Show' | Hosts.ShowAdmin`.
|
||||
- **GPO** (admin): write `HKLM:\Software\Policies\PowerToys` + `Restart-PtRunner` + `Get-PtRunnerLogTail -Pattern 'GPO sets'`. Cleanup: `Remove-Item HKLM:\Software\Policies\PowerToys -Recurse -Force`.
|
||||
- **Mouse Without Borders**: most items `BLK-HARDWARE` (need 2 physical PCs).
|
||||
- **ZoomIt**: most modes inside `BlockInput + WDA_EXCLUDEFROMCAPTURE` overlay → `BLK-OVERLAY-INPUT-BLOCK`. Mode triggers: `ZoomIt.Zoom`, `ZoomIt.Draw`, `ZoomIt.Break`, etc.
|
||||
- **Peek**: see `references/modules/peek.md` for the full recipe (CLI back-door + Shell.Application + Ctrl+Space).
|
||||
|
||||
## Step 6 — Verifier loop per checkbox
|
||||
|
||||
```
|
||||
For each item in module:
|
||||
1. Pick a bucket from the verb in the item (§2.A change a setting / §2.B interact with UI / §2.C trigger an action)
|
||||
2. Walk that bucket's techniques top-to-bottom; stop at the first one that drives the item
|
||||
3. Compare observed behavior to the spec:
|
||||
• matches the spec → PASS (note the method in Category)
|
||||
• product behaves wrong → FAIL, cause=product (repro + expected/actual + screenshot/log + build)
|
||||
4. Checklist item itself is broken — feature removed from source, or spec too ambiguous to judge → FAIL, cause=checklist (cite the source proof / quote the wording)
|
||||
5. Couldn't drive it after ≥2 entry-paths → BLOCKED with a concrete reason (§3)
|
||||
6. Record verdict + evidence + cleanup
|
||||
7. Next item
|
||||
```
|
||||
|
||||
When done, run state hygiene cleanup, write the report **including the §G retrospective**, archive the workspace (Step 7), and exit.
|
||||
|
||||
## Step 7 — Archive the workspace to the sign-off folder (do this LAST)
|
||||
|
||||
The live run works out of `%TEMP%`, but the **final deliverable must live in the module sign-off archive** so reports persist and sync via OneDrive:
|
||||
|
||||
```powershell
|
||||
# After the report is written AND the artifact-existence check passes:
|
||||
$signoff = "$env:OneDrive\PowerToys\Module-Signoff" # e.g. C:\Users\<you>\OneDrive - Microsoft\PowerToys\Module-Signoff
|
||||
New-Item -ItemType Directory -Path $signoff -Force | Out-Null
|
||||
$final = Join-Path $signoff (Split-Path $workspace -Leaf)
|
||||
Move-Item -Path $workspace -Destination $final -Force
|
||||
# Report uses RELATIVE artifacts/… paths, so all links stay valid after the move.
|
||||
Write-Host "Final report: $(Join-Path $final (Split-Path $report -Leaf))"
|
||||
```
|
||||
|
||||
Print the **moved** report path (under `…\PowerToys\Module-Signoff\`) as the last line — never the `%TEMP%` path.
|
||||
|
||||
## Invocation & placeholders
|
||||
|
||||
This skill auto-activates when you ask to verify a PowerToys module's checklist (e.g. "verify all Color Picker items"). **One module per run** — never chain multiple modules into one report. Resolve these placeholders for the module under test:
|
||||
|
||||
| Placeholder | Substitute with |
|
||||
|---|---|
|
||||
| `<Module>` | Exact display name, e.g. `Color Picker`, `Command Palette`, `PowerToys Run`, `FancyZones` (see `references/release-checklist/index.md`). |
|
||||
| `<module>` | Lowercase-kebab-case for file lookup, e.g. `color-picker`, `command-palette`, `power-rename` — used for BOTH `references/release-checklist/<module>.md` (checklist) and `references/modules/<module>.md` (profile, if any). |
|
||||
| `<ModuleDir>` | settings.json sub-dir under `%LOCALAPPDATA%\Microsoft\PowerToys\` (e.g. `AdvancedPaste`, `FancyZones`, `PowerToys Run` (with space)). |
|
||||
| `<N>` | Total item count for this module. |
|
||||
|
||||
**Execution order:** `references/pre-flight.md` → per item, the §2 drive-stack (this file) → `references/reporting-format.md` per-item table → Step 6 verifier loop → `references/pre-flight.md` §Final wrap-up → Step 7 archive → print the final report path.
|
||||
|
||||
## What NOT to do
|
||||
|
||||
- Do NOT chain multiple modules in one report — one module per run.
|
||||
- Do NOT mark an item BLOCKED without a concrete, named obstacle (see §3 and `references/pre-flight.md` §Hard rules).
|
||||
- Do NOT invent steps for a VAGUE checklist item — if the spec is too ambiguous to judge, that is FAIL (cause=checklist), not a guess.
|
||||
- All other rules (foreground guard, always restore mutated state, etc.) live in `references/pre-flight.md` §Hard rules — follow them.
|
||||
|
||||
## Critical pitfalls (PT-specific)
|
||||
|
||||
*Reference, not a sequential step — skim before you start and consult while driving. Numbered for cross-reference only.*
|
||||
|
||||
1. **PT runner does NOT auto-pickup edits to master `settings.json`** (top-level `enabled.<Module>` flags). Call `Restart-PtRunner`.
|
||||
2. **Each module's own `<Module>\settings.json` IS hot-reloaded** via per-module file watcher (~3s debounce). **EXCEPTION — shell-extension/context-menu modules do NOT read this file; see pitfall #18.**
|
||||
3. **PT Run setting key has a space**: `"PowerToys Run"` not `PowerToysRun`.
|
||||
4. **CmdPal AppX foreground from external CLI is unreliable** — Windows foreground-lock blocks `SetForegroundWindow` after the first call. SendInput keys silently leak to your terminal. **Always `Assert-PtForegroundOrAbort` before SendInput.**
|
||||
5. **CmdPal AppX enters TextChanged-broken state** every ~30 probes — `Test-CmdPalDegraded` + `Reset-CmdPalAppX` to recover.
|
||||
6. **CmdPal alias detection (`=`, `<`, `>`, `:`, `$`, `??`, `)`) requires real keystrokes** — `winapp ui set-value` bypasses TextChanged and the alias never fires. Use Send-PtChord + `Assert-PtForegroundOrAbort` for aliases; use set-value for plain queries.
|
||||
7. **CmdPal Esc handler is filtered** by WinUI 3 raw-input hook — use `winapp ui invoke BackButton` instead (see `Reset-CmdPalToHome`).
|
||||
8. **GPO HKLM vs HKCU**: HKLM wins when both are set with conflicting values.
|
||||
9. **HKLM `Software\Policies\PowerToys` writes require admin** — verify with `Test-PtAdmin`.
|
||||
10. **`Stop-Process` is policy-blocked in this session unless you pass `-Id <int>` literally**. Always inline the PID.
|
||||
11. **WinUI 3 islands are largely invisible to UIA** (QuickAccess flyout, RegistryPreview Monaco editor, Peek WebView2). For these, fall back to screenshot + OCR or settings.json diff.
|
||||
12. **OS-reserved chords (Win+L, Win+Tab)** are consumed by Windows before any hook and cannot be injected via SendInput at all.
|
||||
13. **RDP minimized = `SendInput` denied.** Even though `quser` shows the remote session State=Active, minimizing the mstsc client detaches the session's input desktop. `GetForegroundWindow()` returns 0; `SendInput` returns `ACCESS_DENIED (5)`; tests that need synthetic input fail. **Same applies to: closed mstsc with X (Disconnected), local PC sleep (RDP TCP drops), remote screensaver/workstation lock, remote machine sleep.** Run `scripts/pt-session-diagnose.ps1` in pre-flight to detect, and see `references/environment-setup.md` for the full per-scenario table + `powercfg` setup commands the user should run before starting the agent. The agent should call `Test-PtForeground` mid-run before each input-injection-dependent item; if it returns False, mark `BLK-ENV` with mitigation citation (an environment block — not a product FAIL).
|
||||
14. **`winapp ui` arg-order quirk**: `winapp ui inspect --depth N -w $hwnd` may intermittently fail to parse `--depth` as Int64 if `-w` precedes it. **Put `-w $hwnd` AFTER `--depth N`** or as the first arg before any flag. If you see "Cannot bind argument" or numeric parse errors, swap the order and retry.
|
||||
15. **`winapp ui list-windows` line wrapping**: when window titles or process names are long, output may wrap a single window's `HWND <id>: "<title>" ... (proc, PID N)` across multiple lines, breaking single-line regexes. Either pipe through `Out-String` and use a multi-line regex, or use `--json` (when supported) and parse structured output.
|
||||
16. **De-elevation: launching a NON-elevated (Medium IL) child from an elevated agent shell.** The drive-stack only covers gaining *more* privilege; some items need the opposite. From a High-IL shell you cannot `Start-Process` a Medium-IL child directly. Use `scripts/pt-nonelevated.ps1` (`Start-PtNonElevated` / `Invoke-PtNonElevatedCapture`) — a one-shot `RunLevel Limited` + `LogonType Interactive` scheduled task that lands on the user's desktop at their filtered token. Confirm with `Test-ProcessElevated`. Needed for elevation-visibility pairs (File Locksmith L649/L650: non-elevated FL must not see the elevated runner; elevated FL must).
|
||||
17. **Win11 packaged context menus are not observable without real Explorer.** Modern PT context-menu entries are packaged `IExplorerCommand`s (sparse MSIX, e.g. File Locksmith CLSID `{AAF1E27D-…}`). They are **NOT** enumerable via classic `Shell.Application … FolderItem.Verbs()` and **NOT** `CoCreate`-able from a non-Explorer host (`REGDB_E_CLASSNOTREGISTERED`). So "verify the entry appears / no longer appears" cannot be pixel-verified by API. Verify instead via the gate flag the entry's `GetState` reads (e.g. general `enabled.<Module>`) + a source citation that maps it to `ECS_HIDDEN`; treat the literal render as `BLK-VISUAL-RENDER` and recommend a 5-second manual right-click. (Disabling does NOT unregister the package — it stays `Status Ok`; the entry is hidden dynamically.)
|
||||
18. **Shell-extension modules read a module-OWNED settings file, NOT the PT-store `<Module>\settings.json`.** PowerRename, File Locksmith, Image Resizer, and New+ context-menu handlers and exes run *outside* the runner (hosted by Explorer / launched on demand) and cannot use the PT-Settings IPC. Each reads its **own** json in `%LOCALAPPDATA%\Microsoft\PowerToys\<Module>\` *at process/handler launch* (registry-migrated `CSettings`/`Settings` classes — `lib/Settings.cpp` `Load→ParseJson`). The PT-Settings UI writes the *PT-store* `settings.json` (the `bool_*`/`int_*` file `Get-PtModuleSettings` reads); the runner's module DLL syncs PT-store→module-store **only on a Settings-UI change event** — so the PT-store file can be **stale for days** and editing it has **no effect** on the running shell handler. **To drive a settings item on these modules, edit the module-owned file directly (drive-stack §2.A) and relaunch the module (or restart runner+Explorer for the menu handlers), then restore.**
|
||||
|
||||
**Pitfall #18 — module-owned files + their key style** (verified 2026-06-10 against `<PT-repo>\src`):
|
||||
|
||||
| Module | Module-owned file (under `…\PowerToys\<Module>\`) | Key style | PT-store `settings.json` keys (UI/`Get-PtModuleSettings`) |
|
||||
|---|---|---|---|
|
||||
| PowerRename | `power-rename-settings.json` (+ `power-rename-last-run-data.json`, `search-mru.json`, `replace-mru.json`) | `ShowIcon`, `ExtendedContextMenuOnly`, `PersistState`, `MRUEnabled`, `MaxMRUSize`, `UseBoostLib` | `bool_show_icon_on_menu`, `bool_show_extended_menu`, `bool_persist_input`, `bool_mru_enabled`, `int_max_mru_size`, `bool_use_boost_lib` |
|
||||
| File Locksmith | `file-locksmith-settings.json` | `ShowInExtendedContextMenu` | `bool_show_extended_context_menu` |
|
||||
| Image Resizer | `image-resizer-settings.json` | (resize sizes/encoder/etc.) | mirrored `imageresizer*` keys |
|
||||
| New+ | `NewPlus\settings.json` (sub-folder **`NewPlus`**, verified on disk + `constants.h` `powertoy_name=L"NewPlus"`) | `HideFileExtension`, `HideStartingDigits`, `TemplateLocation`, `ReplaceVariables`, `BuiltInNewHidePreference` | mirrored `newplus*` keys |
|
||||
|
||||
Confirm which file actually drives behavior with a quick A/B: edit the module-owned file → relaunch → observe; if behavior follows, that's the source of truth (PowerRename L394/L395/L396/L397/L409 were all driven this way).
|
||||
|
||||
If you find another gap during verification, update this skill (add a recipe) AND consider proposing the addition to references/winapp-ui-testing.md if it's generic enough.
|
||||
@@ -1,143 +0,0 @@
|
||||
# Environment setup for PowerToys verification
|
||||
|
||||
**Audience**: human user preparing a test machine before running a verification agent.
|
||||
**One-time** (per test session) — restore afterward.
|
||||
|
||||
## Why this matters
|
||||
|
||||
PowerToys release checklists test real user interactions: pressing hotkeys, dragging files, switching windows. Many tests use `SendInput` to inject keystrokes. Windows refuses `SendInput` when the calling session has **no attached input desktop** — and several common Windows states cause exactly that to happen:
|
||||
|
||||
- RDP client minimized
|
||||
- Workstation locked (screensaver kicked in, idle timeout)
|
||||
- Remote machine asleep
|
||||
- Local machine asleep (RDP TCP drops)
|
||||
|
||||
If any of these happens mid-verification, items that need synthetic input fail with `BLK-ENV` even though the feature itself works fine. This guide eliminates the env causes so the only BLOCKED verdicts you see are real test/framework limitations.
|
||||
|
||||
## Per-scenario reference table
|
||||
|
||||
| Scenario | Remote session State | `GetForegroundWindow()` | `SendInput` | Verdict for input-injection tests |
|
||||
|---|---|---|---|---|
|
||||
| mstsc window focused | Active | Real HWND | Works | ✅ Drivable |
|
||||
| mstsc visible but not focused (covered or alt-tabbed) | Active | Real HWND | Works | ✅ Drivable |
|
||||
| **mstsc MINIMIZED** | Active | **0** | **ACCESS_DENIED (5)** | ❌ BLK-ENV |
|
||||
| Local machine sleeps / RDP TCP drops | **Disconnected** | 0 | ACCESS_DENIED | ❌ BLK-ENV |
|
||||
| User closes mstsc with X (no signout) | **Disconnected** | 0 | ACCESS_DENIED | ❌ BLK-ENV |
|
||||
| Sign out from the remote | Session destroyed | — | — | ❌ Agent killed |
|
||||
| Remote machine sleeps | Suspended | — | — | ❌ Catastrophic — timing corruption |
|
||||
| Remote screensaver / auto-lock kicks in | Active but desktop locked | 0 | ACCESS_DENIED | ❌ BLK-ENV |
|
||||
| **2nd RDP login as the SAME user** (you reconnect from another client) | the OLD session flips to **Disconnected** | 0 (in the old session) | ACCESS_DENIED | ❌ BLK-ENV — your running test's session got taken over |
|
||||
|
||||
**Key insight**: "Active" in `quser` ≠ "can inject input". Always check `GetForegroundWindow()` first (the diagnostic script `scripts/pt-session-diagnose.ps1` does this).
|
||||
|
||||
## Can I verify two modules at once in two RDP sessions?
|
||||
|
||||
Short answer on a **client edition of Windows (Windows 10/11, ProductType=1)**: **no — not as the same user, and effectively not at all.** This was investigated live on this machine (Windows 11 Enterprise, build 26200, `fSingleSessionPerUser=1` default):
|
||||
|
||||
- **Two monitors ≠ two sessions.** A multi-monitor setup is **one** session spanning both screens — it shares a single input desktop, foreground window, and `SendInput` queue across the monitors. Monitor count has nothing to do with session count, so "I have two monitors" does not give you two sessions to run two modules in.
|
||||
- **Sessions are isolated** — each Windows session has its own input desktop, its own foreground window, and its own `SendInput` queue. So *typing in session B genuinely does NOT disturb session A's foreground or input.* Cross-session interference is **not** the problem (so if you somehow DID have two live sessions — Server/RDS — they could run in parallel without colliding).
|
||||
- **The real blocker is session takeover.** Client Windows allows only **one interactive (console/owning) session at a time**, and `fSingleSessionPerUser=1` (the default) means one user gets **one** session. When you open the *second* RDP connection (as the same user), Windows **disconnects the first session** — it flips to `Disconnected`, its input desktop detaches, `GetForegroundWindow()` → 0, and any in-flight UI test there fails with `ACCESS_DENIED` → BLK-ENV. It's not your *typing* that breaks the test; it's the act of logging in the second session that evicts the first.
|
||||
- A different *user* account doesn't rescue it either: client Windows still permits only one connected interactive session, so the second login still disconnects the first.
|
||||
- Therefore, on client Windows, **run modules serially in one session.** True concurrent multi-session needs Windows Server + the RDS (Remote Desktop Session Host) role; unofficial multi-session patches exist but are out of scope here.
|
||||
|
||||
> **Verdict on the common assumption "I can run two modules in two RDP sessions because I have two monitors":** the *conclusion* (can't run two at once on client Windows) is correct, but the *reasoning* is wrong on two counts — two monitors is still one session, and you can't get two simultaneously-Active sessions on client Windows at all (the 2nd login disconnects the 1st). The limit is "can't open a 2nd Active session", not "the two sessions fight each other".
|
||||
|
||||
**Practical guidance:** keep a single RDP session for the whole run; don't reconnect/relogin mid-run; if you must check something elsewhere, alt-tab inside the *same* session rather than opening a new RDP connection. To detect a takeover after the fact, `qwinsta` will show your former session as `Disconnected`.
|
||||
|
||||
## Pre-run setup checklist
|
||||
|
||||
Run these BEFORE starting the verification agent.
|
||||
|
||||
### On the test machine (the one being verified)
|
||||
|
||||
```powershell
|
||||
# Snapshot current power settings so you can restore after
|
||||
$bk = "$env:TEMP\powercfg-backup-$(Get-Date -f yyyyMMdd-HHmmss).txt"
|
||||
powercfg /query SCHEME_CURRENT SUB_SLEEP > $bk
|
||||
powercfg /query SCHEME_CURRENT SUB_VIDEO >> $bk
|
||||
"# Restore later with the values from $bk" | Set-Content "$bk.note"
|
||||
|
||||
# Disable sleep + display-off + hibernate (AC and battery)
|
||||
powercfg /change standby-timeout-ac 0
|
||||
powercfg /change standby-timeout-dc 0
|
||||
powercfg /change monitor-timeout-ac 0
|
||||
powercfg /change monitor-timeout-dc 0
|
||||
powercfg /change hibernate-timeout-ac 0
|
||||
powercfg /change hibernate-timeout-dc 0
|
||||
|
||||
# Disable screensaver
|
||||
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '0'
|
||||
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '0'
|
||||
|
||||
# Disable workstation lock-on-idle (requires admin)
|
||||
# 0 = never lock. Restore your original value (commonly 600 = 10 min) afterward.
|
||||
$origLock = (Get-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name InactivityTimeoutSecs -EA SilentlyContinue).InactivityTimeoutSecs
|
||||
"$origLock" | Out-File "$bk.lock"
|
||||
Set-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' -Name InactivityTimeoutSecs -Value 0 -EA SilentlyContinue
|
||||
|
||||
# Confirm
|
||||
powercfg /query SCHEME_CURRENT SUB_SLEEP | Select-String 'Power Setting GUID|Current AC Power Setting Index'
|
||||
```
|
||||
|
||||
### On the local machine (the one with the RDP client)
|
||||
|
||||
```powershell
|
||||
# Disable local sleep so RDP TCP stays alive
|
||||
powercfg /change standby-timeout-ac 0
|
||||
|
||||
# Practical habit: put mstsc on a monitor you're NOT actively working on.
|
||||
# Don't minimize. Alt-tab is fine; minimize is not.
|
||||
```
|
||||
|
||||
## Mid-run discipline
|
||||
|
||||
While the agent is running:
|
||||
- **Don't minimize mstsc.** Visible-but-unfocused is OK; minimized is not.
|
||||
- **Don't close mstsc with the X.** If you have to step away, fine — leave it open.
|
||||
- **Don't disconnect or reconnect RDP.** Stay continuously connected for the duration of the run.
|
||||
- **Don't sign out** on either end.
|
||||
- If you do step away and the screen locks (despite the setup above), reconnect/unlock and the agent's `Test-PtSessionStillInteractive` guard (if used) will resume; otherwise items mid-execution will be BLK-ENV.
|
||||
|
||||
## Post-run cleanup (restore)
|
||||
|
||||
```powershell
|
||||
# Restore the values you captured to $bk before starting
|
||||
# (e.g. typical defaults: standby 30min, monitor 15min, screensaver 600s, lock 600s)
|
||||
powercfg /change standby-timeout-ac 30
|
||||
powercfg /change standby-timeout-dc 15
|
||||
powercfg /change monitor-timeout-ac 15
|
||||
powercfg /change monitor-timeout-dc 10
|
||||
powercfg /change hibernate-timeout-ac 0 # often default
|
||||
|
||||
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '1'
|
||||
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '600'
|
||||
|
||||
$origLock = Get-Content "$bk.lock" -EA SilentlyContinue
|
||||
if ($origLock) {
|
||||
Set-ItemProperty 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System' `
|
||||
-Name InactivityTimeoutSecs -Value ([int]$origLock) -EA SilentlyContinue
|
||||
}
|
||||
```
|
||||
|
||||
(Values above are typical; adjust to your environment policy.)
|
||||
|
||||
## Diagnostic before you start
|
||||
|
||||
Run `scripts/pt-session-diagnose.ps1` from the agent shell. Expected output for a GO:
|
||||
|
||||
```
|
||||
PASS - this shell can drive interactive PowerToys tests.
|
||||
```
|
||||
|
||||
If it prints FAIL with a `psexec -i <consoleSession> -s pwsh.exe` hint, you're in a non-console session — relaunch the agent shell as suggested before starting verification.
|
||||
|
||||
## Why this isn't in the global SKILL.md
|
||||
|
||||
These are **human prep steps**, not agent instructions. The agent needs to *detect* a bad environment (via `Test-PtInteractiveDesktop` in pre-flight + `Test-PtSessionStillInteractive` mid-run); the user needs to *prevent* one. Different audiences, different docs.
|
||||
|
||||
## Related
|
||||
|
||||
- `scripts/pt-session-diagnose.ps1` — one-shot session diagnostic
|
||||
- `scripts/pt-foreground-guard.ps1` — `Test-PtForeground` / `Force-PtForeground` / `Assert-PtForegroundOrAbort` used by agent
|
||||
- `SKILL.md` pitfall #13 — short pointer to this doc
|
||||
- `references/pre-flight.md` pre-flight check #4 — agent reads this doc when it detects a bad env
|
||||
@@ -1,99 +0,0 @@
|
||||
# Explorer context-menu flow — driving PowerToys shell-menu modules end-to-end
|
||||
|
||||
**Audience**: agents verifying any PowerToys module whose entry point is the **Windows Explorer right-click context menu** — i.e. **File Locksmith, Image Resizer, PowerRename, New+ (NewPlus)**, and similar.
|
||||
|
||||
This is the *true user flow*: open Explorer → select file(s) → right-click → click the module's menu item. Use it when an item's assertion is specifically about the **context menu** (e.g. "the entry appears / no longer appears", "right-click → X launches the module on the selection"). For the module's *internal* behavior you can still prefer a faster back-door (CLI / `last-run.log` / Named Event) — see each module profile — but the menu presence/launch itself can only be observed this way.
|
||||
|
||||
Helper: `scripts/pt-explorer-contextmenu.ps1` (`Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems`).
|
||||
|
||||
## Which approach first? (CLI / back-door vs synthetic menu)
|
||||
|
||||
**Pick the tool by what the item ASSERTS — not "always synthetic" or "always CLI".**
|
||||
|
||||
| The item asserts… | First approach | Why |
|
||||
|---|---|---|
|
||||
| **The menu itself** — entry *appears / no longer appears*, "right-click → select X", caption / localization of the entry | **Synthetic Explorer menu (this doc)** — the *only* valid observer | The CLI/back-door is **blind to the menu**: it runs even when the entry is correctly hidden, so it gives a false PASS (the L652 trap). If the desktop is locked → `BLK-ENV`; do **not** substitute the CLI. |
|
||||
| **Module behavior** — engine finds the lockers, images get resized, files get renamed (the menu is just the trigger) | **CLI / back-door** (`FileLocksmithCLI.exe`, `last-run.log`, Named Event, DSC) | Instant, deterministic, foreground-free, works on a locked desktop. Synthetic adds ~10s + foreground/retry fragility without changing the assertion. |
|
||||
|
||||
**Golden-path rule (do once per module):** run **one** full synthetic right-click → invoke-the-item → confirm-launch. That proves the menu→launch wiring is actually registered *and* validates that the fast back-door is behaviorally equivalent to the real menu (e.g. File Locksmith L641 `step-04/05` did exactly this). After that one golden run, trust the back-door for the remaining behavior items.
|
||||
|
||||
Net: for a context-menu module, **most items are behavior → CLI-first**; the **menu-presence/absence/launch/localization items → synthetic-first**; plus one golden-path synthetic launch.
|
||||
|
||||
## Is it stable?
|
||||
|
||||
**Yes — with the robust variant below.** Verified repeatedly on Win11 (2026-06-08) launching File Locksmith via a genuine right-click + menu click. Two rules make it reliable; ignore them and it gets flaky:
|
||||
|
||||
1. **Invoke the menu item by UIA InvokePattern, not a coordinate left-click.** The menu item exposes `InvokePattern` (`isInvokable=True`). `winapp ui invoke <selector> -w <menuHwnd>` is robust and needs no foreground/coordinates for the *click*. A synthetic left-click at the item's pixel center also works but is the fragile part (DPI, menu repositioning near screen edges, scrolled menus).
|
||||
2. **The right-click that OPENS the menu still needs synthetic input on a foregrounded window — and occasionally a retry.** The first right-click right after Explorer opens sometimes misses (foreground not settled). `Open-PtExplorerContextMenu` retries up to 3×; that removed the flakiness in testing.
|
||||
|
||||
**Hard prerequisite — unlocked interactive desktop.** Synthetic right-click injects into the session input stream, so it requires foreground. If the workstation is locked / RDP minimized (`GetForegroundWindow()=0`), this flow is `BLK-ENV` — there is no foreground-free way to open a context menu. `Open-PtExplorerContextMenu` throws a clear BLK-ENV error in that case. (A 4-hour idle auto-lock is the common culprit — see `references/environment-setup.md`.)
|
||||
|
||||
**Other constraints:**
|
||||
- **Settings for these modules live in a module-OWNED file, not the PT-store `settings.json`** — see `SKILL.md` pitfall #18. The context-menu handler reads e.g. `power-rename-settings.json` / `file-locksmith-settings.json` / `image-resizer-settings.json` / `New\settings.json` at launch; editing the PT-store `<Module>\settings.json` (what `Get-PtModuleSettings` reads) often has **no effect** on the live handler. Drive icon/extended-menu/feature toggles via the module-owned file + relaunch (restart runner+Explorer for the menu handlers), then restore.
|
||||
- This is the **Win11 packaged** context menu (`Microsoft.UI.Content.PopupWindowSiteBridge` / "PopupHost"). The packaged module commands appear **only** here — not in classic `Shell.Application.Verbs()` and not via `CoCreate` of the command CLSID (`REGDB_E_CLASSNOTREGISTERED`). On Win10, or under "Show more options", you'd get the classic menu instead (different structure).
|
||||
- The menu exists in the UIA tree **only while open** — you must open it with real input first; you can't enumerate it cold.
|
||||
- A menu-launched module UI runs **non-elevated** (Explorer's integrity), even if your agent shell is elevated. Mind elevation-visibility (e.g. a non-elevated File Locksmith can't see higher-IL processes — match locker integrity with `scripts/pt-nonelevated.ps1`).
|
||||
|
||||
## Recipe (robust)
|
||||
|
||||
```powershell
|
||||
. "$skill\scripts\pt-explorer-contextmenu.ps1"
|
||||
|
||||
# 0) Guard: must be an unlocked desktop
|
||||
if (-not (Test-PtDesktopInteractive)) { <# mark BLK-ENV, cite references/environment-setup.md #> }
|
||||
|
||||
# 1) Open Explorer on the target folder and grab its CabinetWClass HWND
|
||||
Start-Process explorer.exe $dir; Start-Sleep 4
|
||||
$hwnd = (winapp ui list-windows --json | ConvertFrom-Json |
|
||||
Where-Object { $_.className -eq 'CabinetWClass' -and $_.title -match [regex]::Escape((Split-Path $dir -Leaf)) } |
|
||||
Select-Object -First 1).hwnd
|
||||
|
||||
# 2) Open the real context menu (synthetic right-click, auto-retry)
|
||||
$menu = Open-PtExplorerContextMenu -ExplorerHwnd $hwnd -FileName 'target.txt'
|
||||
|
||||
# 3a) ASSERT PRESENCE / ABSENCE (e.g. "entry no longer appears" when the module is disabled)
|
||||
$items = Get-PtContextMenuItems -MenuHwnd $menu # all visible MenuItem names
|
||||
$present = $items -contains 'Unlock with File Locksmith'
|
||||
|
||||
# 3b) LAUNCH the module via the real menu (UIA invoke by NAME — robust)
|
||||
$ok = Invoke-PtContextMenuItem -MenuHwnd $menu -ItemName 'Unlock with File Locksmith'
|
||||
|
||||
# 4) Verify the module launched (its process/window appears) — e.g.:
|
||||
Start-Sleep 4
|
||||
$ui = Get-Process PowerToys.FileLocksmithUI -EA SilentlyContinue # or PowerToys.ImageResizer, PowerToys.PowerRename
|
||||
```
|
||||
|
||||
To **assert absence** after disabling a module: re-open the menu and check `Get-PtContextMenuItems` no longer contains the caption (the packaged `GetState` re-reads the enabled flag live, so no Explorer restart is needed between toggles).
|
||||
|
||||
## Multi-file selection (Image Resizer, PowerRename)
|
||||
|
||||
These operate on a **selection** of files. Select first (Shell COM is reliable and foreground-free), then right-click one of the selected items:
|
||||
- Use `scripts/pt-explorer-com.ps1` → `Open-PtExplorerAtPath` + `Select-PtExplorerFiles` to establish the multi-select.
|
||||
- Then `Open-PtExplorerContextMenu` on one selected file and `Invoke-PtContextMenuItem` — the module receives the whole selection (the shell handler enumerates all selected `IShellItem`s).
|
||||
|
||||
## Module captions (match by NAME)
|
||||
|
||||
Match the **visible caption**, not the AutomationId (Explorer assigns per-session numeric IDs like `32012` whose value/order varies). Discover the exact caption at runtime with `Get-PtContextMenuItems`. Verified captions:
|
||||
|
||||
| Module | Launched process | Menu caption (verified ✓ / expected) |
|
||||
|---|---|---|
|
||||
| File Locksmith | `PowerToys.FileLocksmithUI.exe` | ✓ `Unlock with File Locksmith` (NB: **not** the checklist's "What's using this file?") |
|
||||
| PowerRename | `PowerToys.PowerRename.exe` | ✓ `Rename with PowerRename` |
|
||||
| Image Resizer | `PowerToys.ImageResizer.exe` | `Resize images` (verify via `Get-PtContextMenuItems` — caption shifted across versions) |
|
||||
| New+ | (creates from template) | `New+` (submenu) |
|
||||
|
||||
> Tip: if a module's caption is unknown, enable the module, open the menu on an applicable file, and run `Get-PtContextMenuItems` to read the exact string — then hard-match it for present/absent assertions.
|
||||
|
||||
## Common failure modes → fixes
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `BLK-ENV: ... GetForegroundWindow()=0` | desktop locked / RDP minimized | unlock & keep mstsc un-minimized (`references/environment-setup.md`); mark `BLK-ENV`, not a test failure |
|
||||
| "popup not found after N attempts" | foreground not settled (esp. first right-click after Explorer opens) | the helper already retries 3×; raise `-MaxTries`, or pre-foreground the window once before calling |
|
||||
| menu item `invoke` returns but nothing launches | matched the wrong node / item disabled | match `type -eq 'MenuItem'` by exact Name; confirm the module is enabled |
|
||||
| caption not found though module enabled | wrong/old caption string, or it's under "Show more options" (classic menu) | enumerate with `Get-PtContextMenuItems`; for classic menu invoke `expandtoclassic` first |
|
||||
| launched UI shows nothing | menu-launched UI is non-elevated and can't see higher-IL targets | match target integrity (`scripts/pt-nonelevated.ps1`) |
|
||||
|
||||
## Referenced by
|
||||
- `references/modules/file-locksmith.md` (L641/L652 — real right-click launch + menu present/absent)
|
||||
- *(future)* `references/modules/image-resizer.md`, `references/modules/power-rename.md`, `references/modules/new-plus.md` — reference this doc for their context-menu items.
|
||||
@@ -1,116 +0,0 @@
|
||||
# Per-module verification profiles (`references/modules/`)
|
||||
|
||||
This folder holds **one short profile per PowerToys module**. Each profile is self-contained guidance specific to that module — paths, entry-paths, capability/control recipes, common BLOCKED traps, fixture lists, source citations.
|
||||
|
||||
## When to read
|
||||
|
||||
When this skill runs for a specific module, check whether `references/modules/<module>.md` exists here. If yes: **read it BEFORE walking the SKILL.md drive-stack** — it tells you which entry-paths actually work for this module's quirks and which BLOCKED traps to avoid.
|
||||
|
||||
If no profile exists, fall back to SKILL.md + the helper scripts.
|
||||
|
||||
## Shared cross-module flows
|
||||
|
||||
Some flows are common to several modules and live in their own top-level docs (not per-module):
|
||||
- **`../references/explorer-context-menu-flow.md`** — driving the real Win11 Explorer right-click context menu end-to-end (open + assert present/absent + launch). Referenced by File Locksmith and any future **Image Resizer / PowerRename / New+** profiles.
|
||||
|
||||
## Why per-module (not just one big SKILL.md)
|
||||
|
||||
- Each module has its own quirks (Peek's `_isFromCli` guard, CmdPal's TextChanged-broken state, PT Run's mini-popup HWND, Workspaces' snapshot-elevation rules). Bundling all of them into the global SKILL.md bloats context and forces every verification to load 25+ KB of mostly-irrelevant text.
|
||||
- A profile lets a focused verification run with only the relevant 5-10 KB.
|
||||
- New gotchas discovered during a module verification round get added to that module's profile, not the global one — keeps the global doc stable.
|
||||
|
||||
## Profile catalog
|
||||
|
||||
| Module | Profile | Status |
|
||||
|---|---|---|
|
||||
| Peek | `peek.md` | ✅ written 2026-06-08 |
|
||||
| File Locksmith | `file-locksmith.md` | ✅ written 2026-06-08 |
|
||||
| Image Resizer | `image-resizer.md` | ✅ written 2026-06-09 |
|
||||
| PowerRename | `power-rename.md` | ✅ written 2026-06-10 (first to cite `../context-menu-cookbook.md` for shared mechanics) |
|
||||
| New+ | `new-plus.md` | ✅ written 2026-06-18 (registration-gate for menu presence; Settings-UI toggle drives template auto-copy) |
|
||||
| (other modules to be added as we encounter sign-off needs) | — | — |
|
||||
|
||||
## For Explorer-context-menu modules: read the canonical flow doc first
|
||||
|
||||
If you're writing a profile for a module that registers an entry in Explorer's Win11 right-click menu (PowerRename, File Locksmith, Image Resizer, New+, Preview Pane, RegistryPreview), **read `../references/explorer-context-menu-flow.md` first**. It has the canonical synthetic-right-click + UIA-invoke recipe with:
|
||||
|
||||
- Which-approach-first decision rule (CLI back-door vs synthetic menu, with the false-positive trap warning)
|
||||
- Stability rules (UIA InvokePattern, retry on first right-click)
|
||||
- Recipe (robust 5-step flow)
|
||||
- Multi-file selection notes
|
||||
- Module captions table (per-module menu-item display names)
|
||||
- Common failure modes
|
||||
- The unlocked-desktop requirement (BLK-ENV gating)
|
||||
|
||||
The shared helper is `scripts/pt-explorer-contextmenu.ps1` (`Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems`).
|
||||
|
||||
Your module profile then only documents the **module-specific** quirks: settings.json schema keys, expected verb caption regex, capability/control recipes, source citations, ceiling.
|
||||
|
||||
`power-rename.md` is the model — ~9 KB despite covering 18 items because the generic mechanics live in the canonical flow doc.
|
||||
|
||||
## Profile template
|
||||
|
||||
When writing a new profile, use this skeleton:
|
||||
|
||||
```markdown
|
||||
# <Module> — module verification profile
|
||||
|
||||
**PT module**: `<ModuleKey>` (one-line description)
|
||||
**Source**: `<PT-repo>\src\modules\<dir>\` (PT repo)
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\<dir>\settings.json`
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\<dir>\Logs\v<ver>\log_<date>.log`
|
||||
**Exes**: `<full path>`
|
||||
**Default hotkey**: `<keys>` (modifiers + code, plus path to ActivationShortcut in settings)
|
||||
**Named Event**: `Local\<name>` (friendly name in pt-shared-events.ps1 catalog)
|
||||
**DSC resource**: `Microsoft.PowerToys/<Name>Settings`
|
||||
|
||||
## Entry-paths (try in order)
|
||||
|
||||
### 1. <fastest path>
|
||||
<powershell code + when to use + source citation>
|
||||
|
||||
### 2. <alternate path>
|
||||
<...>
|
||||
|
||||
### 3. <last-resort path>
|
||||
<...>
|
||||
|
||||
## Recipes — a control/observation map, NOT a per-test-case answer key
|
||||
|
||||
| # | Capability | Drive (control / settings key) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | <a module capability, e.g. "context-menu entry present when enabled"> | <which AutomationId / control / settings key drives it> | <where the result is visible: preview column, settings.json, disk, log, menu> |
|
||||
| 2 | <next capability> | <...> | <...> |
|
||||
|
||||
> **Mapping process** (agent at runtime): read the actual checklist item → identify the capability → find its row → drive the named control and **design your own inputs + assertions for that item**. If no row matches, it's a NEW capability — drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
|
||||
|
||||
> **Why a map, not an answer key**: the table must carry only **durable module knowledge** — which control drives a capability and where to observe the result. Concrete Search/Replace inputs and expected-output assertions are *per-test-case answers*; baking them in turns the profile into a cheat sheet that (a) lets the agent copy answers without understanding and (b) goes stale the moment a checklist item changes its wording or values. Keep inputs + assertions OUT. Only a real UI redesign (a renamed/moved/removed control) should force an edit to this table.
|
||||
|
||||
## Common BLOCKED traps
|
||||
|
||||
<list of mistakes prior agents made + how to avoid them>
|
||||
|
||||
## Fixture files needed
|
||||
|
||||
<list of pre-canned files the verification expects>
|
||||
|
||||
## Source citations
|
||||
|
||||
<paths in PT repo that explain module behavior or guards>
|
||||
|
||||
## Ceiling
|
||||
|
||||
<observed PASS rate / total>
|
||||
|
||||
## Don'ts
|
||||
|
||||
<list of common mistakes>
|
||||
```
|
||||
|
||||
## Hygiene
|
||||
|
||||
- **Keep each profile under ~10 KB.** If it grows beyond that, the module has too many quirks — escalate to maintainer review of the upstream checklist.
|
||||
- **The recipe table is a control/observation MAP, not an answer key.** Columns are *Capability → Drive (control/key) → Observe*. **Do NOT bake in concrete Search/Replace inputs or expected-output assertions** — those are per-test-case answers that go stale when a checklist item changes and let the agent copy without understanding. The agent designs inputs + assertions at runtime from the actual checklist item.
|
||||
- **Tables are capability-keyed, NOT line-keyed.** Upstream checklist line numbers (`L<n>`) **must not appear** in the profile — they drift between releases (items added/removed/reordered) and turn the table into a silent mismatch trap. PT-source-code file:line citations (e.g. `dllmain.cpp:73`) ARE allowed; they're version-pinned and serve a different purpose.
|
||||
- **Cite source-code line numbers** where module behavior surprises (e.g. CLI guards, debounce timings, fallback chains). Reviewers can verify your claims by reading those lines.
|
||||
- **Update the profile after every verification round**; promote any new technique into the right helper script if it generalizes beyond this module.
|
||||
@@ -1,103 +0,0 @@
|
||||
# File Locksmith — module verification profile
|
||||
|
||||
**PT module**: `File Locksmith` (shows which processes are using selected files/dirs and lets you kill them)
|
||||
**Source**: `<PT-repo>\src\modules\FileLocksmith\` (PT repo)
|
||||
**Settings file (module)**: `%LOCALAPPDATA%\Microsoft\PowerToys\File Locksmith\settings.json` (`{"properties":{"bool_show_extended_menu":{...}}}`) and `file-locksmith-settings.json` (`{"showInExtendedContextMenu":bool}`)
|
||||
**Enable flag**: `%LOCALAPPDATA%\Microsoft\PowerToys\settings.json` → `enabled."File Locksmith"` (general settings; runner-owned)
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\File Locksmith\FileLocksmithUI\Logs\…`
|
||||
**Exes**: UI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.FileLocksmithUI.exe`; CLI = `%LOCALAPPDATA%\PowerToys\FileLocksmithCLI.exe`
|
||||
**Context menu**: Win11 packaged `IExplorerCommand` CLSID `{AAF1E27D-4976-49C2-8895-AAFA743C0A7E}` (sparse pkg `Microsoft.PowerToys.FileLocksmithContextMenu`); legacy `FileLocksmithExt.dll`. Caption resource = "What's using this file?".
|
||||
**Named Event / DSC**: no Named Event. DSC resource `microsoft.powertoys.FileLocksmith.settings` exists (controls module settings, not the master enable flag).
|
||||
**No global hotkey** — entry is the Explorer context menu only.
|
||||
|
||||
## The two back-doors that make this module fully drivable (no Explorer needed)
|
||||
|
||||
### 1. `FileLocksmithCLI.exe` — deterministic engine ground-truth (PREFER for assertions)
|
||||
Accepts paths as args; `--json`, `--kill`, `--wait`, `--timeout`. Uses the **same** `find_processes_recursive` engine as the UI.
|
||||
```powershell
|
||||
$cli = "$env:LOCALAPPDATA\PowerToys\FileLocksmithCLI.exe"
|
||||
& $cli "<file|dir|drive>" --json | ConvertFrom-Json # {processes:[{pid,name,user,files[]}]}
|
||||
& $cli "<path>" --kill # terminate lockers (== End task)
|
||||
```
|
||||
Detection = open **File handles** + **loaded modules** under the path (exact file, exact dir, dir-prefix recursive). `FileLocksmith.cpp:18-113`.
|
||||
|
||||
### 2. `last-run.log` IPC + launch UI — exercises the REAL UI code path
|
||||
The context-menu handler writes selected paths to `…\File Locksmith\last-run.log` then launches the UI; the UI reads them in `MainViewModel()` ctor. Reproduce it:
|
||||
```powershell
|
||||
# UTF-16LE, each path + WCHAR \n, trailing empty-line terminator, NO BOM
|
||||
function Write-LastRun([string[]]$Paths){
|
||||
$f="$env:LOCALAPPDATA\Microsoft\PowerToys\File Locksmith\last-run.log"; $ms=[IO.MemoryStream]::new()
|
||||
foreach($p in $Paths){$b=[Text.Encoding]::Unicode.GetBytes($p);$ms.Write($b,0,$b.Length);$n=[Text.Encoding]::Unicode.GetBytes("`n");$ms.Write($n,0,$n.Length)}
|
||||
$n=[Text.Encoding]::Unicode.GetBytes("`n");$ms.Write($n,0,$n.Length);[IO.File]::WriteAllBytes($f,$ms.ToArray())
|
||||
}
|
||||
Write-LastRun @("C:\path\to\file"); Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.FileLocksmithUI.exe"
|
||||
```
|
||||
Source: `ExplorerCommand.cpp:182-227`, `dllmain.cpp:94-159`, `IPC.cpp`, `NativeMethods.cpp:62-97`.
|
||||
|
||||
### UI selectors (winapp ui)
|
||||
- Window title: `Administrator: File Locksmith` (elevated) vs `File Locksmith` (non-elevated).
|
||||
- Header button = the path label (`btn-<pathname>-…`); top-right `btn-…` = **Reload** (tooltip "Reload").
|
||||
- Per-row End task button = `btn-…` (parent of `lbl-endtask-…`); invoke it (`InvokePattern`).
|
||||
- `RestartAsAdminBtn` = shield icon, **visible only when non-elevated** (`MainPage.xaml:72-82`).
|
||||
- `ProcessesListView` is virtualized; use `winapp ui scroll ProcessesListView --direction down/up`.
|
||||
|
||||
## Recipes — a control/observation map, NOT a per-test-case answer key
|
||||
|
||||
> Maps each capability to **how to drive it** and **where the result shows**. No canned process counts / paths / assertions — design those at runtime from the actual checklist item.
|
||||
|
||||
| # | Capability | Drive (entry-path / control) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | A locked file lists all its locking processes | CLI + UI on one locked file (with multiple lockers) | each locker shows as a ListItem |
|
||||
| 2 | "End task" kills the locker and de-lists it | `winapp ui invoke` the End-task button | locker PID dies + row removed |
|
||||
| 3 | Reload rediscovers a locker started after the UI opened | start a new locker → invoke Reload | the new locker appears |
|
||||
| 4 | Closing a locker externally auto-removes it | external `Stop-Process` on a locker | auto-delisted via `WatchProcess`; empty state shown |
|
||||
| 5 | Directory path finds lockers recursively | CLI/UI with a directory path | lockers inside the tree are listed |
|
||||
| 6 | Drive root lists many lockers without crashing | CLI/UI with a drive root | large list renders; no crash |
|
||||
| 7 | Non-elevated FL does NOT see the elevated runner | run CLI/UI non-elevated via scheduled task `RunLevel Limited` | `PowerToys.exe` absent (medium-IL FL can't see elevated procs) |
|
||||
| 8 | "Restart as administrator" surfaces elevated-only lockers | non-elev UI shows the button; elevated run shows them | elevated run lists `PowerToys.exe` (UAC consent click NOT automatable) |
|
||||
| 9 | Scrolling a large list doesn't crash | UI on a drive root + `winapp ui scroll` | process alive + responsive after scroll |
|
||||
| 10 | Disabling FL removes the Explorer context-menu entry | Settings toggle Off (winapp ui) | `enabled."File Locksmith"`→false; `GetState→ECS_HIDDEN` (source) |
|
||||
|
||||
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control and design your own inputs + assertions. If no row matches, drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
|
||||
|
||||
## Common BLOCKED traps (avoid)
|
||||
- **Don't BLOCK the lock-detection / End-task / scroll-doesn't-crash items as "needs a real installer / right-click"** — both the CLI and the `last-run.log`+UI back-door fully drive them with any locked file. The context menu literally just writes `last-run.log` + launches the same exe.
|
||||
- **Launching the UI from an elevated shell makes it elevated** (title "Administrator: …") and hides the `RestartAsAdminBtn`. To test the non-elevated case (Recipes 7-8), launch via a **scheduled task with `-RunLevel Limited` / LogonType Interactive** — it lands on the user's desktop at medium IL.
|
||||
- **`set-value` does fire the Reload** here (TogglePattern/Invoke work); no TextChanged gotchas.
|
||||
|
||||
## Elevation semantics (non-elevated FL invisibility — Recipes 7-8 core)
|
||||
A medium-IL File Locksmith can't `DuplicateHandle`/read modules of higher-integrity processes, so the **elevated PowerToys.exe runner is invisible** to a non-elevated FL and **visible** to an elevated FL (which also calls `SetDebugPrivilege`, `App.xaml.cs:53-61`). Per-user installs put PowerToys under `%LOCALAPPDATA%\PowerToys`, not "Program Files" — use the PT install dir as the stand-in and note the caveat.
|
||||
|
||||
## Context-menu disable gate (Recipe 10)
|
||||
`GetEnabled()` reads general `enabled."File Locksmith"` (`Settings.cpp:53-77`). When false: Win11 `GetState→ECS_HIDDEN` (`dllmain.cpp:81-84`); legacy `QueryContextMenu→E_FAIL` (`ExplorerCommand.cpp:116-119`). Disabling does NOT unregister the sparse package (stays `Status Ok`) — the entry is hidden dynamically. The packaged `IExplorerCommand` is **not** enumerable via Shell `Verbs()` and **not** `CoCreate`-able from a non-Explorer host (`REGDB_E_CLASSNOTREGISTERED`), so the pixel-level render is the only un-automatable bit (`BLK-VISUAL-RENDER` if you need it).
|
||||
|
||||
## Fixture files needed
|
||||
None pre-canned. Create a temp file and lock it with a helper process (pwsh holding `File.Open(path, OpenOrCreate, Read, ReadWrite)` so multiple lockers coexist).
|
||||
|
||||
## Source citations
|
||||
- `FileLocksmithLibInterop\FileLocksmith.cpp:18-113` — `find_processes_recursive` (handles + modules, recursive).
|
||||
- `FileLocksmithLibInterop\NativeMethods.cpp:62-140` — `ReadPathsFromFile`, `StartAsElevated` (runas/--elevated).
|
||||
- `FileLocksmithLib\IPC.cpp`, `Constants.h` — last-run.log format/path.
|
||||
- `FileLocksmithUI\…\MainViewModel.cs:80-183` — load/EndTask/WatchProcess/RestartElevated.
|
||||
- `FileLocksmithUI\…\MainPage.xaml` — control layout + RestartAsAdminBtn visibility.
|
||||
- `FileLocksmithExt\ExplorerCommand.cpp`, `FileLocksmithContextMenu\dllmain.cpp`, `FileLocksmithLib\Settings.cpp` — enable gate.
|
||||
|
||||
## Ceiling
|
||||
10/10 PASS observed (2026-06-08). The lock-detection / End-task / refresh / drive-scroll items cleanly driven; the Restart-as-admin item PASS-with-caveat (UAC consent click not automatable; outcome verified). **The disable-removes-menu item PASS — behaviorally verified** by a real Explorer right-click: with FL enabled the Win11 menu shows `MenuItem "Unlock with File Locksmith"`; after disabling in Settings the same right-click menu no longer shows it (no Explorer restart needed — `GetState` re-reads `enabled."File Locksmith"` live). NB the **shipped caption is "Unlock with File Locksmith"**, not the checklist's "What's using this file?". The right-click test needs an **unlocked interactive desktop** (a 4-hour idle auto-lock makes `GetForegroundWindow()=0` → `BLK-ENV`).
|
||||
|
||||
## Real right-click verification (Recipe 10) — works on an unlocked desktop
|
||||
**Use the shared flow: `references/explorer-context-menu-flow.md` + `scripts/pt-explorer-contextmenu.ps1`.** FL's caption is **"Unlock with File Locksmith"**. Quick version:
|
||||
```powershell
|
||||
. "$skill\scripts\pt-explorer-contextmenu.ps1"
|
||||
$menu = Open-PtExplorerContextMenu -ExplorerHwnd $hwnd -FileName 'target.txt' # synthetic right-click (+retry)
|
||||
# present/absent assertion:
|
||||
(Get-PtContextMenuItems -MenuHwnd $menu) -contains 'Unlock with File Locksmith' # true enabled / false disabled
|
||||
# real launch (UIA invoke by name):
|
||||
Invoke-PtContextMenuItem -MenuHwnd $menu -ItemName 'Unlock with File Locksmith' # -> launches PowerToys.FileLocksmithUI.exe (non-elevated)
|
||||
# Toggle FL off in Settings, re-open menu, assert the caption is gone. No Explorer restart needed (GetState re-reads live).
|
||||
```
|
||||
|
||||
## Don'ts
|
||||
- Don't expect `Shell.Application.Verbs()` to show the FL entry — it's a Win11 packaged command, invisible to classic verbs.
|
||||
- Don't kill processes by name; use `Stop-Process -Id <pid>`.
|
||||
- Don't forget to restore `enabled."File Locksmith"=true` and close test-spawned UI/Settings after the disable-removes-menu test.
|
||||
@@ -1,87 +0,0 @@
|
||||
# Image Resizer — module verification profile
|
||||
|
||||
**PT module**: `Image Resizer` (resize images via Explorer right-click; WinUI 3 GUI + headless CLI)
|
||||
**Source**: `<PT-repo>\src\modules\imageresizer\` (PT repo)
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\Image Resizer\settings.json` (PowerToys-wrapper shape: `{ "properties": { "imageresizer_*": { "value": … } }, "name": "Image Resizer", "version": "1" }`). A legacy `sizes.json` mirrors `imageresizer_sizes`; `image-resizer-settings.json` is `{}` (unused).
|
||||
**Enable flag**: `%LOCALAPPDATA%\Microsoft\PowerToys\settings.json` → `enabled."Image Resizer"` (runner-owned; restart runner after toggling).
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\Image Resizer\Logs\…`
|
||||
**Exes**: GUI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.ImageResizer.exe`; **CLI = `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.ImageResizerCLI.exe`**.
|
||||
**Context menu**: Win11 packaged `IExplorerCommand` (sparse pkg `ImageResizerContextMenuPackage.msix`, dllmain.cpp) + legacy classic `ImageResizerExt.dll` (`dll/ContextMenuHandler.cpp`). **Shipped caption = "Resize with Image Resizer"** (`IDS_IMAGERESIZER_CONTEXT_MENU_ENTRY`; checklist's "Resize images" is STALE).
|
||||
**No global hotkey / no Named Event / no DSC for the engine** — entry is the Explorer menu (or direct exe launch).
|
||||
|
||||
## The back-door that makes this module ~fully drivable (no Explorer needed)
|
||||
|
||||
### `PowerToys.ImageResizerCLI.exe` — the deterministic engine (PREFER for all resize-behavior items)
|
||||
Shares the exact `ResizeBatch.FromCliOptions` → `ResizeBatch.ProcessAsync` → `ResizeOperation.ExecuteAsync` engine as the GUI (`ui/Cli/ImageResizerCliExecutor.cs:76-108`, `App.xaml.cs:102`). Reads live `Image Resizer\settings.json` then applies CLI overrides (`CliSettingsApplier.cs`).
|
||||
```
|
||||
--width/-w --height/-h --unit/-u {Centimeter|Inch|Percent|Pixel} --fit/-f {Fill|Fit|Stretch}
|
||||
--size <presetIndex> --shrink-only --replace/-r --ignore-orientation --remove-metadata
|
||||
--quality/-q --keep-date-modified --filename/-n "<%1..%6>" --destination/-d <dir>
|
||||
--show-config --help <files…> (also accepts \\.\pipe\<name> and stdin file list)
|
||||
```
|
||||
`--show-config` dumps the effective settings (great pre/post check). `-d <dir>` keeps outputs isolated. Assert output dimensions with `[System.Drawing.Image]::FromFile(p)`.
|
||||
**Caveat**: `--ignore-orientation`/`--shrink-only`/`--replace`/`--keep-date-modified`/`--remove-metadata` are *flags* — they can only set the value **true**; to test the **false** case, temporarily edit `settings.json` (back up + restore).
|
||||
|
||||
### Direct GUI launch — for the two UI-only items (gif warning, size-list populated)
|
||||
`Start-Process PowerToys.ImageResizer.exe "<file>"` opens the window pre-loaded (argv/stdin via `ResizeBatch.FromCommandLine`). Behaviorally identical to the context-menu launch (`dllmain.cpp:219-245` just writes a pipe + launches the same exe). Then drive with `winapp ui`:
|
||||
- Size selector: `SizeComboBox` → `winapp ui invoke SizeComboBox -w <hwnd>` to expand, then `inspect` shows `itm-<name>-XXXX` ListItems.
|
||||
- Gif warning: `Message Text "Gif files with animations may not be correctly resized."` InfoBar, bound to `ViewModel.HasGifFiles` (set when any file ends `.gif`).
|
||||
|
||||
## Engine facts (verified from source — cite these for the resize items)
|
||||
- `ResizeFit`: **Fill=0, Fit=1, Stretch=2** (`ResizeFit.cs`). Fit=`min(scaleX,scaleY)`; Fill=`max`+centered-crop; Stretch=independent (`ResizeOperation.cs:449-498`).
|
||||
- `ResizeUnit`: **Centimeter=0, Inch=1, Percent=2, Pixel=3** (`ResizeUnit.cs`). Inch=`v*dpi`; cm=`v*dpi/2.54`; Percent=`v/100*orig`; Pixel=`v` (`ResizeSize.cs:109-123`). **Outputs depend on image DPI** — read actual DPI and compute expectations from it (a 120-DPI fixture gives 10cm→472px, 4in→480px).
|
||||
- Filename `%1..%6` → original-name, size-name, selected-W, selected-H, **output**-W, **output**-H (`Settings.cs:229-239`, `ResizeOperation.cs:593-601`).
|
||||
- ShrinkOnly: if target scale>1, returns `noTransformNeeded` (file copied unchanged) (`ResizeOperation.cs:462-475`).
|
||||
- KeepDateModified: `SetLastWriteTimeUtc(out, GetLastWriteTimeUtc(src))` (`ResizeOperation.cs:146-149`).
|
||||
- Replace: `File.Replace(out, src, backup)` then recycle backup — no copy left (`ResizeOperation.cs:151-156`).
|
||||
- IgnoreOrientation swap: gated by `IgnoreOrientation && !HasAuto && Unit != Percent` (`ResizeOperation.cs:419-444`).
|
||||
|
||||
## Recipes — a control/observation map, NOT a per-test-case answer key
|
||||
|
||||
> Maps each capability to **which control/CLI flag drives it** and **where the result shows**. CLI flag *names* and fit-mode/unit/field enumerations are stable IR knowledge and stay; concrete flag *values*, fixtures, and expected outputs are per-test-case — design those at runtime.
|
||||
|
||||
| # | Capability | Drive (control / CLI flag) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | Module disabled → context-menu entry absent | toggle `enabled` off + restart runner; synthetic menu (only valid observer) | "Resize with Image Resizer" absent. Gate: `dllmain.cpp:87-91` (ECS_HIDDEN), `ContextMenuHandler.cpp:70-71,383-385`. Locked desktop → BLK-ENV |
|
||||
| 2 | Module enabled → entry present (modern + classic), click launches the GUI | synthetic menu + invoke | `Get-PtContextMenuItems` shows "Resize with Image Resizer"; classic "Show more options" too; invoke → `PowerToys.ImageResizer.exe` launches |
|
||||
| 3 | Remove a built-in size / add a custom size | edit `imageresizer_sizes` (INTEGER Ids!) + launch GUI | `SizeComboBox` reflects the edit (removed gone, custom present) |
|
||||
| 4 | Resize one / multiple files end-to-end | CLI `--size <id> [files…]` | outputs at the size's Fit dimensions |
|
||||
| 5 | GIF animation warning on `.gif` input | GUI on a `.gif` | warning InfoBar present (`winapp ui inspect`) |
|
||||
| 6 | Fit modes (Fill / Fit / Stretch) | CLI `--width --height --fit <mode>` | output shape matches the mode (crop / letterbox / exact) |
|
||||
| 7 | Unit conversion (cm / inch / percent / pixel) | CLI `--unit <u>` | output px = unit converted at the image's DPI |
|
||||
| 8 | Custom filename format (`%1`..`%6` fields) | CLI `--filename <fmt>` | output filename follows the format fields |
|
||||
| 9 | "Keep date modified" | CLI `--keep-date-modified` | output mtime == source mtime (control: without the flag, differs) |
|
||||
| 10 | "Shrink only" | CLI `--shrink-only` | an already-small image is untouched (control: a large one still shrinks) |
|
||||
| 11 | "Replace original" | CLI `--replace` | original replaced in place; no `(name) (1)` copy |
|
||||
| 12 | "Ignore orientation" | settings (false) vs flag (true) | on a portrait target over a landscape image: false→no W/H swap, true→swap |
|
||||
|
||||
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control/flag and design your own inputs + assertions. If no row matches, drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
|
||||
|
||||
## Common BLOCKED traps (avoid)
|
||||
- **Don't mark the resize-behavior items BLOCKED for "needs a real right-click".** The CLI fully drives them with the identical engine; the menu is just the trigger (prove the menu→launch wiring once with the golden path in Recipe 2).
|
||||
- **PowerShell `ConvertTo-Json` writes computed numbers as doubles (`"Id": 3.0`)** → `System.Text.Json` rejects `imageresizer_sizes` and the app silently falls back to the 4 built-in default presets (Small/Medium/Large/Phone). Cast Ids to `[int]` or regex-strip `\.0`. This bit the remove-size-add-custom item (Recipe 3) the first time.
|
||||
- **cm/inch outputs depend on the fixture's DPI, not 96.** System.Drawing saves at the session display DPI (here 120). Compute expectations from the actual DPI.
|
||||
- **Caption is "Resize with Image Resizer", not the checklist's "Resize images"** (both menus). Hard-match the real caption.
|
||||
- **Idle auto-lock = BLK-ENV for the disabled-absent + enabled-present items (Recipes 1-2)** (synthetic right-click needs foreground). Disable lock/sleep before the run (`references/environment-setup.md`).
|
||||
|
||||
## Fixture files needed
|
||||
None pre-canned. Generate with `System.Drawing`: a landscape (e.g. 1200×800) and portrait (800×1200) JPEG, a small (100×100) PNG, a square (400×400) PNG, a `.gif` (single frame is fine — the warning is extension-based), and 3 identical images for the multi-file batch.
|
||||
|
||||
## Source citations
|
||||
- `ui/Cli/ImageResizerCliExecutor.cs`, `ui/Models/CliOptions.cs`, `ui/Cli/CliSettingsApplier.cs`, `ui/Cli/Commands/ImageResizerRootCommand.cs` — CLI surface + engine reuse.
|
||||
- `ui/Models/ResizeOperation.cs:419-501,572-617,146-156` — dimension math, filename, keep-date, replace.
|
||||
- `ui/Models/ResizeSize.cs:78-124`, `ResizeFit.cs`, `ResizeUnit.cs` — unit/fit math + enum order.
|
||||
- `ui/Properties/Settings.cs:65,105-111,229-239,431-552` — paths, defaults, JSON property names, FileNameFormat.
|
||||
- `ImageResizerContextMenu/dllmain.cpp:49,79-133,219-245,284` — modern menu title, enable+image gate, launch, caption.
|
||||
- `dll/ContextMenuHandler.cpp:21,46-48,70-71,383-385` — classic menu caption + enable gate.
|
||||
- `ui/ViewModels/InputViewModel.cs:76,143`, `ImageResizerXAML/Views/InputPage.xaml:293-299`, `Strings/en-us/Resources.resw:148-149` — gif warning.
|
||||
|
||||
## Ceiling
|
||||
**18/18 PASS** observed (2026-06-09). All 14 resize-behavior items + the enabled-entry-present-in-both-menus + the remove-size + the gif-warning items cleanly driven via CLI/GUI. The disabled-entry-absent case (in both modern + classic menus, with sibling entries remaining and the entry returning on re-enable) verified live once the desktop was unlocked. NB: an idle auto-lock will turn the menu-presence Recipes 1-2 into BLK-ENV — disable lock/sleep up front (`references/environment-setup.md`).
|
||||
|
||||
## Don'ts
|
||||
- Don't expect `Shell.Application.Verbs()` to list the entry — it's a Win11 packaged command (classic verbs are blind; `CoCreate` → `REGDB_E_CLASSNOTREGISTERED`).
|
||||
- Don't hardcode 96 DPI for cm/inch math.
|
||||
- Don't write preset Ids as JSON doubles.
|
||||
- Don't kill processes by name; use `Stop-Process -Id <pid>`.
|
||||
- Don't forget to restore `enabled."Image Resizer"=true` + restart runner, and revert any `settings.json`/`sizes.json` edits.
|
||||
@@ -1,77 +0,0 @@
|
||||
# New+ — module verification profile
|
||||
|
||||
**PT module**: `NewPlus` (Explorer right-click → "New+" submenu that creates files/folders from a user templates folder)
|
||||
**Source**: `<PT-repo>\src\modules\NewPlus\` (shell ext) + `<PT-repo>\src\settings-ui\Settings.UI\ViewModels\NewPlusViewModel.cs` (Settings UI)
|
||||
**Module-owned settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\settings.json` — **folder is `NewPlus`, NOT `New`** (matches SKILL.md pitfall #18 table). Keys: `HideFileExtension`, `HideStartingDigits`, `TemplateLocation`, `ReplaceVariables`, `BuiltInNewHidePreference`.
|
||||
**Templates folder (default)**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\Templates` (per `TemplateLocation`)
|
||||
**Default-templates source**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\Assets\NewPlus\Templates` (also `%ProgramFiles%\PowerToys\...` on machine installs)
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\NewPlus\NewPlus.ShellExtension\Logs\v<ver>\log_<date>.log`
|
||||
**Packaged command**: sparse MSIX `Microsoft.PowerToys.NewPlusContextMenu`; command CLSID `{FF90D477-E32A-4BE8-8CC5-A502A97F5401}`
|
||||
**Named Event**: none. **DSC**: n/a.
|
||||
|
||||
> Read **`../references/explorer-context-menu-flow.md` first** — New+ is a Win11 packaged-IExplorerCommand context-menu module; the menu can only be eyeballed via a real synthetic right-click on an **unlocked interactive desktop**. On a locked/RDP-minimized desktop (`Test-PtDesktopInteractive=False`) all "menu appears / template appears / hidden-caption" assertions are BLK-ENV / BLK-VISUAL-RENDER, not product FAILs.
|
||||
|
||||
## Entry-paths (try in order)
|
||||
|
||||
### 1. Enable/disable + registration gate (menu presence/absence) — headless-safe
|
||||
Flip `enabled.NewPlus` in master `settings.json` + `Restart-PtRunner`, **or** toggle the Settings switch (below). Observe the gate, no foreground needed:
|
||||
- CLSID registered ⇒ `Test-Path "HKCU:\Software\Classes\CLSID\{FF90D477-E32A-4BE8-8CC5-A502A97F5401}"` is `True` (enabled) / `False` (disabled).
|
||||
- Log lines `New+ context menu registered` / `... unregistered` + `Runtime registration completed for CLSID ...`.
|
||||
- Sparse package stays `Status Ok` even when disabled (hidden dynamically — SKILL.md pitfall #17).
|
||||
|
||||
### 2. Settings UI toggles via UIA invoke — headless-safe, **required for template auto-copy**
|
||||
`Start-Process …\PowerToys.exe --open-settings=NewPlus`, then `winapp ui invoke <btn> -w <settingsHwnd>`:
|
||||
- Enable toggle: `btn-new-248c` (under `NewPlusEnableToggle`) — **the enable transition runs `CopyTemplateExamples`** (Settings-UI side, `NewPlusViewModel.IsEnabled` setter). A master-`settings.json` flip + runner restart does **NOT** copy templates.
|
||||
- `btn-hidethefileexte-24a0` (Hide file extension), `btn-hideleadingdigi-24a8` (Hide leading digits). AutomationIds carry a per-session suffix — re-`inspect` to get the live id.
|
||||
|
||||
### 3. Synthetic right-click on the folder **BACKGROUND** (the menu-render observer) — needs unlocked desktop
|
||||
New+ lives in the folder-background ("New") menu, **not** a file's context menu — so `pt-explorer-contextmenu.ps1`'s `Open-PtExplorerContextMenu` (which right-clicks a *file item*) is the wrong entry. Right-click an **empty area of the file list** instead, then expand the `New+` submenu (a separate popup window one level deeper):
|
||||
```powershell
|
||||
# force-foreground the CabinetWClass window, GetWindowRect, RightClick at ~45% width / 68% height (empty list area)
|
||||
# -> main bg menu popup (PopupWindowSiteBridge). Then:
|
||||
$np = (winapp ui search 'New+' -w $mainMenuHwnd --json).matches | ? type -eq MenuItem | select -First 1
|
||||
winapp ui invoke $np.selector -w $mainMenuHwnd # expands the New+ submenu
|
||||
# the submenu is the PopupWindowSiteBridge popup that contains 'Open templates' but NOT 'Sort by'
|
||||
# enumerate its MenuItems (templates are 1:1 with the Templates-folder entries) / invoke one by name
|
||||
```
|
||||
Template items render with **caption transforms applied** (HideFileExtension strips `.txt`; HideStartingDigits strips `01. `). Selecting a template creates it in the current folder + enters rename mode. BLK-ENV only if `Test-PtDesktopInteractive` is False. **No Explorer restart needed** for setting A/B — the handler re-reads `NewPlus\settings.json` on each menu build.
|
||||
|
||||
## Recipes — a control/observation map, NOT an answer key
|
||||
|
||||
| # | Capability | Drive (control / settings key) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | Menu entry present when enabled | enable (master flag + restart, or `btn-new-248c`) | CLSID registered in `HKCU\…\CLSID`, log `context menu registered`; *visible submenu* = synthetic menu only (BLK-VISUAL-RENDER if locked) |
|
||||
| 2 | Menu entry absent when disabled | disable | CLSID absent, log `context menu unregistered`; package still `Status Ok` |
|
||||
| 3 | Templates folder created empty | shell ext `create_folder_if_not_exist(root)` on menu build (delete folder → right-click) | folder recreated **empty** — needs synthetic menu (BLK-ENV if locked) |
|
||||
| 4 | Default templates copied when empty | `CopyTemplateExamples` on Settings-UI **enable** transition (`btn-new-248c` off→on) while folder empty | Templates folder repopulated from install Assets (filesystem — headless-safe) |
|
||||
| 5 | A template (file/folder) shows + creates on select | put item in Templates folder; select it in the New+ submenu | submenu item (1:1 with dir entries) + `SHFileOperation FO_COPY` to target — synthetic menu only |
|
||||
| 6 | Hide file extension | `HideFileExtension` / `btn-hidethefileexte-24a0` | strips ext from **menu caption only** (`get_menu_title`, `show_extension=false`); created file keeps ext — caption is BLK-VISUAL-RENDER if locked |
|
||||
| 7 | Hide starting digits/spaces/dots | `HideStartingDigits` / `btn-hideleadingdigi-24a8` | strips leading digits+separator from **both** menu caption and **created filename** (`remove_starting_digits_from_filename` via `get_menu_title` + `copy_template`); needs a digit-prefixed template + render |
|
||||
|
||||
> Verify a setting actually drives behavior by editing the **module-owned** `NewPlus\settings.json` (not the PT-store mirror) and relaunching; the Settings toggles round-trip into this same file.
|
||||
|
||||
## Common BLOCKED traps
|
||||
- **Master-flip + runner restart does not copy default templates** — that's a Settings-UI action (`NewPlusViewModel.IsEnabled`). Use the UIA toggle for any template-auto-copy item.
|
||||
- **Menu render is invisible without a real right-click** — packaged command is not `CoCreate`-able (`REGDB_E_CLASSNOTREGISTERED`) and not in classic `Shell.Application.Verbs()`. Locked desktop ⇒ BLK-ENV; do not substitute a CLI/back-door (there isn't one, and it'd be a false PASS).
|
||||
- **No template-count observable** — `saved_number_of_templates` is an in-memory static (`new_utilities.cpp`), not registry/log.
|
||||
|
||||
## Fixture files needed
|
||||
- A plain file (e.g. `test.txt`) and a folder-with-files to drop into Templates (template-appears items).
|
||||
- A digit-prefixed template (e.g. `01. Test.txt`) to exercise Hide-starting-digits.
|
||||
|
||||
## Source citations
|
||||
- `src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp` — `get_menu_title` (hide-extension), `remove_starting_digits_from_filename`, `copy_object_to`.
|
||||
- `src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h` — `copy_template`, `create_folder_if_not_exist`, `get_newplus_setting_hide_*`, `register_msix_package`.
|
||||
- `src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp` — `create_folder_if_not_exist(root)` + template enumeration.
|
||||
- `src/settings-ui/Settings.UI/ViewModels/NewPlusViewModel.cs` — `CopyTemplateExamples` (creates dir; copies examples only when files==0 && dirs==0), called from `IsEnabled` setter / `OpenNewTemplateFolder` / `DashboardViewModel`.
|
||||
|
||||
## Ceiling
|
||||
- Unlocked interactive desktop: **9/9 PASS** (verified 2026-06-18 via background-menu synthetic right-click + submenu expansion).
|
||||
- Locked/non-interactive desktop: ~3/9 (registration-gate present/absent + template auto-copy); menu-render/select items fall to BLK-ENV/BLK-VISUAL-RENDER — re-run after unlocking.
|
||||
|
||||
## Don'ts
|
||||
- Don't edit `…\PowerToys\New\settings.json` — wrong path; the file is under `NewPlus\`.
|
||||
- Don't use `Open-PtExplorerContextMenu` (file-item right-click) for New+ — it's the folder **background** ("New") menu; right-click empty list space instead.
|
||||
- Don't forget to **expand the `New+` submenu** (invoke it) before enumerating templates — they live one popup deeper than the main menu.
|
||||
- Don't mark menu-render items as product FAIL on a locked desktop — it's BLK-ENV.
|
||||
- Don't restart Explorer to apply a setting change — the handler re-reads `NewPlus\settings.json` per menu build.
|
||||
@@ -1,117 +0,0 @@
|
||||
# Peek — module verification profile
|
||||
|
||||
**PT module**: `Peek` (file previewer activated on Ctrl+Space with Explorer file selected)
|
||||
**Source**: `<PT-repo>\src\modules\Peek\` (PT repo)
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\Peek\settings.json`
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\Peek\Logs\v<ver>\log_<date>.log`
|
||||
**Exes**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.Peek.UI.exe`
|
||||
**Default hotkey**: `Ctrl+Space` (modifiers=`ctrl`, code=32; see `settings.json` → `ActivationShortcut`)
|
||||
**Named Event**: `Local\ShowPeekEvent` (friendly name: `Peek.Show` in `pt-shared-events.ps1` catalog)
|
||||
**DSC resource**: `Microsoft.PowerToys/PeekSettings`
|
||||
|
||||
## Three entry-paths (try in order)
|
||||
|
||||
### 1. CLI back-door — fastest, no Explorer needed
|
||||
```powershell
|
||||
Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.Peek.UI.exe" -ArgumentList "<file>"
|
||||
```
|
||||
**Source**: `Peek.UI\PeekXAML\App.xaml.cs:106-134` — when last arg is not int (=runner PID) and is an existing file, it sets `_launchedFromCli=true`, builds `SelectedItemByPath`, calls `OnShowPeek()`. Bypasses hotkey + Explorer foreground.
|
||||
|
||||
**Use for**: single-file previewer rendering tests (Recipes 1-2) and the CLI-accepts-path assertion (Recipe 8).
|
||||
|
||||
**Cannot use for**: navigation tests (Recipes 4-7, 10-11) — source has `if (_isFromCli) return;` guard that disables arrow navigation, and CLI mode spawns a fresh process every call (no pin-state-across-reopen).
|
||||
|
||||
### 2. Shell.Application COM + Ctrl+Space — Explorer-driven, supports navigation
|
||||
This is the canonical "do what a real user would do" path that drives all the navigation/pin tests.
|
||||
|
||||
```powershell
|
||||
# Dot-source helpers first
|
||||
. "$skill\scripts\pt-explorer-com.ps1"
|
||||
. "$skill\scripts\pt-sendinput-chord.ps1"
|
||||
|
||||
# Set up multi-file selection in Explorer + trigger Peek in one call:
|
||||
$peekHwnd = Invoke-PtPeekWithExplorerSelection `
|
||||
-FolderPath 'D:\fixtures' `
|
||||
-FileNames 'test-markdown.md','test-html.html','test-source.cs'
|
||||
|
||||
# Now Peek is open over a 3-file IShellItemArray. Test:
|
||||
winapp ui invoke 'PinButton' -w $peekHwnd # pin
|
||||
# (move window via SetWindowPos)
|
||||
Send-PtChord -Key 0x27 # Right arrow → switch file
|
||||
# verify the pinned position stuck
|
||||
```
|
||||
|
||||
**Use for**: pin behavior, multi-file navigation, file switching (Recipes 4-7, 10-11).
|
||||
|
||||
**Requires**: interactive desktop session (`Test-PtInteractiveDesktop` must show both `ForegroundOk=True` and `ShellComOk=True`).
|
||||
|
||||
### 3. Named Event signal — quick smoke
|
||||
```powershell
|
||||
Invoke-PtSharedEvent -Name 'Peek.Show'
|
||||
```
|
||||
Wakes the resident Peek process (different from CLI back-door — respects current Explorer foreground selection). Used by some framework tests for the "Peek is enabled and listening" assertion.
|
||||
|
||||
## Recipes — a control/observation map, NOT a per-test-case answer key
|
||||
|
||||
> Maps each Peek *capability* to **how to drive it** and **where the result shows**. It does NOT prescribe concrete fixtures/coords/inputs or expected values — design those at runtime from the actual checklist item. Only a real UI/behavior change should force an edit here.
|
||||
|
||||
| # | Capability | Drive (control / command) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | File-type previewer renders (image / text+code / markdown / PDF / HTML / archive / unsupported) | `Peek.UI.exe <fixture>` (entry-path 1) → `winapp ui inspect -w <hwnd> --depth 7` | the type's previewer node present (`ImagePreview Image`; `PreviewBrowser Pane` for dev/text/md/HTML; archive tree for zip; File-Type/Size/Date view for unsupported). Prefer `winapp ui search` for an in-fixture marker over OCR |
|
||||
| 2 | "Open with default app" via button | `winapp ui invoke LaunchAppButton` | a new editor process/window for `<file>` appears (PID diff) |
|
||||
| 3 | "Open with default app" via Enter | `Assert-PtForegroundOrAbort` → `Send-PtChord -Key <Enter>` | same as #2 |
|
||||
| 4 | Pin keeps window position when switching files | Shell COM + Ctrl+Space (entry-path 2) → `winapp ui invoke PinButton` → move window → navigate to next file | window stays at the pinned coordinates |
|
||||
| 5 | Pin position persists across close + reopen | pinned → Esc to close (graceful — **don't `Stop-Process`**, it bypasses the pin-save handler) → reopen via Shell COM + Ctrl+Space | new window opens at the same pinned coordinates |
|
||||
| 6 | Unpin releases the lock; switching file reverts to default | `winapp ui invoke PinButton` again (unpin) → navigate | window moves to the default position |
|
||||
| 7 | Unpinned reopen uses default position | unpinned → Esc-close → reopen | new window at default, not the stale pinned coords |
|
||||
| 8 | `Peek.UI.exe <file>` CLI opens Peek | entry-path 1 | covered by #1 across file types |
|
||||
| 9 | Concurrent Peek sessions don't crash/interfere | launch `Peek.UI.exe` several times on different files, leaving windows open | each spawns its own process/window; no error in `Peek\Logs` |
|
||||
| 10 | Arrow keys cycle between selected files | Shell COM multi-file selection → Ctrl+Space → `Send-PtChord` Right/Left | window title updates to each file in sequence, wraps at the ends |
|
||||
| 11 | Multi-file selection scopes navigation | select a subset of a folder → navigate | only the selected files cycle, not the rest |
|
||||
| 12 | Activation-hotkey reassignment takes effect | edit `Peek\settings.json` `properties.ActivationShortcut` → `Restart-PtRunner` (**not hot-reloaded** — see Gotchas) → press the new chord, then the old chord | new chord opens Peek; old chord does nothing |
|
||||
|
||||
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control and design your own inputs + assertions for *that* item. If no row matches, it's a NEW capability — drive ad-hoc and add a row (capability + control + observation point; no canned inputs).
|
||||
|
||||
|
||||
|
||||
## BLOCKED triage (single source of truth)
|
||||
|
||||
If the agent only tried the CLI back-door and marked the pin / navigation tests BLOCKED → **misdiagnosis**, try entry-path #2 (Shell.Application COM + Ctrl+Space).
|
||||
|
||||
If the agent tried Shell COM + Ctrl+Space and got `GetForegroundWindow()=0` + `SendInput → ACCESS_DENIED (5)` → **environment**, not framework. The session has no attached input desktop (RDP minimized, screen locked, screensaver, etc.). See `SKILL.md` pitfall #13 and `references/environment-setup.md` for the per-scenario table + powercfg setup commands. Mark BLK-ENV with mitigation citation.
|
||||
|
||||
Both traps were observed in 2026-06-08 sign-off runs; preventing both is now the agent's pre-flight job (`pt-session-diagnose.ps1`).
|
||||
|
||||
## Fixture files needed
|
||||
|
||||
Put these in a workspace `fixtures/` folder before starting:
|
||||
- `small-image.png` (any 200x150 PNG)
|
||||
- `Program.cs` (any C# file)
|
||||
- `readme.md` (markdown with H1 + bold + bullet list)
|
||||
- `test-pdf.pdf` (PDF with embedded text "PDF_FIXTURE_OK" + "PDF_MARKER_42")
|
||||
- `page.html` (HTML with `<h1>` containing "HTMLPEEKMARKER")
|
||||
- `archive.zip` (zip containing 1 small text file)
|
||||
- `unsupported.xyz` (any small binary)
|
||||
- 3 differently-sized images for the pin-position tests (e.g. 320x240, 800x600, 1920x1080)
|
||||
|
||||
## Source citations
|
||||
|
||||
- `<PT-repo>\src\modules\Peek\Peek.UI\PeekXAML\App.xaml.cs:106-134` — CLI arg parsing, `_isFromCli` flag, OnShowPeek call.
|
||||
- `<PT-repo>\src\modules\Peek\Peek.UI\PeekXAML\Models\NavigationManager.cs` — `// TODO: implement navigation` + `if (_isFromCli) return;` guards.
|
||||
- `<PT-repo>\src\common\interop\shared_constants.h` — `ShowPeekEvent` name.
|
||||
|
||||
## Ceiling
|
||||
|
||||
**18/18 = 100%** achievable from a normal interactive admin console session (verified 2026-06-08). The change-shortcut item is PASS-able via the settings.json + runner-restart path — see Recipe 12.
|
||||
|
||||
## Peek-specific gotchas
|
||||
|
||||
- **Activation-shortcut is NOT hot-reloaded.** Editing `Peek\settings.json` `ActivationShortcut` and waiting for the file-watcher debounce does nothing — the centralized keyboard hook only re-registers the chord after `Restart-PtRunner`. Restart after the change AND again after restoring.
|
||||
- **PinButton spawns a `PopupHost` teaching-tip.** Invoking `PinButton` pops a small confirmation flyout (≈192x63) titled `PopupHost` that surfaces *first* in `winapp ui list-windows`. A naive "first HWND" regex grabs the popup, not Peek. Match by title suffix `- Peek` (regex like `HWND (\d+): "([^"]*- Peek)"`) and/or cache the original Peek HWND before invoking PinButton.
|
||||
- **Win11 Notepad tabs/session-restore** muddy the "open-with-default-app" tests (Recipes 2-3): the spawned Notepad restores prior tabs, so the foreground Notepad's title may not show your file. Enumerate all Notepad windows and match `"<file> - Notepad"` explicitly.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- **Don't `Stop-Process PowerToys.Peek.UI -Force`** to close Peek between iterations — bypasses the save handler, breaks the pin-state-persistence tests (Recipes 5, 7). Use Esc / `winapp ui invoke CloseButton`.
|
||||
- **Don't assume CLI back-door supports navigation** — it doesn't (`_isFromCli` guard). For nav tests use Shell COM + Ctrl+Space.
|
||||
- **Don't OCR the previewer surface** when UIA already exposes the correct nodes (`ImagePreview`, `PreviewBrowser`, `LaunchAppButton`, `PinButton`). UIA is more reliable than OCR.
|
||||
@@ -1,114 +0,0 @@
|
||||
# PowerRename — module verification profile
|
||||
|
||||
**PT module**: `PowerRename` (bulk-rename UI launched via Explorer context menu on selected files/folders)
|
||||
**Source**: `<PT-repo>\src\modules\PowerRename\` (PT repo)
|
||||
**Settings file**: `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\settings.json`
|
||||
**Logs**: `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\Logs\v<ver>\log_<date>.log`
|
||||
**Exe**: `%LOCALAPPDATA%\PowerToys\WinUI3Apps\PowerToys.PowerRename.exe`
|
||||
**Activation**: Explorer right-click → "Rename with PowerRename" (Win11 Tier-1 menu; **no classic HKCR verb on Win11**); optional global hotkey if user-configured
|
||||
**DSC resource**: `Microsoft.PowerToys/PowerRenameSettings`
|
||||
|
||||
## Shared mechanics
|
||||
|
||||
For the synthetic-right-click + context-menu-invoke flow that ALL Explorer-context-menu modules use, see **`references/explorer-context-menu-flow.md`** + **`scripts/pt-explorer-contextmenu.ps1`** (`Test-PtDesktopInteractive`, `Open-PtExplorerContextMenu`, `Invoke-PtContextMenuItem`, `Get-PtContextMenuItems`). That doc covers stability rules, multi-file selection, BLK-ENV handling, and module-caption table. Don't duplicate; cite by section.
|
||||
|
||||
For the Win11 IExplorerCommand vs classic HKCR distinction, see `scripts/pt-shell-verbs.ps1` header — PR is **modern-menu-only on Win11**, so classic-verb enumeration via Shell.Application **will not find it**.
|
||||
|
||||
## Entry-paths (try in order)
|
||||
|
||||
### 1. Direct CLI launch with file args — PREFERRED for UI-driven tests (verified 2026-06-10)
|
||||
```powershell
|
||||
$tmp = New-Item -ItemType Directory -Path "$env:TEMP\pr-fixture-$(Get-Random)"
|
||||
1..3 | ForEach-Object { 'x' | Set-Content "$($tmp.FullName)\file$_.txt" }
|
||||
|
||||
Start-Process "$env:LOCALAPPDATA\PowerToys\WinUI3Apps\PowerToys.PowerRename.exe" `
|
||||
-ArgumentList "$($tmp.FullName)\file1.txt","$($tmp.FullName)\file2.txt","$($tmp.FullName)\file3.txt"
|
||||
|
||||
Start-Sleep -Milliseconds 1500
|
||||
$pr = (winapp ui list-windows -a PowerToys.PowerRename 2>$null | Out-String) -split "`r?`n" |
|
||||
ForEach-Object { if ($_ -match 'HWND (\d+):') { [int64]$matches[1] } } | Select-Object -First 1
|
||||
winapp ui inspect -w $pr --depth 5 -i 2>$null | Out-String | Select-String 'CheckBox "file\d\.txt"'
|
||||
# Expect 3 hits (file1/2/3.txt, [on] by default)
|
||||
```
|
||||
Bypasses the context menu entirely; same code path inside the exe (it parses argv as the file list). **Use for every UI-driven option/regex/preview test** (Recipes 4-12 below).
|
||||
|
||||
### 2. Synthetic right-click + Invoke-PtContextMenuItem — for "menu entry present/absent" assertions (Recipes 1-3)
|
||||
Use the canonical flow from `references/explorer-context-menu-flow.md` Recipe. The menu-presence assertion is the ONE thing the CLI back-door cannot prove (it works even if the menu entry is correctly hidden — the false-positive trap described in that doc).
|
||||
|
||||
```powershell
|
||||
. "$skill\scripts\pt-explorer-contextmenu.ps1"
|
||||
$hwnd = Open-PtExplorerContextMenu -FolderPath 'D:\fixtures' -FileNames 'a.txt'
|
||||
$items = Get-PtContextMenuItems -MenuHwnd $hwnd
|
||||
$has = $items | Where-Object Name -match 'Rename with PowerRename'
|
||||
# assert $has -> entry present
|
||||
```
|
||||
|
||||
### 3. Shell COM classic verb (does NOT work on Win11 stock install)
|
||||
```powershell
|
||||
Invoke-PtShellVerb -Path 'D:\fixtures\a.txt' -NamePattern 'PowerRename' # -> False
|
||||
```
|
||||
Returns False on Win11 because PT registers PR only via IExplorerCommand, not as a classic HKCR shell verb. **Use only for negative checks** (and prefer the synthetic-menu enumeration above, which observes the actual Tier-1 menu).
|
||||
|
||||
## Recipes — a control/observation map, NOT a per-test-case answer key
|
||||
|
||||
> **What this table is (and isn't):** it maps each PowerRename *capability* to **which control drives it** (AutomationId / settings key) and **where the result shows up**. It deliberately does **NOT** prescribe specific Search/Replace inputs or expected-output assertions — those are the agent's job to design from the actual checklist item at runtime. Keeping it input/assertion-free means the table survives checklist-wording changes; only a real UI redesign (renamed/moved control) should force an edit here (as happened to rows 5 & 12 in build 0.100.0).
|
||||
|
||||
| # | Capability | Drive (control / settings key) | Observe (where the result shows) |
|
||||
|---|---|---|---|
|
||||
| 1 | Context-menu entry present when enabled, gone when disabled | master `enabled.PowerRename` flip + `Restart-PtRunner`; synthetic menu (entry-path 2) | `Get-PtContextMenuItems` includes / excludes "Rename with PowerRename" |
|
||||
| 2 | "Show icon on context menu" | `ShowIcon` in `power-rename-settings.json` + relaunch | menu entry shows icon vs text-only (screenshot); or HKCR `Icon` |
|
||||
| 3 | "Appear only in extended menu" | `ExtendedContextMenuOnly` + relaunch | Tier-1 menu hides PR; classic "Show more options" still lists it |
|
||||
| 4 | Any search/replace option toggle (regex, match-all, case-sensitive, autocomplete, last-use) | `winapp ui invoke checkBox_regex` / `checkBox_matchAll` / `checkBox_case` (etc.); re-read `power-rename-settings.json` | the settings key flips **and** the preview behavior changes accordingly |
|
||||
| 5 | Case mode (single-select) | toggle **buttons** `toggleButton_lowerCase` / `upperCase` / `titleCase` / `capitalize` (not a dropdown) | preview column shows case-transformed names |
|
||||
| 6 | Scope: include/exclude Files / Folders / Subfolders | `toggleButton_includeFiles` / `includeFolders` / `includeSubfolders` | excluded row types appear disabled in the preview |
|
||||
| 7 | Apply-to scope: name-only / extension-only | the "Apply to" selector | replacement affects only the name vs only the extension (preview) |
|
||||
| 8 | Enumerate items | `toggleButton_enumItems`; Replace accepts `${start=,increment=,padding=}` tokens | preview shows the substituted counter |
|
||||
| 9 | Datetime tokens | Replace accepts `$DD` `$MMMM` `$YYYY` `$hh` `$mm` `$ss` `$fff` | preview value matches `(Get-Item <file>).CreationTime` formatted the same way |
|
||||
| 10 | Boost library (Perl regex beyond .NET, e.g. lookbehind) | `UseBoostLib` — **read at process start; relaunch PR after toggling** | the Perl-only pattern matches in the preview without error |
|
||||
| 11 | Per-row include/exclude in the preview | invoke a row checkbox to uncheck | the unchecked file is unchanged on disk after Rename |
|
||||
| 12 | Filter preview / select-all (NOT a column-header click — headers `TxtBlock_Original`/`TxtBlock_Renamed` are non-interactive labels) | `btn-filter-XXXX` → `button_showAll` / `button_showRenamed`; `checkBox_selectAll` | visible row set shrinks/grows; all rows toggle on/off |
|
||||
|
||||
> **Mapping process**: read the actual checklist item → identify the capability → find its row → drive the named control and design your own inputs + assertions for *that* item. If no row matches, it's a NEW capability — drive it ad-hoc and add a row (capability + control + observation point, no canned inputs).
|
||||
|
||||
|
||||
|
||||
## Fixture files needed
|
||||
|
||||
In a workspace `fixtures/` folder:
|
||||
- `a.txt`, `b.txt`, `c.txt` — multi-select
|
||||
- `IMG_001.png`, `IMG_002.png`, `IMG_003.png` — regex capture
|
||||
- subfolder `subdir/` with 2 inner files — folder/subfolder exclusion
|
||||
- `Foo_A_A_A.txt` — match-all
|
||||
- `MIXED.txt` — case-sensitive
|
||||
|
||||
Always copy fixtures to a disposable temp folder before running actual rename operations.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **TWO settings files — PR reads `power-rename-settings.json`, NOT `settings.json`** (verified 2026-06-10). `%LOCALAPPDATA%\Microsoft\PowerToys\PowerRename\` holds both: (1) `settings.json` = PT-store, keys `bool_mru_enabled`/`bool_persist_input`/`bool_show_icon_on_menu`/`bool_show_extended_menu`/`bool_use_boost_lib`/`int_max_mru_size` (what `Get-PtModuleSettings` + the Settings UI bind to); (2) `power-rename-settings.json` = the module's own store, keys `ShowIcon`/`ExtendedContextMenuOnly`/`PersistState`/`MRUEnabled`/`MaxMRUSize`/`UseBoostLib` — **this is the file the PR UI exe and the context-menu COM handlers actually read at launch** (`lib/Settings.cpp` `CSettings::Load→ParseJson`). The runner (`dll/dllmain.cpp:301-307`) syncs PT-store→module-store only on a Settings-UI *change event*; the PT-store file can sit stale for days. **To drive ShowIcon / ExtendedContextMenuOnly / MRUEnabled / PersistState / UseBoostLib deterministically, edit `power-rename-settings.json` directly + relaunch PR (or restart runner+Explorer for the menu handlers), then restore.** Map (settings.json key → user-facing toggle): ShowIcon→"Show icon on context menu", ExtendedContextMenuOnly→"Appear only in extended menu", MRUEnabled→autocomplete, PersistState→"Show values from last use", UseBoostLib→"Use Boost library". MRU values live in `search-mru.json`/`replace-mru.json`; last-used (persist) in `power-rename-last-run-data.json`.
|
||||
- **"Show icon on context menu" has no Settings-UI toggle in current builds** — drive it via `power-rename-settings.json` `ShowIcon`. Behavior is observable on the synthetic menu (icon vs text-only); source `PowerRenameContextMenu/dllmain.cpp:73` (`GetIcon→null`).
|
||||
- **The "Appear only in extended menu" classic `#32768` popup is not winapp-enumerable** — assert the Tier-1 *hide* (observed; `dllmain.cpp:108` `ECS_HIDDEN`) and cite `PowerRenameExt.cpp:84` (`E_FAIL` unless `CMF_EXTENDEDVERBS`) for the "still in extended menu" half.
|
||||
- **PR registers on the directory *background* menu too** — the synthetic right-click often lands on background (View/Sort by/Group by/...) yet still shows/hides `Rename with PowerRename`, which is a valid, stable surface for menu-entry / icon-visibility / extended-menu-only present-absent comparisons.
|
||||
- **`set-value` on search/replace DOES fire the preview** (TextChanged works, unlike CmdPal) — Apply button enabling/disabling is a reliable match/no-match signal. The search/replace Edit AutomationIds are random per launch (`txt-textbox-XXXX`); discover them each launch by name (`Edit "Search for"` / `Edit "Replace with"`).
|
||||
- **Preview-row uncheck + column-header invokes need the Preview populated first** — set Search/Replace and wait ~500 ms for the regex engine; otherwise the invokes hit an empty list.
|
||||
- **Boost library is read at PR process start** — close + relaunch PR after toggling.
|
||||
- **Icon-on-menu and extended-only checks prefer registry over screenshot** — read HKCR `Extended` / `Icon` REG_SZ; more reliable + locale-independent.
|
||||
- **Disk mutation is real** — run renames against `$env:TEMP\pr-test-<random>`, not real fixtures.
|
||||
- **COM cache staleness** when re-checking verbs after enable/disable — call `Reset-PtShellComCache` from `scripts/pt-shell-verbs.ps1`.
|
||||
|
||||
## Source citations
|
||||
|
||||
- `<PT-repo>\src\modules\PowerRename\dllmain.cpp` — IExplorerCommand registration (no classic HKCR shadow on Win11).
|
||||
- `<PT-repo>\src\modules\PowerRename\PowerRenameUILib\` — XAML for main PR window (toggle/checkbox AutomationIds).
|
||||
- `<PT-repo>\src\modules\PowerRename\PowerRenameLib\Settings.cpp` — settings.json schema canonical property names.
|
||||
|
||||
## Ceiling
|
||||
|
||||
Expected **18/18 = 100%** from an interactive admin console session. Direct-CLI (#1) covers UI-driven items; synthetic-menu (#2) covers menu-presence assertions.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- **Don't** try `Invoke-PtShellVerb 'PowerRename'` — returns False on Win11 (no classic registration). Use synthetic menu via `Invoke-PtContextMenuItem` or direct-CLI.
|
||||
- **Don't** run rename operations against reusable fixtures — copy to a disposable temp folder.
|
||||
- **Don't** trust screenshot-only for icon-on-menu or extended-only checks — registry inspection is faster + locale-independent.
|
||||
- **Don't** skip the synthetic-menu test for the menu-presence assertion — CLI back-door PASSes even when the menu entry is correctly hidden (false-positive trap described in `references/explorer-context-menu-flow.md`).
|
||||
@@ -1,122 +0,0 @@
|
||||
# Pre-flight checks, bootstrap, and state hygiene
|
||||
|
||||
This doc covers the **agent-runtime** environment probing and lifecycle hooks. Read alongside `SKILL.md` (the playbook) and `references/environment-setup.md` (one-time user env prep).
|
||||
|
||||
## Pre-flight checks (do these first; abort if any fails)
|
||||
|
||||
1. **Admin check** — `Test-PtAdmin` must return the elevation level matching `[ADMIN: YES]` items in the module's checklist. If the module contains `[ADMIN: YES]` items and `Test-PtAdmin` returns `False`, **STOP** and tell the user "this module requires an elevated session". Do NOT silently mark those items BLOCKED-LACK-ADMIN — that hides a fixable env issue.
|
||||
|
||||
2. **PT runner present** — `Test-PtRunnerAdmin` should show the runner exists. If it doesn't exist, start PowerToys (`Start-Process "$env:LOCALAPPDATA\PowerToys\PowerToys.exe"`).
|
||||
|
||||
3. **Module installed** — `Get-PtModuleSettings -ModuleDir <ModuleDir>` (or `Get-CmdPalSettings` for CmdPal) returns non-null.
|
||||
|
||||
4. **Interactive-desktop availability + session attachment** — the single most common cause of false-BLOCKED reports is a session mismatch where the agent runs in an elevated **non-console session** (e.g. RDP that's been disconnected/minimized, fast user switching, run-as-different-user, or scheduled-task-with-highest-privilege). In that scenario `Test-PtAdmin=True` but `GetForegroundWindow()=0` and `SendInput` returns `ERROR_ACCESS_DENIED (5)` — input injection cannot reach the active desktop.
|
||||
|
||||
```powershell
|
||||
# Sessions
|
||||
$agentSession = [Diagnostics.Process]::GetCurrentProcess().SessionId
|
||||
$consoleSession = (Get-Process explorer -EA SilentlyContinue | Select-Object -First 1).SessionId
|
||||
"Agent session=$agentSession Console explorer session=$consoleSession"
|
||||
|
||||
# Foreground + Shell COM probe (use scripts/pt-session-diagnose.ps1 for the full version)
|
||||
Add-Type 'using System; using System.Runtime.InteropServices; public class FG4 { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }'
|
||||
$hasFg = $false
|
||||
for ($i = 0; $i -lt 5; $i++) { if ([FG4]::GetForegroundWindow() -ne [IntPtr]::Zero) { $hasFg=$true; break }; Start-Sleep -Milliseconds 200 }
|
||||
$shellOk = $false
|
||||
try { $shellOk = (@((New-Object -ComObject Shell.Application).Windows()).Count -ge 0) } catch {}
|
||||
"Interactive desktop: ForegroundOk=$hasFg ShellComOk=$shellOk"
|
||||
|
||||
if (-not $hasFg -and $agentSession -ne $consoleSession) {
|
||||
Write-Host "===========================================================" -ForegroundColor Red
|
||||
Write-Host "NON-INTERACTIVE SESSION DETECTED" -ForegroundColor Red
|
||||
Write-Host "Agent is in Session $agentSession but the active console is Session $consoleSession." -ForegroundColor Red
|
||||
Write-Host "SendInput, global hotkeys, and arrow-key navigation will NOT work here." -ForegroundColor Red
|
||||
Write-Host "Items requiring input injection will be marked BLK-ENV up-front." -ForegroundColor Red
|
||||
Write-Host "Mitigation: see references/environment-setup.md, or relaunch in console session:" -ForegroundColor Yellow
|
||||
Write-Host " psexec -accepteula -h -i $consoleSession -s pwsh.exe" -ForegroundColor Yellow
|
||||
Write-Host "===========================================================" -ForegroundColor Red
|
||||
# Continue verification — schema/UIA/CLI-based tests still produce real evidence
|
||||
}
|
||||
```
|
||||
|
||||
**Key distinction** (all rows assume `Admin=True`):
|
||||
- **ForegroundOk + ShellComOk** → Everything works — interactive elevated session.
|
||||
- **ShellComOk only (ForegroundOk false)** → Non-interactive (e.g. Session ≠ console, RDP minimized, screen locked, screensaver). Only schema / UIA-invoke / CLI / Named-Event tests work. Mark input-injection items as `BLK-ENV` and **cite `references/environment-setup.md` in the report** so the user can fix env and re-run.
|
||||
- **Neither (ShellComOk false)** → Session 0 / service context — even Shell COM fails. Very few tests possible.
|
||||
|
||||
5. **Discipline: try AT LEAST 2 distinct entry-paths before marking BLOCKED.** For Peek/FZ/Workspaces/Image Resizer/PowerRename/File Locksmith specifically, the obvious entry-path is the global hotkey but Shell.Application COM driving Explorer also works — see per-module profiles under `references/modules/`. Marking BLOCKED after trying only the CLI launch (a common trap) hides easily-PASS-able items in an interactive session.
|
||||
|
||||
## Bootstrap (paste at start of your verification script)
|
||||
|
||||
```powershell
|
||||
$skill = '<this skill folder>' # the folder containing SKILL.md
|
||||
Get-ChildItem "$skill\scripts" -Filter '*.ps1' | ForEach-Object { . $_.FullName }
|
||||
|
||||
$workspace = "$env:TEMP\verify-<Module>-$(Get-Date -Format yyyyMMdd-HHmmss)"
|
||||
New-Item -ItemType Directory -Path $workspace, "$workspace\artifacts" | Out-Null
|
||||
$report = "$workspace\verify-<Module>.md"
|
||||
|
||||
"# <Module> verification — $(Get-Date -Format 'yyyy-MM-dd HH:mm')" | Set-Content $report
|
||||
"" | Add-Content $report
|
||||
"## Pre-flight" | Add-Content $report
|
||||
"- IsAdmin: $(Test-PtAdmin)" | Add-Content $report
|
||||
"- PT runner: PID=$((Test-PtRunnerAdmin).Pid) Elevated=$((Test-PtRunnerAdmin).Elevated)" | Add-Content $report
|
||||
|
||||
# Then proceed with pre-flight checks #4-#6 above and write their results into the report.
|
||||
```
|
||||
|
||||
## State hygiene (CRITICAL — always restore)
|
||||
|
||||
Wrap any settings/registry mutation in try/finally:
|
||||
|
||||
```powershell
|
||||
# Per-item: settings.json edits
|
||||
$bk = Backup-PtModuleSettings -ModuleDir <ModuleDir>
|
||||
try {
|
||||
# ... mutate + assert ...
|
||||
} finally {
|
||||
Restore-PtModuleSettings -ModuleDir <ModuleDir> -BackupPath $bk
|
||||
}
|
||||
|
||||
# After GPO/admin tests
|
||||
Remove-Item HKLM:\Software\Policies\PowerToys -Recurse -Force -EA SilentlyContinue
|
||||
Remove-Item HKCU:\Software\Policies\PowerToys -Recurse -Force -EA SilentlyContinue
|
||||
Remove-Item 'C:\Windows\PolicyDefinitions\PowerToys.admx' -Force -EA SilentlyContinue
|
||||
Remove-Item 'C:\Windows\PolicyDefinitions\en-US\PowerToys.adml' -Force -EA SilentlyContinue
|
||||
|
||||
# Spawned processes (notepad, regedit, etc.) — kill by PID, not by name
|
||||
foreach ($pid in $spawnedPids) { Stop-Process -Id $pid -Force -EA SilentlyContinue }
|
||||
```
|
||||
|
||||
## Final wrap-up (run AFTER all per-item tables are written)
|
||||
|
||||
1. **Run state-hygiene cleanup** above for everything that wasn't restored per-item.
|
||||
2. **Write the top-of-report summary** per `references/reporting-format.md` §B.
|
||||
3. **Write the §G Retrospective** — reflect on the run itself: every friction (classified by source + severity + minutes/attempts cost + suggested fix), or `Everything was smooth — no friction encountered.` See `references/reporting-format.md` §G. Don't skip it; it's how the skill improves.
|
||||
4. **Verify every screenshot referenced in the report actually exists on disk** (before the move, while paths still resolve under `$workspace`):
|
||||
```powershell
|
||||
$missing = Get-Content $report | Select-String 'artifacts/L\d+/step-\d+-[^\.\s]+\.(png|txt|log|json|ps1)' -AllMatches |
|
||||
ForEach-Object { $_.Matches.Value } | Sort-Object -Unique |
|
||||
Where-Object { -not (Test-Path (Join-Path $workspace $_)) }
|
||||
if ($missing) { Write-Warning "Missing artifacts: $($missing -join ', ')" }
|
||||
```
|
||||
5. **Move the workspace to the sign-off archive** (LAST step, after the report + artifact check pass):
|
||||
```powershell
|
||||
$signoff = "$env:OneDrive\PowerToys\Module-Signoff"
|
||||
New-Item -ItemType Directory -Path $signoff -Force | Out-Null
|
||||
$final = Join-Path $signoff (Split-Path $workspace -Leaf)
|
||||
Move-Item -Path $workspace -Destination $final -Force
|
||||
$report = Join-Path $final (Split-Path $report -Leaf)
|
||||
```
|
||||
The report uses **relative** `artifacts/…` paths, so the whole tree moves intact.
|
||||
6. **Print the FINAL (moved) report path** as the very last line of your response — the `…\Module-Signoff\verify-<Module>-<timestamp>\verify-<Module>.md` path, NOT the temp path.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Never silently send keys via SendInput** to a target window without first calling `Assert-PtForegroundOrAbort -AppId <id>`. Keys silently leak to your terminal if the target isn't foreground.
|
||||
- **Never mark BLOCKED without trying at least 2 distinct entry-paths from the drive-stack** (SKILL.md §2). If you can't drive the item, name the specific obstacle (not "I can't").
|
||||
- **Never assume any external repo is cloned locally.** The helpers under `scripts/` are self-contained. Use `Test-Path` guards before referencing any external path.
|
||||
- **Never invent test steps for a `[CLARITY: VAGUE-*]` item** — mark it **FAIL (cause: checklist-ambiguous)** and quote the original wording so the user can fix the checklist. The checklist is test code; an undefinable test is a broken test.
|
||||
- **Always restore state** before exiting (even on error). State hygiene wraps every mutation in try/finally.
|
||||
- **Separate the two FAIL causes**: *product* FAILs are bugs to file; *checklist* FAILs (stale feature or ambiguous spec) are items to rewrite/prune. If a large share of a module's items are checklist-FAILs, the checklist needs an overhaul before re-verifying — don't punt drivable items into a FAIL.
|
||||
- **Never continue past 3 consecutive errors against the same item** — mark it BLOCKED with the concrete symptom/obstacle and move on. Per-item budget is ~5 minutes; if stuck longer, it's BLOCKED (name the wall).
|
||||
@@ -1,45 +0,0 @@
|
||||
# Environment Variables — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with two metadata tags:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
**Clarity**:
|
||||
- (no marker) - clear, has explicit assert
|
||||
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
|
||||
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
|
||||
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
|
||||
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables (20 items)
|
||||
|
||||
- [ ] **[ADMIN: YES]** (L791) Launch as administrator ON - Launch Environment Variables and confirm that SYSTEM variables ARE editable and Add variable button is enabled
|
||||
- [ ] **[ADMIN: YES]** (L792) Launch as administrator OFF - Launch Environment Variables and confirm that SYSTEM variables ARE NOT editable and Add variable button is disabled
|
||||
- [ ] **[ADMIN: NO]** (L795) Add new User variable. Open OS Environment variables window and confirm that added variable is there. Also, confirm that it's added to "Applied variables" list.
|
||||
- [ ] **[ADMIN: NO]** (L796) Edit one User variable. Open OS Environment variables window and confirm that variable is changed. Also, confirm that change is applied to "Applied variables" list.
|
||||
- [ ] **[ADMIN: NO]** (L797) Remove one User variable. Open OS Environment variables window and confirm that variable is removed. Also, confirm that variable is removed from "Applied variables" list.
|
||||
- [ ] **[ADMIN: NO]** (L801) Add new profile with no variables and name it "Test_profile_1" (referenced below by name)
|
||||
- [ ] **[ADMIN: NO]** (L802) Edit "Test_profile_1": Add one new variable to profile e.g. name: "profile_1_variable_1" value: "profile_1_value_1"
|
||||
- [ ] **[ADMIN: NO]** (L803) Add new profile "Test_profile_2": From "Add profile dialog" add two new variables (profile_2_variable_1:profile_2_value_1 and profile_2_variable_2:profile_2_value_2). Set profile to enabled and click Save. Open OS Environment variables window and confirm that all variables from the profile are applied correctly. Also, confirm that "Applied variables" list contains all variables from the profile.
|
||||
- [ ] **[ADMIN: NO]** (L804) Apply "Test_profile_1" while "Test_profile_2" is still aplpied. Open OS Environment variables window and confirm that all variables from Test_profile_2 are unapplied and that all variables from Test_profile_1 are applied. Also, confirm that state of "Applied variables" list is updated correctly.
|
||||
- [ ] **[ADMIN: NO]** (L805) Unapply applied profile. Open OS Environment variables window and confirm that all variables from the profile are unapplied correctly. Also, confirm that "Applied variables" list does not contain variables from the profile.
|
||||
- [ ] **[ADMIN: NO]** (L808) To "Test_profile_1" add one existing variable from USER variables, e.g. TMP. After adding, change it's value to e.g "test_TMP" (or manually add variable named TMP with value test_TMP).
|
||||
- [ ] **[ADMIN: NO]** (L809) Apply "Test_profile_1". Open OS Environment variables window and confirm that TMP variable in USER variables has value "test_TMP". Confirm that there is backup variable "TMP_PowerToys_Test_profile_1" with original value of TMP var. Also, confirm that "Applied variables" list is updated correctly - there is TMP profile variable, and backup User variable..
|
||||
- [ ] **[ADMIN: NO]** (L810) Unapply "Test_profile_1". Open OS Environment variables window and confirm that TMP variable in USER variable has original value and that there is no backup variable. Also, confirm that "Applied variables" list is updated correctly.
|
||||
- [ ] **[ADMIN: NO]** (L813) In "Applied variables" list confirm that PATH variable is shown properly: value of USER Path concatenated to the end of SYSTEM Path.
|
||||
- [ ] **[ADMIN: NO]** (L814) To "Test_profile_1" add variable named PATH with value "path1;path2;path3" and click Save. Confirm that PATH variable in profile is shown as list (list of 3 values and not as path1;path2;path3).
|
||||
- [ ] **[ADMIN: NO]** (L815) Edit PATH variable from "Test_profile_1". Try different options from ... menu (Delete, Move up, Move down, etc...). Click Save.
|
||||
- [ ] **[ADMIN: NO]** (L816) Apply "Test_profile_1". Open OS Environment variables window and confirm that profile is applied correctly - Path value and backup variable. Also, in "Applied variables" list check that Path variable has correct value: value of profile PATH concatenated to the end of SYSTEM Path.
|
||||
- [ ] **[ADMIN: NO]** (L819) Close the app and reopen it. Confirm that the state of the app is the same as before closing.
|
||||
- [ ] **[ADMIN: NO]** (L821) "Test_profile_1" should still be applied (if not apply it). Delete "Test_profile_1". Confirm that profile is unapplied (both in OS Environment variables window and "Applied variables" list).
|
||||
- [ ] **[ADMIN: NO]** (L822) Delete "Test_profile_2". Check profiles.json file and confirm that both profiles are gone.
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# File Locksmith — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with two metadata tags:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
**Clarity**:
|
||||
- (no marker) - clear, has explicit assert
|
||||
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
|
||||
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
|
||||
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
|
||||
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
|
||||
|
||||
---
|
||||
|
||||
## File Locksmith (10 items)
|
||||
|
||||
- [ ] **[ADMIN: COND]** (L641) Right-click the executable file, select "Unlock with File Locksmith" and verify it shows up. (2 entries will show, since the installer starts two processes)
|
||||
- [ ] **[ADMIN: COND]** (L642) End the tasks in File Locksmith UI and verify that closes the installer.
|
||||
- [ ] **[ADMIN: COND]** (L643) Start the installer executable again and press the Refresh button in File Locksmith UI. It should find new processes using the files.
|
||||
- [ ] **[ADMIN: COND]** (L644) Close the installer window and verify the processes are delisted from the File Locksmith UI. Close the window
|
||||
- [ ] **[ADMIN: COND]** (L646) Right click the directory where the executable is located, select "Unlock with File Locksmith" and verify it shows up.
|
||||
- [ ] **[ADMIN: COND]** (L647) Right click the drive where the executable is located, select "Unlock with File Locksmith" and verify it shows up. You can close the PowerToys installer now.
|
||||
- [ ] **[ADMIN: COND]** (L649) Right click "Program Files", select "Unlock with File Locksmith" and verify "PowerToys.exe" doesn't show up.
|
||||
- [ ] **[ADMIN: YES]** (L650) Press the File Locksmith "Restart as an administrator" button and verify "PowerToys.exe" shows up.
|
||||
- [ ] **[ADMIN: YES]** (L651) Right-click the drive where Windows is installed, select "Unlock with File Locksmith" and scroll down and up, verify File Locksmith doesn't crash with all those entries being shown. Repeat after clicking the File Locksmith "Restart as an administrator" button.
|
||||
- [ ] **[ADMIN: COND]** (L652) Disable File Locksmith in Settings and verify the context menu entry no longer appears.
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# Image Resizer — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with two metadata tags:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
**Clarity**:
|
||||
- (no marker) - clear, has explicit assert
|
||||
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
|
||||
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
|
||||
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
|
||||
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
|
||||
|
||||
---
|
||||
|
||||
## Image Resizer (18 items)
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L309) Disable the Image Resizer and check that `Resize with Image Resizer` is absent in the context menu
|
||||
- [ ] **[ADMIN: NO]** (L310) Enable the Image Resizer and check that `Resize with Image Resizer` is present in the context menu (both Win11 modern and old menus)
|
||||
- [ ] **[ADMIN: NO]** (L311) Remove one image size and add a custom image size. Open the Image Resize window from the context menu and verify changes are populated
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L312) Resize one image
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L313) Resize multiple images
|
||||
- [ ] **[ADMIN: NO]** (L314) Open image resizer to resize a .gif and verify "Gif files with animations may not be correctly resized." warning appears
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L316) Resize images with Fill option
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L317) Resize images with Fit option
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L318) Resize images with Stretch option
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L320) Resize using dimension Centimeters
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L321) Resize using dimension Inches
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L322) Resize using dimension Percents
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L323) Resize using dimension Pixels
|
||||
- [ ] **[ADMIN: NO]** (L325) Change Filename format to %1 - %2 - %3 - %4 - %5 - %6 and verify applied
|
||||
- [ ] **[ADMIN: NO]** (L326) Check Use original date modified and verify modified date not changed for resized
|
||||
- [ ] **[ADMIN: NO]** (L327) Check Make pictures smaller but not larger and verify smaller pictures not resized
|
||||
- [ ] **[ADMIN: NO]** (L328) Check Resize the original pictures (don't create copies) and verify original is resized
|
||||
- [ ] **[ADMIN: NO]** (L329) Uncheck Ignore the orientation and verify swapped W/H actually resizes if W!=H
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Release checklist — per-module index
|
||||
|
||||
One file per module; a verification run loads only its module's file.
|
||||
|
||||
> **Scope:** only modules that have been verified end-to-end (with a sign-off report) are checked in here so far. The remaining modules' checklists will be added as each is verified.
|
||||
|
||||
| Module | Items | File |
|
||||
|---|---:|---|
|
||||
| Environment Variables | 20 | `environment-variables.md` |
|
||||
| File Locksmith | 10 | `file-locksmith.md` |
|
||||
| Image Resizer | 18 | `image-resizer.md` |
|
||||
| New+ | 9 | `new-plus.md` |
|
||||
| Peek | 18 | `peek.md` |
|
||||
| PowerRename | 18 | `power-rename.md` |
|
||||
@@ -1,34 +0,0 @@
|
||||
# New+ — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with two metadata tags:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
**Clarity**:
|
||||
- (no marker) - clear, has explicit assert
|
||||
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
|
||||
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
|
||||
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
|
||||
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
|
||||
|
||||
---
|
||||
|
||||
## New+ (9 items)
|
||||
|
||||
- [ ] **[ADMIN: NO]** (L969) Verify NewPlus menu is in Explorer context menu. (Windows 11 tier 1 context menu only. May need Explorer restart.)
|
||||
- [ ] **[ADMIN: NO]** (L971) Verify NewPlus menu is not in Explorer context menu.
|
||||
- [ ] **[ADMIN: NO]** (L973) Verify the folder is created and empty.
|
||||
- [ ] **[ADMIN: NO]** (L974) Copy a file to the templates folder, verify it's added to the New+ context menu and that if you select it the file is created.
|
||||
- [ ] **[ADMIN: NO]** (L975) Copy a folder with files inside to the templates folder, verify it's added to the New+ context menu and that if you select it the folder and files inside are created.
|
||||
- [ ] **[ADMIN: NO]** (L976) Delete all files and folders from inside the templates folder. Verify that no templates are available in the context menu.
|
||||
- [ ] **[ADMIN: NO]** (L977) Disable and re-Enable New+ while the templates folder is still empty. Verify the default templates were copied over and are available in the context menu.
|
||||
- [ ] **[ADMIN: NO]** (L979) Test the "Hide template filename extension" option in Settings.
|
||||
- [ ] **[ADMIN: NO]** (L980) Test the "Hide template filename starting digits, spaces and dots" option in Settings.
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# Peek — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with two metadata tags:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
**Clarity**:
|
||||
- (no marker) - clear, has explicit assert
|
||||
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
|
||||
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
|
||||
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
|
||||
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
|
||||
|
||||
---
|
||||
|
||||
## Peek (18 items)
|
||||
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L697) Image
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L698) Text or dev file
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L699) Markdown file
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L700) PDF
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L701) HTML
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L702) Archive files (.zip, .tar, .rar)
|
||||
- [ ] **[ADMIN: NO]** (L703) Any other not mentioned file (.exe for example) to verify the unsupported file view is shown
|
||||
- [ ] **[ADMIN: NO]** (L706) Pin the window, switch between images of different size, verify the window stays at the same place and the same size.
|
||||
- [ ] **[ADMIN: NO]** (L707) Pin the window, close and reopen Peek, verify the new window is opened at the same place and the same size as before.
|
||||
- [ ] **[ADMIN: NO]** (L708) Unpin the window, switch to a different file, verify the window is moved to the default place.
|
||||
- [ ] **[ADMIN: NO]** (L709) Unpin the window, close and reopen Peek, verify the new window is opened on the default place.
|
||||
- [ ] **[ADMIN: NO]** (L712) By clicking a button.
|
||||
- [ ] **[ADMIN: NO]** (L713) By pressing enter.
|
||||
- [ ] **[ADMIN: NO]** (L716) Can use peek command to peek files
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L717) Peek can work without problem when a peek session is on
|
||||
- [ ] **[ADMIN: NO]** (L719) Switch between files in the folder using `LeftArrow` and `RightArrow`, verify you can switch between all files in the folder.
|
||||
- [ ] **[ADMIN: NO]** (L720) Open multiple files, verify you can switch only between selected files.
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-AMBIGUOUS]** (L721) Change the shortcut, verify the new one works.
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# PowerRename — PowerToys release checklist
|
||||
|
||||
> Source: split from `release-checklist-annotated.md` (generated 2026-06-06). One module per file.
|
||||
|
||||
## Legend
|
||||
|
||||
Each item is annotated with two metadata tags:
|
||||
|
||||
**Admin requirement**:
|
||||
- `[ADMIN: NO]` - runnable from a standard (non-elevated) shell
|
||||
- `[ADMIN: YES]` - requires elevated session (writes to HKLM, %WinDir%\System32, MSI install, GPO templates, etc.)
|
||||
- `[ADMIN: COND]` - conditional - the basic case is non-admin but specific sub-cases require admin (e.g. "test with elevated target app", "Restart as admin" variants)
|
||||
|
||||
**Clarity**:
|
||||
- (no marker) - clear, has explicit assert
|
||||
- `[CLARITY: VAGUE-NO-STEPS]` - original wording is just a module/feature name without procedural steps
|
||||
- `[CLARITY: VAGUE-NO-ASSERT]` - original wording describes an action but does not state the expected outcome
|
||||
- `[CLARITY: VAGUE-AMBIGUOUS]` - original wording uses vague verbs like "works" without a measurable outcome
|
||||
- `[REWRITTEN]` - original wording was vague; this checklist has rewritten the description to be concrete. Original wording preserved in italics below the item.
|
||||
|
||||
---
|
||||
|
||||
## PowerRename (18 items)
|
||||
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-AMBIGUOUS]** (L393) Check if disable and enable of the module works. (On Win11) Check if both old context menu and Win11 tier1 context menu items are present when module is enabled.
|
||||
- [ ] **[ADMIN: NO]** (L394) Check that with the `Show icon on context menu` icon is shown and vice versa.
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-AMBIGUOUS]** (L395) Check if `Appear only in extended context menu` works.
|
||||
- [ ] **[ADMIN: NO]** (L396) Enable/disable autocomplete.
|
||||
- [ ] **[ADMIN: NO]** (L397) Enable/disable `Show values from last use`.
|
||||
- [ ] **[ADMIN: NO]** (L399) Make Uppercase/Lowercase/Titlecase (could be selected only one at the time)
|
||||
- [ ] **[ADMIN: NO]** (L400) Exclude Folders/Files/Subfolder Items (could be selected several)
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L401) Item Name/Extension Only (one at the time)
|
||||
- [ ] **[ADMIN: NO]** (L402) Enumerate Items. Test advanced enumeration using different values for every field ${start=10,increment=2,padding=4}.
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L403) Case Sensitive
|
||||
- [ ] **[ADMIN: NO]** (L404) Match All Occurrences. If checked, all matches of text in the `Search` field will be replaced with the Replace text. Otherwise, only the first instance of the `Search` for text in the file name will be replaced (left to right).
|
||||
- [ ] **[ADMIN: NO]** (L406) Search with an expression (e.g. `(.*).png`)
|
||||
- [ ] **[ADMIN: NO]** (L407) Replace with an expression (e.g. `foo_$1.png`)
|
||||
- [ ] **[ADMIN: NO] [CLARITY: VAGUE-NO-STEPS]** (L408) Replace using file creation date and time (e.g. `$hh-$mm-$ss-$fff` `$DD_$MMMM_$YYYY`)
|
||||
- [ ] **[ADMIN: NO]** (L409) Turn on `Use Boost library` and test with Perl Regular Expression Syntax (e.g. `(?<=t)est`)
|
||||
- [ ] **[ADMIN: NO]** (L411) In the `preview` window uncheck some items to exclude them from renaming.
|
||||
- [ ] **[ADMIN: NO]** (L412) Use the **Filter** (funnel) button above the file list → choose "Only show files that will be renamed" / "Show all files" to filter the preview.
|
||||
- [ ] **[ADMIN: NO]** (L413) Use the **Select/deselect all** checkbox above the file list to toggle all rows checked/unchecked.
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
# Reporting format
|
||||
|
||||
This doc defines the **required** report shape for every per-module verification run. Modeled on `PR-validation\Round1\PR-47211-validation\report.md` style — table-driven, reproducible, no prose narratives.
|
||||
|
||||
## §A — Per-item table (one per checklist item)
|
||||
|
||||
```markdown
|
||||
## Item L<line_num> — <verbatim description from the module's checklist> — **<PASS|FAIL|BLOCKED>** <emoji>
|
||||
|
||||
**Admin**: <NO|COND|YES> | **Clarity**: <CLEAR|VAGUE-*|REWRITTEN> | **Category**: <PASS: verification method (free text) · FAIL: cause = product | checklist-stale | checklist-ambiguous · BLOCKED: a BLK-* reason>
|
||||
|
||||
### Verification steps performed
|
||||
|
||||
| # | Step | winapp / probe commands | Evidence / result |
|
||||
|---|---|---|---|
|
||||
| 1 | <what step 1 does> | `<exact command>`<br>`<another command if multiple>` | <what you observed; reference artifact filename> |
|
||||
| 2 | <what step 2 does> | `<command>` | <evidence>; screenshot: `artifacts/L<line>/step-02-<name>.png` |
|
||||
| 3 | ... | ... | ... |
|
||||
|
||||
### Artifacts produced
|
||||
- `artifacts/L<line>/step-01-<name>.png` — <one-line description>
|
||||
- `artifacts/L<line>/step-02-<name>.txt` — full inspect dump
|
||||
- ...
|
||||
|
||||
### Verdict reasoning
|
||||
- ✅ <assertion 1 that PASSed, with reference to the line of code / settings key / log line that proves it>
|
||||
- ✅ <assertion 2>
|
||||
- ❌ <if BLOCKED, the specific obstacle: "BLK-HARDWARE because MWB needs 2 physical PCs; this session has 1 ([System.Windows.Forms.Screen]::AllScreens.Count = 1)">
|
||||
|
||||
### Caveats (optional)
|
||||
- <Any deviation from the user-documented flow, e.g. "Tested via settings.json write rather than UI checkbox because SelectionItemPattern.Select clobbers other selections in ListView.">
|
||||
```
|
||||
|
||||
## §B — Top-of-report summary (write LAST, after all per-item tables)
|
||||
|
||||
```markdown
|
||||
# <Module> verification report — <YYYY-MM-DD HH:MM>
|
||||
|
||||
## Summary
|
||||
- **PASS**: <n> · **FAIL (product)**: <n> · **FAIL (checklist)**: <n> · **BLOCKED**: <n> · **Total**: <n> · **PASS%**: <n>
|
||||
- **Top blocker categories**: <category>: <count>, <category>: <count>, ...
|
||||
- **Items needing follow-up**: L<line> (<reason>), L<line> (<reason>), ...
|
||||
- **State mutations performed + restored**: <count> settings.json edits restored, <count> registry keys removed, <count> fixture files deleted
|
||||
|
||||
## Pre-flight
|
||||
- IsAdmin: <true|false>
|
||||
- PT runner: PID=<n> Elevated=<true|false>
|
||||
- <Module> settings file: <path> (exists=<true|false>)
|
||||
- Interactive desktop: ForegroundOk=<true|false> ShellComOk=<true|false>
|
||||
|
||||
## Items
|
||||
<all per-item tables here, in line_num order>
|
||||
|
||||
## Cleanup performed
|
||||
- <list of every restore action taken>
|
||||
|
||||
## Retrospective (self-reflection on the run — write LAST)
|
||||
<Per §G. If the whole run was frictionless, write exactly: **Everything was smooth — no friction encountered.**>
|
||||
```
|
||||
|
||||
## §C — Required rules for step tables
|
||||
|
||||
1. **Every `winapp ui ...` command goes in the "winapp / probe commands" cell, verbatim, in backticks**, including `-w <hwnd>` / `-a <appId>` arguments and full selector strings. Reviewers will paste these into their own shell to reproduce.
|
||||
2. **Every screenshot path goes in the "Evidence" cell** of the step that produced it, formatted as `screenshot: artifacts/L<line>/step-NN-<name>.png`. Never embed screenshots as `` in the table body (breaks GitHub markdown rendering inside cells); just give the path.
|
||||
3. **If a step has multiple commands**, separate them in the same cell with `<br>` so they render as one cell with multiple lines.
|
||||
4. **PowerShell scriptlets > 3 lines**: write them to a separate `.ps1` in the artifacts folder and reference as ``script: `artifacts/L<line>/step-NN.ps1` `` in the cell. Keep the table cell to 1-3 lines.
|
||||
5. **`—` (em dash) is allowed for non-CLI steps** like "Read sign-off entry + diff", "Create validation folder", "Cleanup notepad". Don't fabricate a command for steps that were purely cognitive or file-system level.
|
||||
6. **Numbered steps must be contiguous** (1, 2, 3, ...). Don't skip numbers.
|
||||
7. **At least one screenshot per PASS item if the item is a user-visible behavioral test**. Schema-only assertions (settings.json key check) don't need screenshots; behavioral tests (popup shown, dialog appeared, theme switched) do.
|
||||
|
||||
## §D — Reporting style
|
||||
|
||||
- Be specific. "Verified via UIA inspect returned `itm-calculator-XXXX`" beats "verified UIA".
|
||||
- Include exact UIA selectors, log line text, settings.json keys, and screenshot filenames so the user can audit.
|
||||
- For BLOCKED items, the 1-sentence reason should name **what specifically blocks**, e.g.:
|
||||
- "BLK-HARDWARE: requires 2nd monitor; session has 1 (verified via `[System.Windows.Forms.Screen]::AllScreens.Count`)."
|
||||
- "BLK-DRAG-REQUIRED: synthetic mouse drag insufficient for FZ snap-and-drag; needs real cursor motion."
|
||||
- "BLK-ENV: SendInput returned ACCESS_DENIED (5) because Session $agentSession ≠ console Session $consoleSession. See `references/environment-setup.md`."
|
||||
- "BLK-EXTERNAL-APP: requires real OpenAI API key; no key provisioned in test env."
|
||||
|
||||
## §E — Reporting anti-patterns (extra strict)
|
||||
|
||||
- Do NOT collapse multiple probe commands into a single English sentence like "verified via UIA". List every `winapp ui ...` command verbatim in a step row.
|
||||
- Do NOT skip the step table for "trivial" items. Even a 1-step item (e.g. "Get-CmdPalSettings shows EnableDock=true") gets a 1-row table.
|
||||
- Do NOT write screenshot references as `` inside table cells (GitHub renders markdown images poorly in cells). Write them as plain text path: `screenshot: artifacts/L<line>/step-NN-<name>.png`.
|
||||
- Do NOT use "the test passed" as a screenshot caption — describe what's visible (e.g. "Settings page with FZ template grid showing 7 templates").
|
||||
- Do NOT reference screenshots that you didn't actually capture. The final wrap-up `Test-Path` loop (see `references/pre-flight.md` §Final wrap-up step 3) will catch missing files; failing that check means the report is invalid.
|
||||
- Do NOT cite source code line numbers (e.g. `CharacterMappings.cs:273`) without having actually read that line. If you cite source, the path must be real and the line number must contain what you claim.
|
||||
|
||||
## §F — Example item (reference: PR-47211 validation report style)
|
||||
|
||||
```markdown
|
||||
## Item L455 — Activate Quick Accent (left Alt + arrow key) on a character, verify accents popup — **PASS** ✅
|
||||
|
||||
**Admin**: NO | **Clarity**: CLEAR | **Category**: drove full UIA flow + asserted accents popup
|
||||
|
||||
### Verification steps performed
|
||||
|
||||
| # | Step | winapp / probe commands | Evidence / result |
|
||||
|---|---|---|---|
|
||||
| 1 | Locate Settings window | `winapp ui list-windows --json` | `hwnd=263304`, `PowerToys.Settings` PID 31740 |
|
||||
| 2 | Navigate to Quick Accent + expand language flyout | `winapp ui invoke QuickAccentNavItem -w 263304`<br>`winapp ui invoke btn-choosecharacter-1c4d -w 263304` | Page loaded; flyout expanded |
|
||||
| 3 | Enumerate language list + screenshot | `winapp ui inspect btn-choosecharacter-1c4d -w 263304 --depth 5`<br>`winapp ui screenshot -w 263304 -o "artifacts/L455/step-03-language-list.png"` | 38 spoken + 6 special languages, alphabetic. screenshot: `artifacts/L455/step-03-language-list.png` |
|
||||
| 4 | Single-language (French) popup test | `winapp ui invoke itm-french-1cac -w 263304`<br>`winapp ui inspect characters -w <popupHwnd> --depth 3`<br>`winapp ui screenshot -w <popupHwnd> -o "artifacts/L455/step-04-popup-FR-E.png"` | Popup chars for **E** = `é è ê ë €` (5), matches `FR.VK_E` in `CharacterMappings.cs:273`. screenshot: `artifacts/L455/step-04-popup-FR-E.png` |
|
||||
| 5 | Restore baseline | — | settings.json reverted to `selected_lang="ALL"` |
|
||||
|
||||
### Artifacts produced
|
||||
- `artifacts/L455/step-03-language-list.png` — Settings page with expanded language flyout
|
||||
- `artifacts/L455/step-03-language-list.txt` — full UIA inspect dump of the list
|
||||
- `artifacts/L455/step-04-popup-FR-E.png` — Popup with French only: `é è ê ë €`
|
||||
|
||||
### Verdict reasoning
|
||||
- ✅ Popup characters match `CharacterMappings.cs` entries exactly (5/5 for FR.VK_E)
|
||||
- ✅ Popup appeared within 500ms of hold-A; no crash
|
||||
- ✅ Language list ordering is alphabetic by localized name
|
||||
```
|
||||
|
||||
## §G — Retrospective (self-reflection)
|
||||
|
||||
After the run, reflect on the **process** (not the product) so the skill itself gets better over time. **If nothing slowed you down, write exactly one line: `Everything was smooth — no friction encountered.`** Otherwise, list each friction as a row and assign a source + severity.
|
||||
|
||||
```markdown
|
||||
## Retrospective
|
||||
|
||||
| # | Friction (what slowed you / what was wrong) | Source | Severity | Cost | Suggested fix |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | <concrete description — what you expected vs what happened> | <one source tag below> | <HIGH/MED/LOW> | <~min wasted · N attempts> | <the doc line / helper function / tool behavior to change> |
|
||||
```
|
||||
|
||||
**Source** — classify each friction into exactly one bucket so the right owner can fix it:
|
||||
|
||||
| Source tag | Meaning |
|
||||
|---|---|
|
||||
| `SKILL-UNCLEAR` | This skill's `SKILL.md` / `references/pre-flight.md` / module profile guidance was missing, ambiguous, or wrong. |
|
||||
| `WINAPP-TOOL-BUG` | The `winapp` CLI itself misbehaved (crash, wrong output, flag not honored) — a product defect in the tool. |
|
||||
| `WINAPP-DOC-UNCLEAR` | `references/winapp-ui-testing.md` was unclear/incorrect about how to use the tool (the tool worked; the docs misled you). |
|
||||
| `HELPER-FLAW` | A shipped `scripts/*.ps1` had a logic bug, bad default, or wrong assumption. Name the function. |
|
||||
| `PT-PRODUCT` | A PowerToys behavior/quirk made driving hard (distinct from a product **FAIL** — this is friction, not a checklist failure). |
|
||||
| `CHECKLIST` | The checklist item itself was wrong/stale/ambiguous (e.g. describes a renamed or removed control). Note: this usually *also* produces a `FAIL (cause: checklist-*)` verdict on the item; log it here too so the checklist owner sees it as a process-improvement signal. |
|
||||
| `ENVIRONMENT` | RDP/session/desktop/elevation friction not already covered by `references/environment-setup.md`. |
|
||||
|
||||
**Severity** — judge by *impact on future agents*, not just yourself:
|
||||
- **HIGH** — most agents will hit it; blocks progress or wastes >10 min, or you needed a non-obvious workaround.
|
||||
- **MED** — many agents may hit it; cost a few minutes or 2-3 retries; workaround exists once known.
|
||||
- **LOW** — edge case or cosmetic; <1 min; noted for completeness.
|
||||
|
||||
**Cost** — be concrete: approximate minutes wasted **and** number of attempts (e.g. `~8 min · 3 attempts`). This is the raw signal for prioritizing skill fixes.
|
||||
|
||||
**Suggested fix** — point at the specific artifact to change: a doc line/section, a helper function name, or a `winapp` behavior to file. Vague reflections ("docs could be clearer") are not actionable — cite the line.
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
## Retrospective
|
||||
|
||||
| # | Friction | Source | Severity | Cost | Suggested fix |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `winapp ui inspect --depth 7 -w $hwnd` threw "Cannot bind argument" until I moved `-w` after `--depth`. | `WINAPP-TOOL-BUG` | MED | ~6 min · 3 attempts | Already noted in pitfall #14, but the tool should parse flag order — file against winapp. |
|
||||
| 2 | SKILL.md §2.A says "wait 4s debounce" but PowerRename needed a full `Restart-PtRunner`; the module-owned-file note (pitfall #18) wasn't cross-linked from §2.A. | `SKILL-UNCLEAR` | HIGH | ~12 min · 4 attempts | Add an explicit "shell-ext modules → see pitfall #18" pointer inside §2.A. |
|
||||
```
|
||||
@@ -1,531 +0,0 @@
|
||||
# WinUI UI-testing mechanics (winapp ui)
|
||||
|
||||
> **Provenance:** Adapted from the `winui-ui-testing` skill in [microsoft/win-dev-skills](https://github.com/microsoft/win-dev-skills) (MIT, © Microsoft Corporation and Contributors), with PowerToys-specific edits. This is a **reference doc** for the `powertoys-module-verification` skill — it is intentionally not a standalone skill (no frontmatter), so it is not separately discovered.
|
||||
|
||||
Automated UI testing for WinUI 3 apps — generate a batch test script, run all tests in one pass, read results. Covers element assertions, interactions, value checking (TextBox, ComboBox, ToggleSwitch), file pickers, flyouts, dialogs, persistence, and accessibility audits.
|
||||
|
||||
### Approach
|
||||
|
||||
The goal of this skill is to validate UI and app functionality automatically, without manual interaction, by exercising the app's UI elements, verifying their state, and asserting that the app behaves as expected under test conditions.
|
||||
|
||||
There are two main approaches:
|
||||
1. Interactive exploration — manually run the app, use `winapp ui <command>` to explore the UI tree, find AutomationIds, verify element properties, and test functionality interactively. This is useful for discovery, but slow and expensive if repeated for every test iteration.
|
||||
2. Scripted batch testing — generate a `ui-tests.ps1` script that exercises all UI elements and asserts expected behavior in one pass. This allows you to run the tests automatically, capture results, and iterate quickly without manually interacting with the app each time.
|
||||
|
||||
Unless the user asked for interactive exploration, or you are unfamiliar with the code/app or need to explore the UI tree to discover AutomationIds for hidden or dynamically generated elements (flyouts, dialogs, lazy-loaded content), **prefer scripted batch testing** — it is faster, repeatable, and produces a record of pass/fail results that can be reviewed and acted on.
|
||||
|
||||
### `winapp ui` Verbs
|
||||
|
||||
`status`, `inspect`, `search`, `get-property`, `get-value`, `screenshot`, `invoke`, `click`, `set-value`, `focus`, `scroll`, `scroll-into-view`, `wait-for`, `list-windows`, `get-focused`. Run `winapp ui --cli-schema` for the complete command structure as JSON, or `winapp ui <verb> --help` for any single verb.
|
||||
|
||||
### Step 1: Use the Running App
|
||||
|
||||
If the app is already running, use its PID. **Do NOT relaunch** — use the PID already captured from the build step. If the app is not running, build and launch it using the guidance in the winui-dev-workflow skill.
|
||||
|
||||
### Step 2: Write the Test Script
|
||||
|
||||
**If you wrote the code:** Skip inspect — you already know all the AutomationIds and control structure from the XAML and code-behind. Write tests directly from that knowledge. Inspect misses popups, flyouts, dialogs, and lazy-loaded content anyway.
|
||||
|
||||
**If you're verifying code you didn't write:** Run inspect first to discover the UI:
|
||||
```powershell
|
||||
winapp ui inspect -a <PID> --interactive
|
||||
```
|
||||
Then read the XAML files to find AutomationIds that aren't currently visible (flyout items, dialog buttons, secondary pages).
|
||||
|
||||
Create a `ui-tests.ps1` file that tests all the app's requirements in one pass:
|
||||
|
||||
```powershell
|
||||
# ui-tests.ps1
|
||||
param([Parameter(Mandatory)][int]$AppPid)
|
||||
# NOTE: Do NOT name the parameter $Pid — it's read-only in PowerShell
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$pass = 0; $fail = 0; $results = @()
|
||||
|
||||
# Get main window HWND (avoids PopupHost interference with JSON parsing)
|
||||
$windows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json
|
||||
$hwnd = ($windows | Where-Object { $_.title -ne "PopupHost" } | Select-Object -First 1).hwnd
|
||||
|
||||
function Test-UI {
|
||||
param([string]$Name, [scriptblock]$Script)
|
||||
# IMPORTANT: Inside $Script, use 'throw' to signal failure — NOT 'exit 1'
|
||||
# (exit terminates the entire script, not just the test)
|
||||
try {
|
||||
$output = & $Script 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$script:pass++; $script:results += @{ name = $Name; status = "PASS" }
|
||||
} else {
|
||||
$script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$output" }
|
||||
}
|
||||
} catch {
|
||||
$script:fail++; $script:results += @{ name = $Name; status = "FAIL"; detail = "$_" }
|
||||
}
|
||||
}
|
||||
|
||||
# ─── Element Existence ───
|
||||
Test-UI "NavHome exists" { winapp ui wait-for "NavHome" -a $AppPid -t 3000 }
|
||||
Test-UI "NavSettings exists" { winapp ui wait-for "NavSettings" -a $AppPid -t 3000 }
|
||||
|
||||
# ─── Navigation ───
|
||||
Test-UI "Navigate to Settings" { winapp ui invoke "NavSettings" -a $AppPid }
|
||||
Test-UI "Settings page loaded" { winapp ui wait-for "TxtUserName" -a $AppPid -t 3000 }
|
||||
|
||||
# ─── Interactions ───
|
||||
Test-UI "Set username" { winapp ui set-value "TxtUserName" "TestUser" -a $AppPid }
|
||||
Test-UI "Click Save" { winapp ui invoke "BtnSave" -a $AppPid } # commits the TextBox binding
|
||||
Test-UI "Username value set" {
|
||||
winapp ui wait-for "TxtUserName" -a $AppPid --value "TestUser" -t 2000
|
||||
}
|
||||
|
||||
# ─── Value assertions for different control types ───
|
||||
Test-UI "Theme is System default" {
|
||||
winapp ui wait-for "CmbTheme" -a $AppPid --value "System default" -t 2000
|
||||
}
|
||||
Test-UI "Logging is off" {
|
||||
winapp ui wait-for "TglLogging" -a $AppPid --value "Off" -t 2000
|
||||
}
|
||||
|
||||
# ─── Accessibility Audit ───
|
||||
# Only audit controls in the app's main window (exclude OS picker/popup controls)
|
||||
$allElements = (winapp ui inspect -a $AppPid --interactive --json 2>$null | ConvertFrom-Json).elements
|
||||
$appElements = @($allElements | Where-Object {
|
||||
$_.type -match 'Button|TextBox|ComboBox|CheckBox|ToggleSwitch|TabItem|Edit' -and
|
||||
$_.name -notmatch 'Minimize|Maximize|Close|System' -and # window chrome
|
||||
$_.className -notmatch 'PickerHost|#32770|CabinetWClass' # OS dialogs
|
||||
})
|
||||
$missingId = @($appElements | Where-Object { -not $_.automationId })
|
||||
if ($missingId.Count -eq 0) {
|
||||
$pass++; $results += @{ name = "All app controls have AutomationId"; status = "PASS" }
|
||||
} else {
|
||||
$fail++
|
||||
$names = ($missingId | ForEach-Object { "$($_.type) '$($_.name)'" }) -join ", "
|
||||
$results += @{ name = "AutomationId coverage"; status = "FAIL"; detail = "Missing: $names" }
|
||||
}
|
||||
|
||||
# ─── State Screenshots (capture each meaningful state for visual review) ───
|
||||
New-Item -ItemType Directory -Force -Path "screenshots" | Out-Null
|
||||
winapp ui screenshot -a $AppPid -o "screenshots/01-initial.png" 2>$null
|
||||
# ...take more screenshots after key interactions above (mode switches, dialogs opened, etc.)
|
||||
|
||||
# ─── Final Screenshot ───
|
||||
winapp ui screenshot -a $AppPid -o "test-screenshot.png" 2>$null
|
||||
|
||||
# ─── Results ───
|
||||
Write-Host "`nPassed: $pass | Failed: $fail"
|
||||
$results | Where-Object { $_.status -eq "FAIL" } | ForEach-Object {
|
||||
Write-Host " FAIL: $($_.name) — $($_.detail)" -ForegroundColor Red
|
||||
}
|
||||
$results | ConvertTo-Json | Out-File "test-results.json"
|
||||
if ($fail -gt 0) { exit 1 } else { exit 0 }
|
||||
```
|
||||
|
||||
### What to Test
|
||||
|
||||
Write tests for **every requirement** from the user's prompt:
|
||||
|
||||
| Requirement type | Test approach |
|
||||
|---|---|
|
||||
| "Has a button that does X" | `search` to verify exists, `invoke` to click, `wait-for --value` to check result |
|
||||
| "Text field shows value" | `wait-for "TxtName" --value "expected"` — works for TextBox, TextBlock, labels |
|
||||
| "Status bar contains text" | `wait-for "StatusBar" --value "words" --contains` — substring match for dynamic content |
|
||||
| "Dropdown is set to X" | `wait-for "CmbTheme" --value "Dark"` — reads the selected item automatically |
|
||||
| "Toggle is on/off" | `wait-for "TglFeature" --value "On"` — reads the toggle state |
|
||||
| "Navigation between pages" | `invoke` nav item, `wait-for` a page-specific element to appear |
|
||||
| "Open file dialog" | `invoke` trigger, `list-windows` to find picker HWND, interact with `-w` |
|
||||
| "Save file dialog" | Same as open — find picker with `list-windows`, `set-value` filename, `invoke` Save |
|
||||
| "Right-click context menu" | `click --right` on element, `invoke` the flyout MenuItem |
|
||||
| "Confirmation dialog" | `invoke` trigger, `search` for dialog buttons, `invoke` Primary/Secondary/Close |
|
||||
| "Data persists" | Set values, `invoke` a button (to commit bindings), verify data file on disk (`Get-Content` + `ConvertFrom-Json`) |
|
||||
| "All controls accessible" | `inspect --interactive --json` + check all have AutomationId |
|
||||
|
||||
### Step 3: Run and Read Results
|
||||
|
||||
```powershell
|
||||
.\ui-tests.ps1 -AppPid <PID>
|
||||
```
|
||||
|
||||
Read `test-results.json` for structured pass/fail. Only fix code if tests fail.
|
||||
|
||||
### Step 3.5: Look at the Screenshots
|
||||
|
||||
UIA assertions don't see clipping, overlap, wrong theming, or controls bleeding past their container — UIA returns `PASS` while the app is visually broken. **Capture screenshots with `winapp ui screenshot` and view each PNG.**
|
||||
|
||||
Capture the initial state and any state after a major interaction (the State Screenshots block in the script template above handles this).
|
||||
|
||||
**Visual checklist — fail the run if any item is `no`:**
|
||||
- [ ] No unintended scrollbars
|
||||
- [ ] No text ending in `…` that shouldn't be
|
||||
- [ ] Hero elements fully visible (not sliced)
|
||||
- [ ] Right-edge controls fully visible
|
||||
- [ ] No overlapping rows
|
||||
- [ ] Content uses the available width — no asymmetric dead zones (e.g. content pinned to one edge leaving empty space on the other)
|
||||
- [ ] Spacing intentional — not cramped, not unintentionally vast
|
||||
- [ ] Theming matches the user's ask (Light/Dark/HighContrast if relevant)
|
||||
- [ ] Focus/hover/error states render if tested
|
||||
|
||||
If the checklist fails, it's a bug — fix before declaring done. Window too small → grow per `winui-design` Step 4.
|
||||
|
||||
### Step 4: Fix and Rerun (if the user asked for it)
|
||||
|
||||
If tests fail:
|
||||
1. Read the failure details from `test-results.json`
|
||||
2. Batch-fix all issues in one pass
|
||||
3. Rebuild with `.\BuildAndRun.ps1` (blocking mode — shows crash info if the fix broke something)
|
||||
4. Rerun `.\ui-tests.ps1 -AppPid <PID>` (parse PID from the `launched (PID: XXXXX)` output)
|
||||
|
||||
**Maximum 2 fix-and-rerun cycles.** If the same tests keep failing after 2 cycles, report them as known issues and move on — do not keep iterating.
|
||||
|
||||
### Assertion Reference
|
||||
|
||||
Use `wait-for --value` as the primary assertion — it uses a smart fallback chain that reads the right value for any control type:
|
||||
|
||||
| Control type | `--value` reads from | Example |
|
||||
|---|---|---|
|
||||
| TextBlock / Label | Name property | `wait-for "LblTitle" --value "Home"` |
|
||||
| TextBox / NumberBox | ValuePattern | `wait-for "TxtName" --value "John"` |
|
||||
| RichEditBox | TextPattern | `wait-for "Editor" --value "Hello"` |
|
||||
| ComboBox | Selected item (SelectionPattern) | `wait-for "CmbTheme" --value "Dark"` |
|
||||
| ToggleSwitch | Toggle state (On/Off) | `wait-for "TglDark" --value "On"` |
|
||||
| CheckBox | Toggle state (On/Off) | `wait-for "ChkAgree" --value "On"` |
|
||||
|
||||
**Full assertion commands:**
|
||||
|
||||
| Assertion | Command |
|
||||
|---|---|
|
||||
| Element exists | `winapp ui wait-for "Id" -a PID -t 3000` |
|
||||
| Element has exact value | `winapp ui wait-for "Id" -a PID --value "expected" -t 3000` |
|
||||
| Value contains text | `winapp ui wait-for "Id" -a PID --value "words" --contains -t 3000` |
|
||||
| Element gone | `winapp ui wait-for "Id" -a PID --gone -t 3000` |
|
||||
| Specific property | `winapp ui wait-for "Id" -a PID -p IsEnabled --value "True" -t 3000` |
|
||||
| Button clickable | `winapp ui invoke "Id" -a PID` (exit code 0) |
|
||||
| Set then verify | `winapp ui set-value "Id" "text" -a PID` then `wait-for --value` |
|
||||
| Screenshot | `winapp ui screenshot -a PID -o path.png` |
|
||||
| Dialog appeared | `winapp ui list-windows -a PID --json` (check window count) |
|
||||
| Right-click menu | `winapp ui click "Id" -a PID --right` then `wait-for` menu item |
|
||||
| Read raw property | `winapp ui get-property "Id" -a PID -p IsEnabled --json` |
|
||||
| Read current value (no wait) | `(winapp ui get-value "Id" -a PID --json \| ConvertFrom-Json).text` — always pass `--json` when capturing into a variable (plain stdout can include advisory text like "Auto-selected HWND … from N windows"); otherwise prefer `wait-for --value` |
|
||||
| Scroll item into view | `winapp ui scroll-into-view "Id" -a PID` — call before `wait-for` on virtualized ListView/repeater items below the fold |
|
||||
| Set keyboard focus | `winapp ui focus "Id" -a PID` — cleaner than clicking another control to trigger a TextBox `LostFocus` commit |
|
||||
|
||||
### Testing File Pickers
|
||||
|
||||
File/folder pickers (FileOpenPicker, FileSavePicker, FolderPicker) run in a separate `PickerHost` process but are fully interactable. The picker appears as an owned dialog window.
|
||||
|
||||
```powershell
|
||||
# 1. Trigger the picker
|
||||
winapp ui invoke "BtnOpenFile" -a $AppPid
|
||||
|
||||
# 2. Find the picker window (it's a dialog owned by the app window)
|
||||
Start-Sleep 1
|
||||
$allWindows = winapp ui list-windows -a $AppPid --json 2>$null | ConvertFrom-Json
|
||||
$picker = $allWindows | Where-Object { $_.title -match "Open|Save" }
|
||||
$pickerHwnd = $picker.hwnd
|
||||
|
||||
# 3. Interact with the picker using -w <HWND>
|
||||
# Type a filename:
|
||||
winapp ui set-value "FileNameControlHost" "test.txt" -w $pickerHwnd
|
||||
# Click Open/Save:
|
||||
winapp ui invoke "Open" -w $pickerHwnd # or "Save", "Cancel"
|
||||
# Or cancel:
|
||||
winapp ui invoke "Cancel" -w $pickerHwnd
|
||||
|
||||
# 4. Verify the app processed the file
|
||||
winapp ui wait-for "StatusBar" -a $AppPid -p Name --value "opened" -t 3000
|
||||
```
|
||||
|
||||
**Tip:** Use `winapp ui inspect -w <pickerHwnd> --interactive` to discover the picker's controls — they include the folder tree, file list, filename textbox, and Open/Cancel buttons.
|
||||
|
||||
### Testing Context Menus and Flyouts
|
||||
|
||||
MenuFlyouts and ContextFlyouts are fully testable. They appear in the UI automation tree when open.
|
||||
|
||||
```powershell
|
||||
# 1. Right-click to open a ContextFlyout
|
||||
winapp ui click "LstItems" -a $AppPid --right
|
||||
Start-Sleep 0.5
|
||||
|
||||
# 2. The flyout MenuItems appear in the tree immediately
|
||||
# Find them with inspect or search:
|
||||
winapp ui inspect -a $AppPid --interactive # shows MnuCopy, MnuDelete, etc.
|
||||
|
||||
# 3. Click a flyout item
|
||||
winapp ui invoke "MnuCopy" -a $AppPid
|
||||
|
||||
# 4. Verify the action
|
||||
winapp ui wait-for "StatusText" -a $AppPid -p Name --value "Copied" -t 2000
|
||||
```
|
||||
|
||||
**For MenuBar flyouts** (File, Edit, View menus):
|
||||
```powershell
|
||||
# Click the menu header to open
|
||||
winapp ui invoke "FileMenu" -a $AppPid
|
||||
Start-Sleep 0.5
|
||||
# Click the sub-item
|
||||
winapp ui invoke "MenuSaveAs" -a $AppPid
|
||||
```
|
||||
|
||||
### Testing ContentDialogs
|
||||
|
||||
ContentDialogs are in-app controls (same window) — they appear directly in the UI tree when shown.
|
||||
|
||||
```powershell
|
||||
# 1. Trigger the dialog
|
||||
winapp ui invoke "BtnDelete" -a $AppPid
|
||||
Start-Sleep 0.5
|
||||
|
||||
# 2. The dialog buttons appear in the tree
|
||||
# For a standard confirmation dialog:
|
||||
winapp ui search "Primary" -a $AppPid --json # finds the primary button
|
||||
winapp ui invoke "Primary" -a $AppPid # click "Yes"/"Delete"/"Save"
|
||||
# Or:
|
||||
winapp ui invoke "Secondary" -a $AppPid # click "No"/"Don't Save"
|
||||
winapp ui invoke "Close" -a $AppPid # click "Cancel"
|
||||
|
||||
# 3. Wait for dialog to dismiss
|
||||
winapp ui wait-for "Primary" -a $AppPid --gone -t 3000
|
||||
```
|
||||
|
||||
**Tip:** ContentDialog buttons often don't have custom AutomationIds — use `inspect` to find the actual selector (slug or text match).
|
||||
|
||||
### Key Gotchas
|
||||
|
||||
- **`set-value` does NOT commit default TextBox bindings** — WinUI 3 `x:Bind TwoWay` on TextBox.Text updates the ViewModel on `LostFocus` by default. UIA `set-value` changes the text but doesn't trigger focus events. **Fix:** apps should use `UpdateSourceTrigger=PropertyChanged` on TextBox bindings (see design skill). If the app doesn't, `invoke` a button or `click` another element after `set-value` to trigger `LostFocus`.
|
||||
- **Verify persistence via the data file, not UI relaunch** — killing and relaunching a packaged app from a test script is fragile (MSIX registration timing, PID issues). Instead, check the data file on disk: `Get-Content $dataFile | ConvertFrom-Json` and verify expected values.
|
||||
- **Use `$AppPid` not `$Pid`** — `$Pid` is a read-only automatic variable in PowerShell
|
||||
- **Use `--value` without `-p`** — it auto-detects the right UIA pattern (TextPattern → ValuePattern → TogglePattern → SelectionPattern → Name). Only use `-p PropertyName --value` when you need a specific property like `IsEnabled`
|
||||
- **File pickers need `-w <HWND>`** — they run in a separate PickerHost process, so `-a PID` won't find them. Use `list-windows` to discover the picker HWND first
|
||||
- **Flyouts need a short `Start-Sleep`** after triggering — the menu items appear in the tree asynchronously
|
||||
|
||||
### CRITICAL — `invoke` vs `click`: choose the right verb
|
||||
|
||||
**`winapp ui invoke <sel>`** dispatches through UIA's **`InvokePattern` via COM IPC**:
|
||||
- ✅ Bypasses Windows UIPI (User Interface Privilege Isolation)
|
||||
- ✅ Works even when your test runs elevated and the target is non-elevated AppX
|
||||
- ✅ Does NOT steal foreground / does NOT trigger focus-loss handlers
|
||||
- ✅ Works on Buttons, ListItems, ToggleSwitches, CheckBoxes — anything that exposes `InvokePattern` or `TogglePattern`
|
||||
- ❌ Does NOT work on elements without an UIA action pattern (plain Grid, Text, Pane) — error message says "does not support any invoke pattern"
|
||||
|
||||
**`winapp ui click <sel>`** uses Win32 **`SendInput`** under the hood:
|
||||
- ❌ **BLOCKED by UIPI** when source is elevated and target is non-elevated (or any AppX) — error: `SendInput failed — the target window may be elevated`
|
||||
- ❌ Triggers foreground change → can dismiss popups, dialogs, AppX windows that hide on deactivation
|
||||
- ✅ Only use when you genuinely need a synthetic mouse click (e.g. testing mouse hover/right-click flyouts where InvokePattern is unavailable)
|
||||
- ✅ Subject to your process having interactive desktop access
|
||||
|
||||
**Rule of thumb**: try `invoke` first; only fall back to `click` if the target lacks InvokePattern AND you have a non-elevated test runner.
|
||||
|
||||
### CRITICAL — DataTemplate AutomationId vs ListItem InvokePattern
|
||||
|
||||
When XAML binds `AutomationProperties.AutomationId="{x:Bind <DataProperty>}"` inside a `ListView.ItemTemplate`'s `<DataTemplate>`, the AutomationId lives on the **inner Grid (Group)** the template produces — NOT on the outer ListItem the ListView wraps around it. The outer ListItem is what carries `InvokePattern`.
|
||||
|
||||
Concrete example (CmdPal PR #48033 binds Command.Id this way):
|
||||
|
||||
```powershell
|
||||
# This FAILS with "does not support any invoke pattern":
|
||||
winapp ui invoke 'com.microsoft.cmdpal.calculator' -w $hwnd
|
||||
# Element grp-commicrosoftcmd-XXXX (Group) does not support any invoke pattern.
|
||||
# No invokable ancestor was found.
|
||||
|
||||
# This WORKS — find by Name (matches all 3 siblings), pick the ListItem child:
|
||||
$r = winapp ui search 'Calculator' -w $hwnd --json | ConvertFrom-Json
|
||||
$li = $r.matches | Where-Object type -eq 'ListItem' | Select-Object -First 1
|
||||
winapp ui invoke $li.selector -w $hwnd # selector like 'itm-calculator-7e3f'
|
||||
```
|
||||
|
||||
If you encounter "does not support any invoke pattern" while trying to use a data-bound AutomationId, this is almost always the cause. The fix is to search by Name and invoke the sibling ListItem.
|
||||
|
||||
### CRITICAL — Keystroke input that bypasses UIPI (PostMessage)
|
||||
|
||||
`winapp ui` has no `send-keys` verb. For keystroke input into elevated/AppX targets where SendInput fails, use **inline Win32 `PostMessage WM_KEYDOWN/WM_KEYUP`** which goes through the target's message queue without UIPI checks:
|
||||
|
||||
```powershell
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class K {
|
||||
[DllImport("user32.dll", CharSet=CharSet.Auto)]
|
||||
public static extern bool PostMessage(IntPtr h, uint msg, IntPtr wp, IntPtr lp);
|
||||
public const uint WM_KEYDOWN = 0x0100;
|
||||
public const uint WM_KEYUP = 0x0101;
|
||||
}
|
||||
"@
|
||||
|
||||
function Send-KeyToHwnd {
|
||||
param([IntPtr]$Hwnd, [byte]$Vk)
|
||||
[void][K]::PostMessage($Hwnd, [K]::WM_KEYDOWN, [IntPtr]$Vk, [IntPtr]0)
|
||||
Start-Sleep -Milliseconds 30
|
||||
[void][K]::PostMessage($Hwnd, [K]::WM_KEYUP, [IntPtr]$Vk, [IntPtr]0)
|
||||
}
|
||||
|
||||
# Common VK codes:
|
||||
# 0x08 Backspace 0x09 Tab 0x0D Enter 0x1B Escape
|
||||
# 0x25 Left 0x26 Up 0x27 Right 0x28 Down
|
||||
Send-KeyToHwnd -Hwnd $h -Vk 0x28 # Down arrow
|
||||
Send-KeyToHwnd -Hwnd $h -Vk 0x0D # Enter
|
||||
```
|
||||
|
||||
**Caveats**:
|
||||
- WinUI3 apps' raw-input hooks may NOT process some keys via WM_KEYDOWN — `Esc` in particular often goes ignored (use BackButton invoke instead). Arrow keys + Enter typically work for ListView navigation.
|
||||
- PostMessage returns immediately; allow 50-200 ms before reading state.
|
||||
- Repeat `Send-KeyToHwnd` calls work for multi-step navigation (Down × 5 to scroll, then Enter).
|
||||
|
||||
### CRITICAL — Global hotkeys / PowerToys activation chords (SendInput, verified working)
|
||||
|
||||
`PostMessage` above targets a specific window's queue. To fire a **global hotkey** (e.g. a PowerToys activation chord like `Win+Shift+C`) you must inject into the **system input stream** with `SendInput` so the low-level keyboard hook (`WH_KEYBOARD_LL`) sees it. This **works for Win+ chords** — the common belief that "Win+ chords can't be injected" is false; it's almost always a **marshaling bug** (`SendInput` returns `0`, `GetLastError()==87`) from building the `INPUT[]` array in PowerShell. Build the array in C#:
|
||||
|
||||
```powershell
|
||||
Add-Type @"
|
||||
using System; using System.Runtime.InteropServices; using System.Collections.Generic;
|
||||
public static class Inj {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct INPUT { public uint type; public KEYBDINPUT ki; public int p1; public int p2; } // p1/p2 pad the union -> cb=40 on x64
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
||||
[DllImport("user32.dll", SetLastError=true)] static extern uint SendInput(uint n, INPUT[] p, int cb);
|
||||
const uint KEYUP = 0x0002;
|
||||
static INPUT K(ushort vk, bool up){ INPUT i=new INPUT(); i.type=1; i.ki.wVk=vk; i.ki.dwFlags=up?KEYUP:0; return i; }
|
||||
public static uint Chord(ushort[] mods, ushort key){ // mods down -> key tap -> mods up (reverse)
|
||||
var l=new List<INPUT>();
|
||||
foreach(var m in mods) l.Add(K(m,false));
|
||||
l.Add(K(key,false)); l.Add(K(key,true));
|
||||
for(int i=mods.Length-1;i>=0;i--) l.Add(K(mods[i],true));
|
||||
var a=l.ToArray(); return SendInput((uint)a.Length,a,Marshal.SizeOf(typeof(INPUT)));
|
||||
}
|
||||
}
|
||||
"@
|
||||
# LWIN=0x5B CTRL=0x11 SHIFT=0x10 ALT=0x12 ; main key VK from the module's settings.json "code"
|
||||
$sent = [Inj]::Chord([uint16[]]@(0x5B,0x10), [uint16]0x43) # Win+Shift+C (Color Picker)
|
||||
if ($sent -eq 0) { throw "SendInput failed err=$([Runtime.InteropServices.Marshal]::GetLastWin32Error())" }
|
||||
```
|
||||
|
||||
**Caveats**:
|
||||
- The injector must run at the **same or higher integrity level** as the hook owner (PowerToys runner). Default per-user installs run the runner at Medium IL, so a normal shell works; if the runner is elevated, run the injector elevated too (otherwise UIPI silently drops the injection).
|
||||
- Must run in the interactive desktop session.
|
||||
- OS-reserved chords (Win+L, Win+Tab) are consumed by Windows before any hook and cannot be injected this way.
|
||||
- Verify the result via the runner trace log line `… hotkey is invoked from Centralized keyboard hook` (`%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs\runner-log_<date>.log`) and/or the module's observable side-effect (overlay window, spawned editor process).
|
||||
|
||||
### CRITICAL — Verify foreground BEFORE every SendInput targeting a specific window
|
||||
|
||||
`SendInput` injects into the **session-wide** input stream — it goes to whatever IS foreground at the moment. If your target window has lost foreground (very common with AppX windows), the keys silently land in another window (often your own terminal) with no error returned.
|
||||
|
||||
Always check the foreground state immediately before calling `SendInput`. For winapp ui's output, the literal substring `foreground` appears in the line for the foreground window:
|
||||
|
||||
```powershell
|
||||
function Test-AppForeground {
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
|
||||
return ($r -match 'foreground')
|
||||
}
|
||||
|
||||
# Force foreground (works ONCE per session reliably; subsequent attempts may be blocked by
|
||||
# Windows foreground-lock):
|
||||
function Force-AppForeground {
|
||||
param([Parameter(Mandatory)][IntPtr]$Hwnd, [int]$ProcessId)
|
||||
Add-Type -TypeDefinition @'
|
||||
using System; using System.Runtime.InteropServices;
|
||||
public static class Fg {
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
||||
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
||||
[DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int pid);
|
||||
}
|
||||
'@ -EA SilentlyContinue
|
||||
[Fg]::AllowSetForegroundWindow($ProcessId) | Out-Null
|
||||
[Fg]::ShowWindow($Hwnd, 9) | Out-Null # SW_RESTORE
|
||||
$fg = [Fg]::GetForegroundWindow(); $fgPid = 0
|
||||
$fgThread = [Fg]::GetWindowThreadProcessId($fg, [ref]$fgPid)
|
||||
$curThread = [Fg]::GetCurrentThreadId()
|
||||
if ($fgThread -ne 0 -and $fgThread -ne $curThread) { [Fg]::AttachThreadInput($curThread, $fgThread, $true) | Out-Null }
|
||||
[Fg]::BringWindowToTop($Hwnd) | Out-Null
|
||||
[Fg]::SetForegroundWindow($Hwnd) | Out-Null
|
||||
if ($fgThread -ne 0 -and $fgThread -ne $curThread) { [Fg]::AttachThreadInput($curThread, $fgThread, $false) | Out-Null }
|
||||
Start-Sleep -Milliseconds 400
|
||||
}
|
||||
|
||||
# Guard pattern: abort instead of silently sending keys to wrong window
|
||||
if (-not (Test-AppForeground -AppId 'Microsoft.CmdPal.UI')) {
|
||||
Force-AppForeground -Hwnd $h -ProcessId $pid
|
||||
if (-not (Test-AppForeground -AppId 'Microsoft.CmdPal.UI')) {
|
||||
throw 'Cannot force CmdPal foreground; aborting SendInput batch'
|
||||
}
|
||||
}
|
||||
# ... now safe to SendInput ...
|
||||
```
|
||||
|
||||
**Tip**: when foreground cannot be reliably maintained, prefer `winapp ui set-value` (UIA-IPC, no foreground required) or `winapp ui invoke` (UIA InvokePattern, no foreground required) instead of SendInput.
|
||||
|
||||
### CRITICAL — `set-value` bypasses TextChanged for some apps (CmdPal alias detection)
|
||||
|
||||
`winapp ui set-value` writes the value through UIA's ValuePattern, which fires a programmatic value-change event. **It does NOT raise the `TextBox.TextChanged` event** the way real keystrokes do. For apps whose logic listens to `TextChanged` rather than to property changes — most notably CmdPal's alias detection (typing `=`, `<`, `>`, `:`, `$`, `??`, `)` in MainSearchBox triggers navigation to a provider sub-page) — `set-value` will set the text but the alias will NOT activate.
|
||||
|
||||
Workarounds:
|
||||
- For plain queries: `winapp ui set-value` works fine (CmdPal still re-runs all providers on value change).
|
||||
- For alias-triggered navigation: use **real keystrokes** via Force-AppForeground + SendInput, typing one character at a time with ~60-100ms delay so the alias detector sees the TextChanged sequence.
|
||||
- Alternative: invoke the provider tile directly by its stable AutomationId (e.g. `winapp ui invoke 'com.microsoft.cmdpal.calculator' -w $hwnd`) when you only need the destination page, not the alias path.
|
||||
|
||||
### CRITICAL — Stunted UIA tree recovery
|
||||
|
||||
After ~30+ rapid `set-value` calls or after AppX has been interactive too long, an AppX window's UIA tree can degrade to a "stunted" state where `winapp ui inspect -w $h --depth 6` returns only ~5 elements (TitleBar / Close / Min / Max / RootPane) — even though the app looks fine visually.
|
||||
|
||||
Probe + recover pattern:
|
||||
|
||||
```powershell
|
||||
# Probe: any healthy ListView-based AppX has >50 UIA nodes at depth 6
|
||||
$probe = winapp ui inspect -w $h --depth 6 --json | ConvertFrom-Json
|
||||
$nodes = 0
|
||||
$stack = [System.Collections.Stack]::new()
|
||||
if ($probe.windows[0].elements) { foreach ($e in $probe.windows[0].elements) { $stack.Push($e) } }
|
||||
while ($stack.Count -gt 0) {
|
||||
$n = $stack.Pop(); $nodes++
|
||||
if ($n.PSObject.Properties['children']) { foreach ($c in $n.children) { $stack.Push($c) } }
|
||||
}
|
||||
|
||||
if ($nodes -lt 6) {
|
||||
Write-Warning "UIA tree stunted ($nodes nodes); restarting AppX"
|
||||
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | ForEach-Object {
|
||||
Stop-Process -Id $_.Id -Force
|
||||
Wait-Process -Id $_.Id -Timeout 5 -EA SilentlyContinue
|
||||
}
|
||||
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
|
||||
Start-Sleep 5
|
||||
# Re-resolve HWND with list-windows
|
||||
}
|
||||
```
|
||||
|
||||
### Settings.json mutation safety contract
|
||||
|
||||
When the only realistic way to reach a needed test state is editing the app's persistent settings (e.g. multi-select that the UI's `SelectionItemPattern.Select` clobbers), wrap mutations with **byte-identical backup + restore-on-exit**:
|
||||
|
||||
```powershell
|
||||
$settings = "$env:LOCALAPPDATA\Packages\Microsoft.CommandPalette_8wekyb3d8bbwe\LocalState\settings.json"
|
||||
$backup = "$env:TEMP\settings-backup-$(Get-Random).json"
|
||||
$origBytes = [System.IO.File]::ReadAllBytes($settings)
|
||||
[System.IO.File]::WriteAllBytes($backup, $origBytes)
|
||||
try {
|
||||
# 1. Stop the AppX so we can write the file (apps usually hold it open)
|
||||
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | Stop-Process -Force
|
||||
Start-Sleep 1
|
||||
# 2. Mutate
|
||||
$j = $origBytes | ForEach-Object { [char]$_ } | Join-String | ConvertFrom-Json
|
||||
$j.SomeKey = 'TestValue'
|
||||
[System.IO.File]::WriteAllBytes($settings, [System.Text.Encoding]::UTF8.GetBytes(($j | ConvertTo-Json -Depth 10)))
|
||||
# 3. Restart AppX so it re-reads the mutated settings
|
||||
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
|
||||
Start-Sleep 5
|
||||
# 4. ... run your test ...
|
||||
} finally {
|
||||
# ALWAYS restore — verify byte-identical via length + SHA256
|
||||
Get-Process Microsoft.CmdPal.UI -EA SilentlyContinue | Stop-Process -Force -EA SilentlyContinue
|
||||
Start-Sleep 1
|
||||
[System.IO.File]::WriteAllBytes($settings, $origBytes)
|
||||
$check = [System.IO.File]::ReadAllBytes($settings)
|
||||
if ($check.Length -ne $origBytes.Length) { Write-Error "Restore length mismatch!" }
|
||||
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: this should be used ONLY when the UI route is unreachable. Any setting flippable through the AppX Settings UI should be flipped that way instead (it's the documented user flow and tests real binding code).
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# scripts/pt-admin-probe.ps1
|
||||
# Verify the current session is elevated AND that PT runner inherits the admin token.
|
||||
|
||||
if (-not ('PtTok' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class PtTok {
|
||||
[DllImport("advapi32.dll", SetLastError=true)]
|
||||
public static extern bool OpenProcessToken(IntPtr h, uint da, out IntPtr t);
|
||||
[DllImport("advapi32.dll", SetLastError=true)]
|
||||
public static extern bool GetTokenInformation(IntPtr t, uint c, IntPtr ti, uint l, out uint rl);
|
||||
[DllImport("kernel32.dll")] public static extern IntPtr GetCurrentProcess();
|
||||
[DllImport("kernel32.dll")] public static extern IntPtr OpenProcess(uint da, bool inh, int pid);
|
||||
[DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr h);
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
function Test-PtAdmin {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether the current session is elevated by reading the process token's TokenElevation
|
||||
information class (20). Returns $true if elevated.
|
||||
#>
|
||||
[CmdletBinding()] param()
|
||||
$t = [IntPtr]::Zero
|
||||
[PtTok]::OpenProcessToken([PtTok]::GetCurrentProcess(), 8, [ref]$t) | Out-Null
|
||||
$ti = [Runtime.InteropServices.Marshal]::AllocHGlobal(4)
|
||||
$rl = 0
|
||||
try {
|
||||
[PtTok]::GetTokenInformation($t, 20, $ti, 4, [ref]$rl) | Out-Null
|
||||
return ([Runtime.InteropServices.Marshal]::ReadInt32($ti) -eq 1)
|
||||
} finally {
|
||||
[Runtime.InteropServices.Marshal]::FreeHGlobal($ti)
|
||||
[PtTok]::CloseHandle($t) | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ProcessElevated {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether a specific PID is elevated (TokenElevation = 1).
|
||||
#>
|
||||
[CmdletBinding()] param([Parameter(Mandatory)][int]$ProcessId)
|
||||
$proc = [PtTok]::OpenProcess(0x1000, $false, $ProcessId) # PROCESS_QUERY_LIMITED_INFORMATION
|
||||
if ($proc -eq [IntPtr]::Zero) { return $null }
|
||||
try {
|
||||
$t = [IntPtr]::Zero
|
||||
if (-not [PtTok]::OpenProcessToken($proc, 8, [ref]$t)) { return $null }
|
||||
try {
|
||||
$ti = [Runtime.InteropServices.Marshal]::AllocHGlobal(4)
|
||||
$rl = 0
|
||||
try {
|
||||
[PtTok]::GetTokenInformation($t, 20, $ti, 4, [ref]$rl) | Out-Null
|
||||
return ([Runtime.InteropServices.Marshal]::ReadInt32($ti) -eq 1)
|
||||
} finally { [Runtime.InteropServices.Marshal]::FreeHGlobal($ti) }
|
||||
} finally { [PtTok]::CloseHandle($t) | Out-Null }
|
||||
} finally { [PtTok]::CloseHandle($proc) | Out-Null }
|
||||
}
|
||||
|
||||
function Test-PtRunnerAdmin {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether the PT runner (PowerToys.exe) is currently running elevated.
|
||||
.OUTPUTS
|
||||
PSCustomObject with .Found (bool), .Pid (int), .Elevated (bool|$null)
|
||||
#>
|
||||
$pt = Get-Process PowerToys -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $pt) { return [pscustomobject]@{ Found=$false; Pid=$null; Elevated=$null } }
|
||||
[pscustomobject]@{
|
||||
Found = $true
|
||||
Pid = $pt.Id
|
||||
Elevated = (Test-ProcessElevated -ProcessId $pt.Id)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
# scripts/pt-clipboard-diff.ps1
|
||||
# Multi-format clipboard inspection. Used to assert that AdvancedPaste plain-paste actually strips
|
||||
# rich formats while preserving UnicodeText (and similar before/after assertions).
|
||||
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
function Get-PtClipboardFormats {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the list of format names currently on the clipboard (e.g. UnicodeText, HTML Format,
|
||||
Rich Text Format, FileDrop, DeviceIndependentBitmap, etc.).
|
||||
#>
|
||||
$obj = [System.Windows.Forms.Clipboard]::GetDataObject()
|
||||
if (-not $obj) { return @() }
|
||||
return $obj.GetFormats()
|
||||
}
|
||||
|
||||
function Get-PtClipboardText {
|
||||
[System.Windows.Forms.Clipboard]::GetText()
|
||||
}
|
||||
|
||||
function Compare-PtClipboardFormatDiff {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Diff helper. Given a 'before' formats list (from Get-PtClipboardFormats), return:
|
||||
- Added: formats present in current clipboard but not in before
|
||||
- Removed: formats present in before but not in current
|
||||
- Common: formats present in both
|
||||
.EXAMPLE
|
||||
$before = Get-PtClipboardFormats # e.g. UnicodeText + HTML Format + RTF
|
||||
# ... user/script triggers AP plain-paste ...
|
||||
$diff = Compare-PtClipboardFormatDiff -Before $before
|
||||
# $diff.Removed should contain 'HTML Format' and 'Rich Text Format'
|
||||
# $diff.Common should still contain 'UnicodeText'
|
||||
#>
|
||||
param([Parameter(Mandatory)][string[]]$Before)
|
||||
$current = Get-PtClipboardFormats
|
||||
[pscustomobject]@{
|
||||
Before = $Before
|
||||
Current = $current
|
||||
Added = @($current | Where-Object { $_ -notin $Before })
|
||||
Removed = @($Before | Where-Object { $_ -notin $current })
|
||||
Common = @($current | Where-Object { $_ -in $Before })
|
||||
}
|
||||
}
|
||||
|
||||
function Set-PtClipboardRich {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Put HTML + UnicodeText on the clipboard so plain-paste detection has something to strip.
|
||||
Useful as test fixture before invoking AdvancedPaste.PasteAsPlainText.
|
||||
#>
|
||||
param(
|
||||
[string]$Text = 'Hello world',
|
||||
[string]$Html = '<html><body><b>Hello</b> <i>world</i></body></html>'
|
||||
)
|
||||
$obj = New-Object System.Windows.Forms.DataObject
|
||||
$obj.SetText($Text, [System.Windows.Forms.TextDataFormat]::UnicodeText)
|
||||
$obj.SetText($Html, [System.Windows.Forms.TextDataFormat]::Html)
|
||||
[System.Windows.Forms.Clipboard]::SetDataObject($obj, $true)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
# scripts/pt-cmdpal-recycle.ps1
|
||||
# Recover CmdPal AppX from "stuck" states (TextChanged-broken, sub-page hang, foreground-lock).
|
||||
# The helper Microsoft.CmdPal.Ext.PowerToys is kept alive so the CmdPal.Show event listener wiring
|
||||
# is not lost on recycle.
|
||||
|
||||
function Reset-CmdPalAppX {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Kill the Microsoft.CmdPal.UI process and relaunch the AppX. Returns the new HWND or 0 on failure.
|
||||
.NOTES
|
||||
Symptoms requiring this:
|
||||
- set-value MainSearchBox echoes the text but ZERO ListItems appear within 1.5s
|
||||
- winapp ui invoke <button> hangs subsequent inspect calls
|
||||
- Force-PtForeground returns false repeatedly
|
||||
#>
|
||||
$cp = Get-Process Microsoft.CmdPal.UI -ErrorAction SilentlyContinue
|
||||
if ($cp) {
|
||||
Stop-Process -Id $cp.Id -Force
|
||||
$deadline = (Get-Date).AddSeconds(5)
|
||||
while ((Get-Process -Id $cp.Id -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) {
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
}
|
||||
Start-Process 'shell:AppsFolder\Microsoft.CommandPalette_8wekyb3d8bbwe!App'
|
||||
$deadline = (Get-Date).AddSeconds(10)
|
||||
do {
|
||||
Start-Sleep -Milliseconds 300
|
||||
$r = winapp ui list-windows -a Microsoft.CmdPal.UI 2>$null | Out-String
|
||||
if ($r -match 'HWND (\d+):') { return [IntPtr][int64]$matches[1] }
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
return [IntPtr]::Zero
|
||||
}
|
||||
|
||||
function Reset-CmdPalToHome {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Navigate CmdPal back to the home page from any sub-page by invoking BackButton via UIA.
|
||||
CmdPal's Esc handler is unreachable via SendInput from elevated sessions (UIPI), and Esc-via-
|
||||
PostMessage is filtered by the WinUI 3 raw-input hook. BackButton invoke via UIA InvokePattern
|
||||
works regardless.
|
||||
#>
|
||||
$homePlaceholder = 'Search for apps, files and commands'
|
||||
for ($i = 0; $i -lt 6; $i++) {
|
||||
$cur = winapp ui get-value 'MainSearchBox' -a Microsoft.CmdPal.UI 2>$null
|
||||
if ($cur -and ($cur -match [regex]::Escape($homePlaceholder))) { break }
|
||||
winapp ui invoke 'BackButton' -a Microsoft.CmdPal.UI 2>$null | Out-Null
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
# Re-signal Show in case BackButton dismissed the window
|
||||
if (Get-Command Invoke-PtSharedEvent -ErrorAction SilentlyContinue) {
|
||||
try { Invoke-PtSharedEvent -Name 'CmdPal.Show' | Out-Null } catch {}
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
function Test-CmdPalDegraded {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Probe the AppX with a known-good query ('notepad') and verify >=1 ListItem appears within
|
||||
1500ms. Returns $true if degraded (TextChanged-broken).
|
||||
#>
|
||||
Reset-CmdPalToHome
|
||||
winapp ui set-value 'MainSearchBox' 'notepad' -a Microsoft.CmdPal.UI 2>$null | Out-Null
|
||||
$deadline = (Get-Date).AddMilliseconds(1500)
|
||||
do {
|
||||
$insLines = (winapp ui inspect -a Microsoft.CmdPal.UI --depth 7 -i 2>$null) -split "`n"
|
||||
$items = $insLines | Where-Object { $_ -match 'itm-' -and $_ -match 'ListItem' }
|
||||
if ($items.Count -gt 0) {
|
||||
winapp ui set-value 'MainSearchBox' '' -a Microsoft.CmdPal.UI 2>$null | Out-Null
|
||||
return $false
|
||||
}
|
||||
Start-Sleep -Milliseconds 150
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
return $true
|
||||
}
|
||||
|
||||
function Invoke-CmdPalQuery {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Type a query into MainSearchBox after returning to home. Auto-recovers if AppX is degraded.
|
||||
Returns the result items as an array of strings (text lines starting with itm-).
|
||||
.EXAMPLE
|
||||
$items = Invoke-CmdPalQuery -Query 'notepad'
|
||||
if ($items | Where-Object { $_ -match 'Notepad' }) { 'PASS' } else { 'FAIL' }
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$Query, [int]$WaitMs = 800)
|
||||
Reset-CmdPalToHome
|
||||
winapp ui set-value 'MainSearchBox' $Query -a Microsoft.CmdPal.UI 2>$null | Out-Null
|
||||
Start-Sleep -Milliseconds $WaitMs
|
||||
$out = winapp ui inspect -a Microsoft.CmdPal.UI --depth 7 -i 2>$null | Out-String
|
||||
$items = ($out -split "`r?`n" | Where-Object { $_ -match 'itm-' -and $_ -match 'ListItem' })
|
||||
if ($items.Count -eq 0) {
|
||||
if (Test-CmdPalDegraded) {
|
||||
Reset-CmdPalAppX | Out-Null
|
||||
return (Invoke-CmdPalQuery -Query $Query -WaitMs $WaitMs)
|
||||
}
|
||||
}
|
||||
return $items
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
# scripts/pt-explorer-com.ps1
|
||||
# Drive Explorer windows via Shell.Application COM to set up file selections, then trigger
|
||||
# PT modules that read IShellItemArray from the foreground Explorer window (Peek, Image Resizer,
|
||||
# PowerRename, File Locksmith, Workspaces).
|
||||
#
|
||||
# This bypasses needing a real mouse / interactive selection — Shell COM does the selection
|
||||
# programmatically, then the PT hotkey (e.g. Ctrl+Space for Peek) fires the centralized hook
|
||||
# which reads Explorer's selection at the moment of activation.
|
||||
#
|
||||
# Requires an interactive desktop session. If GetForegroundWindow() returns 0 or no Explorer
|
||||
# windows are open, the functions return $null/$false instead of throwing — callers should
|
||||
# treat that as a BLK-ENV signal (an environment block, not a product FAIL).
|
||||
|
||||
function Get-PtExplorerWindows {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return all open Explorer windows as Shell COM objects (with .LocationName, .Document.Folder, etc.).
|
||||
Returns @() if no Explorer windows are open.
|
||||
#>
|
||||
try {
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
return @($shell.Windows() | Where-Object { $_.Name -eq 'File Explorer' -or $_.FullName -match 'explorer\.exe$' })
|
||||
} catch { return @() }
|
||||
}
|
||||
|
||||
function Open-PtExplorerAtPath {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Open a fresh Explorer window at the given path. Returns the Shell COM window object.
|
||||
Useful when no Explorer is open yet.
|
||||
#>
|
||||
[CmdletBinding()] param([Parameter(Mandatory)][string]$Path)
|
||||
if (-not (Test-Path $Path)) { throw "Path not found: $Path" }
|
||||
Start-Process explorer.exe -ArgumentList $Path
|
||||
Start-Sleep -Milliseconds 1500
|
||||
$wins = Get-PtExplorerWindows
|
||||
# Note: the -replace must be wrapped in its own parens, otherwise the ',' in -replace '\\','/'
|
||||
# is parsed as a second argument to [regex]::Escape() (overload error: "argument count: 2").
|
||||
$needle = [regex]::Escape(((Resolve-Path $Path).Path -replace '\\','/'))
|
||||
return ($wins | Where-Object { $_.LocationURL -match $needle } | Select-Object -First 1)
|
||||
}
|
||||
|
||||
function Select-PtExplorerFiles {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Select 1+ files in an open Explorer window via Shell COM. The window comes to foreground.
|
||||
.DESCRIPTION
|
||||
Uses Shell.Application's SelectItem(item, flags) API. Flags:
|
||||
0x01 = SVSI_SELECT
|
||||
0x04 = SVSI_DESELECTOTHERS (apply to the first item only when selecting multiple)
|
||||
0x08 = SVSI_ENSUREVISIBLE
|
||||
0x20 = SVSI_FOCUSED
|
||||
Returns $true on success, $false if any file wasn't found in the folder.
|
||||
.EXAMPLE
|
||||
$win = Get-PtExplorerWindows | Select-Object -First 1
|
||||
Select-PtExplorerFiles -ExplorerWindow $win -FileNames 'test-markdown.md','test-html.html','test-source.cs'
|
||||
Send-PtChord -Mods 0x11 -Key 0x20 # Ctrl+Space → Peek opens on 3 selected files
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]$ExplorerWindow,
|
||||
[Parameter(Mandatory)][string[]]$FileNames
|
||||
)
|
||||
if (-not $ExplorerWindow.Document) { return $false }
|
||||
$folder = $ExplorerWindow.Document.Folder
|
||||
$first = $true
|
||||
foreach ($name in $FileNames) {
|
||||
$item = $folder.ParseName($name)
|
||||
if (-not $item) { Write-Warning "File not found in folder: $name"; return $false }
|
||||
# First item: SELECT + DESELECTOTHERS + ENSUREVISIBLE + FOCUSED = 0x2D
|
||||
# Subsequent items: SELECT + ENSUREVISIBLE = 0x09
|
||||
$flags = if ($first) { 0x2D } else { 0x09 }
|
||||
$ExplorerWindow.Document.SelectItem($item, $flags)
|
||||
$first = $false
|
||||
}
|
||||
Start-Sleep -Milliseconds 300
|
||||
return $true
|
||||
}
|
||||
|
||||
function Invoke-PtPeekWithExplorerSelection {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Set up an Explorer multi-file selection and trigger Peek via Ctrl+Space.
|
||||
Returns the new Peek window HWND, or $null on failure.
|
||||
.EXAMPLE
|
||||
$h = Invoke-PtPeekWithExplorerSelection -FolderPath D:\fixtures -FileNames 'a.png','b.md','c.cs'
|
||||
winapp ui invoke PinButton -w $h
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$FolderPath,
|
||||
[Parameter(Mandatory)][string[]]$FileNames
|
||||
)
|
||||
$win = Get-PtExplorerWindows | Where-Object { $_.LocationURL -match [regex]::Escape(($FolderPath -replace '\\','/')) } | Select-Object -First 1
|
||||
if (-not $win) { $win = Open-PtExplorerAtPath -Path $FolderPath }
|
||||
if (-not $win) { return $null }
|
||||
if (-not (Select-PtExplorerFiles -ExplorerWindow $win -FileNames $FileNames)) { return $null }
|
||||
|
||||
# Capture pre-state Peek HWND list to detect the new window
|
||||
$beforeHwnds = @(Get-Process PowerToys.Peek.UI -EA SilentlyContinue | ForEach-Object MainWindowHandle)
|
||||
|
||||
# Fire Ctrl+Space (Peek default). Requires pt-sendinput-chord.ps1 to be dot-sourced first.
|
||||
if (-not (Get-Command Send-PtChord -EA SilentlyContinue)) {
|
||||
throw "Send-PtChord not loaded. Dot-source scripts/pt-sendinput-chord.ps1 first."
|
||||
}
|
||||
Send-PtChord -Mods 0x11 -Key 0x20 | Out-Null # Ctrl+Space
|
||||
Start-Sleep -Milliseconds 1200
|
||||
|
||||
# Find the new Peek window HWND
|
||||
$afterHwnds = @(Get-Process PowerToys.Peek.UI -EA SilentlyContinue | ForEach-Object MainWindowHandle)
|
||||
$new = $afterHwnds | Where-Object { $_ -ne 0 -and $_ -notin $beforeHwnds } | Select-Object -First 1
|
||||
if (-not $new) { $new = $afterHwnds | Where-Object { $_ -ne 0 } | Select-Object -First 1 }
|
||||
return $new
|
||||
}
|
||||
|
||||
function Test-PtInteractiveDesktop {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Probe whether the current session is interactive (foreground + Shell COM both working).
|
||||
Returns a PSCustomObject with .ForegroundOk and .ShellComOk.
|
||||
.EXAMPLE
|
||||
$env = Test-PtInteractiveDesktop
|
||||
if (-not $env.ForegroundOk -or -not $env.ShellComOk) {
|
||||
Write-Warning "Non-interactive session — Explorer-driven techniques will fail."
|
||||
}
|
||||
#>
|
||||
Add-Type 'using System; using System.Runtime.InteropServices; public class FG3 { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }' -EA SilentlyContinue
|
||||
$hasFg = $false
|
||||
for ($i = 0; $i -lt 5; $i++) {
|
||||
if ([FG3]::GetForegroundWindow() -ne [IntPtr]::Zero) { $hasFg = $true; break }
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
$shellOk = $false
|
||||
try { @((New-Object -ComObject Shell.Application).Windows()).Count | Out-Null; $shellOk = $true } catch {}
|
||||
[pscustomobject]@{ ForegroundOk = $hasFg; ShellComOk = $shellOk }
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
# pt-explorer-contextmenu.ps1 — drive any Explorer (Win11) context-menu PowerToys module
|
||||
# end-to-end the way a real user does: open Explorer, select file(s), synthetic right-click
|
||||
# to OPEN the menu, then UIA-invoke the module's menu item by NAME (robust — no coordinate
|
||||
# click). Used by File Locksmith, Image Resizer, PowerRename, New+, etc.
|
||||
#
|
||||
# See explorer-context-menu-flow.md for the full write-up, stability notes, and per-module captions.
|
||||
#
|
||||
# Requires an UNLOCKED interactive desktop (synthetic right-click needs foreground). Check first:
|
||||
# if ([PtFg]::GetForegroundWindow() -eq [IntPtr]::Zero) -> desktop locked -> BLK-ENV.
|
||||
|
||||
Add-Type -TypeDefinition @'
|
||||
using System; using System.Runtime.InteropServices;
|
||||
public static class PtCtx {
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int c);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
||||
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
||||
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] public static extern void mouse_event(uint f, uint dx, uint dy, uint d, IntPtr e);
|
||||
public const uint RIGHTDOWN=0x0008, RIGHTUP=0x0010, LEFTDOWN=0x0002, LEFTUP=0x0004;
|
||||
public static void ForceForeground(IntPtr h) {
|
||||
IntPtr fg = GetForegroundWindow(); uint fp;
|
||||
uint ft = GetWindowThreadProcessId(fg, out fp); uint ct = GetCurrentThreadId();
|
||||
ShowWindow(h, 9);
|
||||
if (ft != 0 && ft != ct) AttachThreadInput(ct, ft, true);
|
||||
BringWindowToTop(h); SetForegroundWindow(h);
|
||||
if (ft != 0 && ft != ct) AttachThreadInput(ct, ft, false);
|
||||
}
|
||||
public static void RightClick(int x, int y) {
|
||||
SetCursorPos(x, y); System.Threading.Thread.Sleep(250);
|
||||
mouse_event(RIGHTDOWN,0,0,0,IntPtr.Zero); System.Threading.Thread.Sleep(70); mouse_event(RIGHTUP,0,0,0,IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
'@ -ErrorAction SilentlyContinue
|
||||
|
||||
function Test-PtDesktopInteractive {
|
||||
# Polls up to $TimeoutSec for a foreground window. A momentary 0 is common for a few seconds
|
||||
# right after Restart-PtRunner / Explorer restart — without the poll that blip is misclassified
|
||||
# as a locked desktop (false BLK-ENV). A genuinely locked/non-interactive desktop stays 0 for
|
||||
# the whole window and still returns $false.
|
||||
param([int]$TimeoutSec = 5)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
if ([PtCtx]::GetForegroundWindow() -ne [IntPtr]::Zero) { return $true }
|
||||
Start-Sleep -Milliseconds 250
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
return $false
|
||||
}
|
||||
|
||||
# Opens the Win11 context menu for a file in an already-open Explorer window and returns the
|
||||
# menu popup HWND. $ExplorerHwnd = the CabinetWClass window; $FileName = item to right-click.
|
||||
function Open-PtExplorerContextMenu {
|
||||
param([Parameter(Mandatory)][int]$ExplorerHwnd, [Parameter(Mandatory)][string]$FileName, [int]$MaxTries = 3)
|
||||
if (-not (Test-PtDesktopInteractive)) { throw 'BLK-ENV: desktop is locked / no foreground (GetForegroundWindow()=0). Unlock and retry.' }
|
||||
for ($try = 1; $try -le $MaxTries; $try++) {
|
||||
[PtCtx]::ForceForeground([IntPtr]$ExplorerHwnd); Start-Sleep -Milliseconds 500
|
||||
$item = (winapp ui search $FileName -w $ExplorerHwnd --json 2>$null | ConvertFrom-Json).matches |
|
||||
Where-Object { $_.type -eq 'ListItem' } | Select-Object -First 1
|
||||
if (-not $item) { throw "File item '$FileName' not found in Explorer window $ExplorerHwnd" }
|
||||
# Right-click near the row's LEFT edge (on the filename), not the geometric center:
|
||||
# in Details view the ListItem rect spans ~full row width (measured 71% of window), so
|
||||
# x+width/2 lands far right over other columns / empty canvas → background menu or missed
|
||||
# click. x + min(80, width/2) is on the filename in Details AND on the tile in Icons view.
|
||||
[PtCtx]::RightClick([int]($item.x + [Math]::Min(80, $item.width/2)), [int]($item.y + $item.height/2))
|
||||
Start-Sleep -Seconds 2
|
||||
# The Win11 menu is its own top-level popup window:
|
||||
$menu = winapp ui list-windows --json 2>$null | ConvertFrom-Json |
|
||||
Where-Object { $_.className -match 'PopupWindowSiteBridge' } | Sort-Object height -Descending | Select-Object -First 1
|
||||
if ($menu) { return $menu.hwnd }
|
||||
Start-Sleep -Milliseconds 500 # retry: foreground/menu wasn't ready (common on the first attempt right after Explorer opens)
|
||||
}
|
||||
throw "Context-menu popup window not found after $MaxTries right-click attempts"
|
||||
}
|
||||
|
||||
# Invokes a context-menu item by its visible NAME (robust — UIA InvokePattern, no coord click).
|
||||
# Returns $true if invoked. Match the module caption, e.g.:
|
||||
# File Locksmith -> 'Unlock with File Locksmith' PowerRename -> 'Rename with PowerRename'
|
||||
# Image Resizer -> 'Resize images' (verify by enumerating) New+ -> 'New+'
|
||||
function Invoke-PtContextMenuItem {
|
||||
param([Parameter(Mandatory)][int]$MenuHwnd, [Parameter(Mandatory)][string]$ItemName)
|
||||
$m = (winapp ui search $ItemName -w $MenuHwnd --json 2>$null | ConvertFrom-Json).matches |
|
||||
Where-Object { $_.type -eq 'MenuItem' } | Select-Object -First 1
|
||||
if (-not $m) { return $false } # caller can treat $false as "entry absent" (e.g. module disabled)
|
||||
winapp ui invoke $m.selector -w $MenuHwnd 2>$null | Out-Null
|
||||
return $true
|
||||
}
|
||||
|
||||
# Lists all context-menu item names (for discovering a module's caption or asserting absence).
|
||||
function Get-PtContextMenuItems {
|
||||
param([Parameter(Mandatory)][int]$MenuHwnd)
|
||||
winapp ui inspect -w $MenuHwnd --depth 8 2>$null | Out-String |
|
||||
Select-String 'MenuItem "([^"]+)"' -AllMatches | ForEach-Object { $_.Matches } | ForEach-Object { $_.Groups[1].Value }
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
# scripts/pt-foreground-guard.ps1
|
||||
# Verify and force a window to foreground BEFORE sending SendInput.
|
||||
# Without this guard, SendInput keys silently leak to the caller's terminal when
|
||||
# the target window has lost foreground (common with CmdPal AppX where Windows
|
||||
# foreground-lock blocks SetForegroundWindow after the first attempt).
|
||||
#
|
||||
# Use winapp ui set-value for UIA-friendly inputs (no foreground required).
|
||||
# Use this guard ONLY when you need real keystrokes (e.g. CmdPal alias detection).
|
||||
|
||||
if (-not ('PtFg' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class PtFg {
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
||||
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);
|
||||
[DllImport("user32.dll")] public static extern bool AllowSetForegroundWindow(int pid);
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
function Test-PtForeground {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether the target AppX is currently foreground by parsing winapp ui list-windows output
|
||||
for the literal substring 'foreground'.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
|
||||
return ($r -match 'foreground')
|
||||
}
|
||||
|
||||
function Get-PtHwnd {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the first HWND for the given AppX/exe. Returns [IntPtr]::Zero if none.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
$r = winapp ui list-windows -a $AppId 2>$null | Out-String
|
||||
if ($r -match 'HWND (\d+):') { return [IntPtr][int64]$matches[1] }
|
||||
return [IntPtr]::Zero
|
||||
}
|
||||
|
||||
function Force-PtForeground {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Force the target AppX window to foreground using the AttachThreadInput + AllowSetForegroundWindow
|
||||
trick. Returns $true if window is foreground after this attempt; $false otherwise.
|
||||
.NOTES
|
||||
Windows foreground-lock will block subsequent SetForegroundWindow calls in the same session if
|
||||
a real interactive event hasn't fired recently. If this returns $false repeatedly, the only
|
||||
reliable recovery is to recycle the AppX (kill + relaunch via shell:AppsFolder URI).
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
$h = Get-PtHwnd -AppId $AppId
|
||||
if ($h -eq [IntPtr]::Zero) { return $false }
|
||||
|
||||
# Permission grant
|
||||
$proc = Get-Process | Where-Object { $_.MainWindowHandle -eq $h } | Select-Object -First 1
|
||||
if ($proc) { [PtFg]::AllowSetForegroundWindow($proc.Id) | Out-Null }
|
||||
|
||||
[PtFg]::ShowWindow($h, 9) | Out-Null # SW_RESTORE
|
||||
Start-Sleep -Milliseconds 150
|
||||
|
||||
# AttachThreadInput trick
|
||||
$fg = [PtFg]::GetForegroundWindow()
|
||||
$fgPid = 0
|
||||
$fgThread = [PtFg]::GetWindowThreadProcessId($fg, [ref]$fgPid)
|
||||
$curThread = [PtFg]::GetCurrentThreadId()
|
||||
if ($fgThread -ne 0 -and $fgThread -ne $curThread) {
|
||||
[PtFg]::AttachThreadInput($curThread, $fgThread, $true) | Out-Null
|
||||
}
|
||||
[PtFg]::BringWindowToTop($h) | Out-Null
|
||||
[PtFg]::SetForegroundWindow($h) | Out-Null
|
||||
[PtFg]::ShowWindow($h, 5) | Out-Null # SW_SHOW
|
||||
if ($fgThread -ne 0 -and $fgThread -ne $curThread) {
|
||||
[PtFg]::AttachThreadInput($curThread, $fgThread, $false) | Out-Null
|
||||
}
|
||||
Start-Sleep -Milliseconds 400
|
||||
return (Test-PtForeground -AppId $AppId)
|
||||
}
|
||||
|
||||
function Assert-PtForegroundOrAbort {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Guard helper. Throws if the target AppX is NOT foreground. Use this immediately before any
|
||||
SendInput call to ensure keys don't leak to the wrong window.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$AppId)
|
||||
if (-not (Test-PtForeground -AppId $AppId)) {
|
||||
if (-not (Force-PtForeground -AppId $AppId)) {
|
||||
throw "ABORT: $AppId is not foreground and cannot be forced foreground. SendInput would leak to wrong window."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
# pt-nonelevated.ps1 — launch a process at MEDIUM integrity (non-elevated) from an
|
||||
# already-elevated agent shell. Needed for tests that assert elevation-dependent
|
||||
# visibility (e.g. File Locksmith L649/L650: a non-elevated module must NOT see
|
||||
# higher-integrity processes; an elevated one must).
|
||||
#
|
||||
# The drive-stack in SKILL.md only covers gaining MORE privilege. De-elevation is the
|
||||
# opposite problem: from a High-IL shell you cannot simply CreateProcess a Medium-IL
|
||||
# child. The robust, dependency-free way is a one-shot Scheduled Task registered with
|
||||
# RunLevel=Limited + LogonType=Interactive, which lands on the logged-on user's desktop
|
||||
# at their filtered (medium) token. (The classic explorer-shell-injection trick is more
|
||||
# fragile across sessions.)
|
||||
#
|
||||
# Functions:
|
||||
# Start-PtNonElevated -Exe <path> [-Arguments <str>] -> launches a GUI/console exe non-elevated, returns the spawned PID(s)
|
||||
# Invoke-PtNonElevatedCapture -Exe <path> -Arguments <str> -OutFile <path> -> runs a console exe non-elevated, redirects stdout/err to a file, waits, returns the file path
|
||||
#
|
||||
# Verify elevation of the result with Test-ProcessElevated (scripts/pt-admin-probe.ps1).
|
||||
|
||||
function Start-PtNonElevated {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Exe,
|
||||
[string]$Arguments = '',
|
||||
[int]$WaitSeconds = 5,
|
||||
[string]$MatchProcessName # optional: base name to enumerate spawned PIDs (e.g. 'PowerToys.FileLocksmithUI')
|
||||
)
|
||||
if (-not (Test-Path $Exe)) { throw "Exe not found: $Exe" }
|
||||
$taskName = "PtNonElev_$([guid]::NewGuid().ToString('N').Substring(0,8))"
|
||||
$before = @()
|
||||
if ($MatchProcessName) { $before = @(Get-Process -Name $MatchProcessName -EA SilentlyContinue | Select-Object -Expand Id) }
|
||||
try {
|
||||
$action = New-ScheduledTaskAction -Execute $Exe -Argument $Arguments
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -RunLevel Limited -LogonType Interactive
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
Start-Sleep -Seconds $WaitSeconds
|
||||
if ($MatchProcessName) {
|
||||
$after = @(Get-Process -Name $MatchProcessName -EA SilentlyContinue | Select-Object -Expand Id)
|
||||
return ($after | Where-Object { $_ -notin $before })
|
||||
}
|
||||
return $null
|
||||
}
|
||||
finally {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -EA SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PtNonElevatedCapture {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Exe,
|
||||
[string]$Arguments = '',
|
||||
[Parameter(Mandatory)][string]$OutFile,
|
||||
[int]$TimeoutSeconds = 30
|
||||
)
|
||||
if (-not (Test-Path $Exe)) { throw "Exe not found: $Exe" }
|
||||
Remove-Item $OutFile -EA SilentlyContinue
|
||||
$wrap = [IO.Path]::ChangeExtension($OutFile, '.cmd')
|
||||
"`"$Exe`" $Arguments > `"$OutFile`" 2>&1" | Set-Content -Encoding ascii $wrap
|
||||
$taskName = "PtNonElev_$([guid]::NewGuid().ToString('N').Substring(0,8))"
|
||||
try {
|
||||
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument "/c `"$wrap`""
|
||||
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -RunLevel Limited -LogonType Interactive
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
do { Start-Sleep 1; $info = Get-ScheduledTaskInfo -TaskName $taskName }
|
||||
while ($info.LastTaskResult -eq 267009 -and (Get-Date) -lt $deadline) # 267009 = task still running
|
||||
Start-Sleep 1
|
||||
return $OutFile
|
||||
}
|
||||
finally {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -EA SilentlyContinue
|
||||
Remove-Item $wrap -EA SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
# scripts/pt-sendinput-chord.ps1
|
||||
# Inject a global hotkey chord (e.g. Win+Shift+/) into the system input stream.
|
||||
# Critical: INPUT struct MUST be cb=40 on x64 (with padding for the MOUSEINPUT union member).
|
||||
# The common bug "Win+ hotkeys can't be injected" is a marshaling error producing 32-byte struct
|
||||
# and SendInput returns 0 with GetLastError()==87 (ERROR_INVALID_PARAMETER).
|
||||
#
|
||||
# This SHOULD be a last resort. Prefer Named Events (Invoke-PtSharedEvent) when the module exposes one.
|
||||
# Use this only for: (a) explicit hotkey-trigger verification tests, (b) modules without Named Events,
|
||||
# (c) UI keystrokes inside an already-foreground window (use Send-KeyToHwnd via PostMessage instead
|
||||
# for elevated -> non-elevated AppX, see references/winapp-ui-testing.md).
|
||||
|
||||
if (-not ('PtChord' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Collections.Generic;
|
||||
public static class PtChord {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct INPUT { public uint type; public KEYBDINPUT ki; public int pad1; public int pad2; } // pad to 40 bytes
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
|
||||
[DllImport("user32.dll", SetLastError=true)]
|
||||
static extern uint SendInput(uint n, INPUT[] p, int cb);
|
||||
const uint KEYUP = 0x0002;
|
||||
static INPUT K(ushort vk, bool up) { INPUT i=new INPUT(); i.type=1; i.ki.wVk=vk; i.ki.dwFlags=up?KEYUP:0; return i; }
|
||||
public static uint Chord(ushort[] mods, ushort key) {
|
||||
var l=new List<INPUT>();
|
||||
foreach(var m in mods) l.Add(K(m,false));
|
||||
l.Add(K(key,false)); l.Add(K(key,true));
|
||||
for(int i=mods.Length-1;i>=0;i--) l.Add(K(mods[i],true));
|
||||
var a=l.ToArray();
|
||||
return SendInput((uint)a.Length, a, Marshal.SizeOf(typeof(INPUT)));
|
||||
}
|
||||
public static uint Tap(ushort key) { return Chord(new ushort[0], key); }
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
# Common VK codes for chord mods:
|
||||
# LWIN=0x5B RWIN=0x5C CTRL=0x11 SHIFT=0x10 ALT=0x12
|
||||
# Main key VKs:
|
||||
# 0x08 Backspace 0x09 Tab 0x0D Enter 0x1B Escape 0x20 Space
|
||||
# 0x25 Left 0x26 Up 0x27 Right 0x28 Down
|
||||
# 0x30..0x39 0..9 0x41..0x5A A..Z
|
||||
|
||||
function Send-PtChord {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Inject a hotkey chord. Returns number of inputs Windows accepted (0 = failed; check GetLastError).
|
||||
.EXAMPLE
|
||||
Send-PtChord -Mods 0x5B,0x10 -Key 0x43 # Win+Shift+C (Color Picker)
|
||||
Send-PtChord -Mods 0x5B,0x11 -Key 0x52 # Win+Ctrl+R (PowerOcr)
|
||||
Send-PtChord -Mods 0x5B,0xA4 -Key 0x20 # Win+Alt+Space (CmdPal default)
|
||||
Send-PtChord -Key 0x0D # plain Enter (no mods)
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[uint16[]]$Mods = @(),
|
||||
[Parameter(Mandatory)][uint16]$Key
|
||||
)
|
||||
$sent = [PtChord]::Chord($Mods, $Key)
|
||||
if ($sent -eq 0) {
|
||||
$err = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
||||
throw "SendInput failed (returned 0, GetLastError=$err). Likely caller is at lower integrity than PT runner, or chord is OS-reserved (Win+L, Win+Tab)."
|
||||
}
|
||||
return $sent
|
||||
}
|
||||
|
||||
function Wait-PtHotkeyAccepted {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
After Send-PtChord, verify the PT runner saw it by tailing its log for the centralized-hook line.
|
||||
Returns the matching log line (if any) within $TimeoutSec.
|
||||
.EXAMPLE
|
||||
Send-PtChord -Mods 0x5B,0x10 -Key 0x43
|
||||
$line = Wait-PtHotkeyAccepted -ModuleHint 'Color' -TimeoutSec 3
|
||||
if (-not $line) { throw "Runner did not log hotkey invocation" }
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param([string]$ModuleHint = '', [int]$TimeoutSec = 3)
|
||||
$log = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\PowerToys\RunnerLogs" -Filter 'runner-log_*.log' -EA SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $log) { return $null }
|
||||
$start = (Get-Date).AddSeconds(-2)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
$line = Get-Content $log.FullName -Tail 50 -EA SilentlyContinue |
|
||||
Where-Object { $_ -match 'hotkey is invoked from Centralized keyboard hook' -and ($ModuleHint -eq '' -or $_ -match $ModuleHint) } |
|
||||
Select-Object -Last 1
|
||||
if ($line) { return $line }
|
||||
Start-Sleep -Milliseconds 200
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
return $null
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
# pt-session-diagnose.ps1
|
||||
# Diagnose whether the current shell can drive interactive PowerToys tests.
|
||||
# Tells you in one go: am I on the active console session, can I see foreground windows,
|
||||
# and can I use Shell COM. If not, prints the exact psexec mitigation command.
|
||||
|
||||
Add-Type 'using System; using System.Runtime.InteropServices;
|
||||
public class WTS { [DllImport("kernel32.dll")] public static extern uint WTSGetActiveConsoleSessionId(); }
|
||||
public class FG { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }'
|
||||
|
||||
Write-Host "--- Logged-on users + sessions ---" -ForegroundColor Cyan
|
||||
quser 2>&1
|
||||
|
||||
Write-Host "`n--- This shell's session ---" -ForegroundColor Cyan
|
||||
$me = [Diagnostics.Process]::GetCurrentProcess()
|
||||
" PID: $($me.Id)"
|
||||
" Session: $($me.SessionId)"
|
||||
|
||||
Write-Host "`n--- Console Explorer session(s) ---" -ForegroundColor Cyan
|
||||
$exps = Get-Process explorer -ErrorAction SilentlyContinue
|
||||
if ($exps) {
|
||||
$exps | Select-Object Id, SessionId, @{N='StartTime';E={$_.StartTime}} | Format-Table -AutoSize
|
||||
} else {
|
||||
Write-Host " (no explorer.exe running)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "`n--- Windows active console + foreground + Shell COM ---" -ForegroundColor Cyan
|
||||
$activeConsole = [WTS]::WTSGetActiveConsoleSessionId()
|
||||
$fg = [FG]::GetForegroundWindow()
|
||||
$shellOk = $false
|
||||
try { @((New-Object -ComObject Shell.Application).Windows()).Count | Out-Null; $shellOk = $true } catch {}
|
||||
" WTSGetActiveConsoleSessionId() = $activeConsole"
|
||||
" GetForegroundWindow() = $fg"
|
||||
" Shell.Application available = $shellOk"
|
||||
|
||||
Write-Host "`n--- Verdict ---" -ForegroundColor Cyan
|
||||
$consoleSession = ($exps | Select-Object -First 1).SessionId
|
||||
if ($me.SessionId -eq $consoleSession -and $fg -ne 0 -and $shellOk) {
|
||||
Write-Host " PASS - this shell can drive interactive PowerToys tests." -ForegroundColor Green
|
||||
} elseif ($me.SessionId -eq $consoleSession -and $fg -eq 0) {
|
||||
Write-Host " WARN - same session as explorer but no foreground (workstation locked?). Unlock and re-run." -ForegroundColor Yellow
|
||||
} elseif (-not $shellOk) {
|
||||
Write-Host " FAIL - Shell COM unavailable (likely Session 0 / service context). Very few tests possible." -ForegroundColor Red
|
||||
} else {
|
||||
Write-Host " FAIL - shell in Session $($me.SessionId), console explorer in Session $consoleSession. Input injection denied." -ForegroundColor Red
|
||||
Write-Host " Mitigation: relaunch in the console session with:" -ForegroundColor Yellow
|
||||
Write-Host " psexec -accepteula -h -i $consoleSession -s pwsh.exe" -ForegroundColor Yellow
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
# scripts/pt-shared-events.ps1
|
||||
# Signal PowerToys modules via Win32 named events.
|
||||
# Catalog source: PowerToys repo src/common/interop/shared_constants.h
|
||||
# (Friendly-name mapping was originally surfaced by community frameworks; the values themselves
|
||||
# are stable PT public IPC names. This file is self-contained — no external repo required.)
|
||||
# Reason: instead of pressing a hotkey (which is racey, foreground-sensitive, and UIPI-fragile),
|
||||
# directly SetEvent on the kernel event the module is waiting on. Same code path as the hotkey.
|
||||
|
||||
if (-not ('PtEv' -as [type])) {
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class PtEv {
|
||||
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
||||
private static extern IntPtr OpenEventW(uint dwAccess, bool bInherit, string lpName);
|
||||
[DllImport("kernel32.dll", SetLastError=true)]
|
||||
private static extern bool SetEvent(IntPtr h);
|
||||
[DllImport("kernel32.dll", SetLastError=true)]
|
||||
private static extern bool CloseHandle(IntPtr h);
|
||||
private const uint EVENT_MODIFY_STATE = 0x0002;
|
||||
private const uint SYNCHRONIZE = 0x00100000;
|
||||
|
||||
public static bool Signal(string fullName) {
|
||||
IntPtr h = OpenEventW(EVENT_MODIFY_STATE | SYNCHRONIZE, false, fullName);
|
||||
if (h == IntPtr.Zero) {
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
throw new System.ComponentModel.Win32Exception(err,
|
||||
"OpenEvent failed for '" + fullName + "' (err=" + err + "). Owning module process may not be running.");
|
||||
}
|
||||
try { return SetEvent(h); } finally { CloseHandle(h); }
|
||||
}
|
||||
|
||||
public static bool Exists(string fullName) {
|
||||
IntPtr h = OpenEventW(SYNCHRONIZE, false, fullName);
|
||||
if (h == IntPtr.Zero) return false;
|
||||
CloseHandle(h); return true;
|
||||
}
|
||||
}
|
||||
'@
|
||||
}
|
||||
|
||||
# Friendly-name -> full event name map (per Local\ namespace).
|
||||
# Source: <PT-repo>\src\common\interop\shared_constants.h
|
||||
$script:PtSharedEvents = @{
|
||||
# ── Hotkey-activated module triggers ──
|
||||
'AOT.Pin' = 'Local\AlwaysOnTopPinEvent-892e0aa2-cfa8-4cc4-b196-ddeb32314ce8'
|
||||
'AOT.IncreaseOpacity' = 'Local\AlwaysOnTopIncreaseOpacityEvent-a1b2c3d4-e5f6-7890-abcd-ef1234567890'
|
||||
'AOT.DecreaseOpacity' = 'Local\AlwaysOnTopDecreaseOpacityEvent-b2c3d4e5-f6a7-8901-bcde-f12345678901'
|
||||
'AdvancedPaste.ShowUI' = 'Local\PowerToys_AdvancedPaste_ShowUI'
|
||||
'CmdPal.Show' = 'Local\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a'
|
||||
'ColorPicker.Show' = 'Local\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525'
|
||||
'CropAndLock.Reparent' = 'Local\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36'
|
||||
'CropAndLock.Thumbnail' = 'Local\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434'
|
||||
'CursorWrap.Trigger' = 'Local\CursorWrapTriggerEvent-1f8452b5-4e6e-45b3-8b09-13f14a5900c9'
|
||||
'EnvVars.Show' = 'Local\PowerToysEnvironmentVariables-ShowEnvironmentVariablesEvent-1021f616-e951-4d64-b231-a8f972159978'
|
||||
'EnvVars.ShowAdmin' = 'Local\PowerToysEnvironmentVariables-EnvironmentVariablesAdminEvent-8c95d2ad-047c-49a2-9e8b-b4656326cfb2'
|
||||
'FancyZones.ToggleEditor' = 'Local\FancyZones-ToggleEditorEvent-1e174338-06a3-472b-874d-073b21c62f14'
|
||||
'FindMyMouse.Trigger' = 'Local\FindMyMouseTriggerEvent-5a9dc5f4-1c74-4f2f-a66f-1b9b6a2f9b23'
|
||||
'Hosts.Show' = 'Local\Hosts-ShowHostsEvent-5a0c0aae-5ff5-40f5-95c2-20e37ed671f0'
|
||||
'Hosts.ShowAdmin' = 'Local\Hosts-ShowHostsAdminEvent-60ff44e2-efd3-43bf-928a-f4d269f98bec'
|
||||
'LightSwitch.Toggle' = 'Local\PowerToys-LightSwitch-ToggleEvent-d8dc2f29-8c94-4ca1-8c5f-3e2b1e3c4f5a'
|
||||
'LightSwitch.Light' = 'Local\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca'
|
||||
'LightSwitch.Dark' = 'Local\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368'
|
||||
'MeasureTool.Trigger' = 'Local\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199'
|
||||
'MouseCrosshairs.Trigger' = 'Local\MouseCrosshairsTriggerEvent-0d4c7f92-0a5c-4f5c-b64b-8a2a2f7e0b21'
|
||||
'MouseHighlighter.Trigger' = 'Local\MouseHighlighterTriggerEvent-1e3c9c3d-3fdf-4f9a-9a52-31c9b3c3a8f4'
|
||||
'MouseJump.Show' = 'Local\MouseJumpEvent-aa0be051-3396-4976-b7ba-1a9cc7d236a5'
|
||||
'NewKeyboardManager.Open' = 'Local\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f'
|
||||
'Peek.Show' = 'Local\ShowPeekEvent'
|
||||
'PowerDisplay.Toggle' = 'Local\PowerToysPowerDisplay-ToggleEvent-5f1a9c3e-7d2b-4e8f-9a6c-3b5d7e9f1a2c'
|
||||
'PowerLauncher.Invoke' = 'Local\PowerToysRunInvokeEvent-30f26ad7-d36d-4c0e-ab02-68bb5ff3c4ab'
|
||||
'PowerOcr.Show' = 'Local\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a'
|
||||
'RegistryPreview.Trigger' = 'Local\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687'
|
||||
'ShortcutGuide.Trigger' = 'Local\ShortcutGuide-TriggerEvent-d4275ad3-2531-4d19-9252-c0becbd9b496'
|
||||
'TextExtractor.Show' = 'Local\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a'
|
||||
'Workspaces.Hotkey' = 'Local\PowerToys-Workspaces-HotkeyEvent-2625C3C8-BAC9-4DB3-BCD6-3B4391A26FD0'
|
||||
'Workspaces.LaunchEditor' = 'Local\Workspaces-LaunchEditorEvent-a55ff427-cf62-4994-a2cd-9f72139296bf'
|
||||
'ZoomIt.Zoom' = 'Local\PowerToysZoomIt-ZoomEvent-1e4190d7-94bc-4ad5-adc0-9a8fd07cb393'
|
||||
'ZoomIt.Draw' = 'Local\PowerToysZoomIt-DrawEvent-56338997-404d-4549-bd9a-d132b6766975'
|
||||
'ZoomIt.Break' = 'Local\PowerToysZoomIt-BreakEvent-17f2e63c-4c56-41dd-90a0-2d12f9f50c6b'
|
||||
'ZoomIt.LiveZoom' = 'Local\PowerToysZoomIt-LiveZoomEvent-390bf0c7-616f-47dc-bafe-a2d228add20d'
|
||||
'ZoomIt.Snip' = 'Local\PowerToysZoomIt-SnipEvent-2fd9c211-436d-4f17-a902-2528aaae3e30'
|
||||
'ZoomIt.SnipOcr' = 'Local\PowerToysZoomIt-SnipOcrEvent-a7c3b1d2-9e4f-4a6b-8d5c-1f2e3a4b5c6d'
|
||||
'ZoomIt.Record' = 'Local\PowerToysZoomIt-RecordEvent-74539344-eaad-4711-8e83-23946e424512'
|
||||
|
||||
# ── Termination triggers (clean shutdown without process kill) ──
|
||||
'AOT.Terminate' = 'Local\AlwaysOnTopTerminateEvent-cfdf1eae-791f-4953-8021-2f18f3837eae'
|
||||
'Awake.Exit' = 'Local\PowerToysAwakeExitEvent-c0d5e305-35fc-4fb5-83ec-f6070cfaf7fe'
|
||||
'CmdPal.Exit' = 'Local\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd'
|
||||
'ColorPicker.Terminate' = 'Local\TerminateColorPickerEvent-3d676258-c4d5-424e-a87a-4be22020e813'
|
||||
'CropAndLock.Exit' = 'Local\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a'
|
||||
'FZE.Exit' = 'Local\PowerToys-FZE-ExitEvent-ca8c73de-a52c-4274-b691-46e9592d3b43'
|
||||
'Hosts.Terminate' = 'Local\Hosts-TerminateHostsEvent-d5410d5e-45a6-4d11-bbf0-a4ec2d064888'
|
||||
'KBM.Terminate' = 'Local\TerminateKBMSharedEvent-a787c967-55b6-47de-94d9-56f39fed839e'
|
||||
'MouseJump.Terminate' = 'Local\TerminateMouseJumpEvent-252fa337-317f-4c37-a61f-99464c3f9728'
|
||||
'Peek.Terminate' = 'Local\TerminatePeekEvent-267149fe-7ed2-427d-a3ad-9e18203c037c'
|
||||
'PowerAccent.Exit' = 'Local\PowerToysPowerAccentExitEvent-53e93389-d19a-4fbb-9b36-1981c8965e17'
|
||||
'PowerOcr.Terminate' = 'Local\TerminatePowerOCREvent-08e5de9d-15df-4ea8-8840-487c13435a67'
|
||||
'PowerDisplay.Terminate' = 'Local\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a'
|
||||
'Run.Exit' = 'Local\PowerToysRunExitEvent-3e38e49d-a762-4ef1-88f2-fd4bc7481516'
|
||||
'ShortcutGuide.Exit' = 'Local\ShortcutGuide-ExitEvent-35697cdd-a3d2-47d6-a246-34efcc73eac0'
|
||||
'Settings.Terminate' = 'Local\PowerToysRunnerTerminateSettingsEvent-c34cb661-2e69-4613-a1f8-4e39c25d7ef6'
|
||||
'ZoomIt.Exit' = 'Local\PowerToysZoomIt-ExitEvent-36641ce6-df02-4eac-abea-a3fbf9138220'
|
||||
'GrabAndMove.Exit' = 'Local\PowerToysGrabAndMove-ExitEvent-b8c4d2e3-5f6a-7b8c-9d0e-1f2a3b4c5d6e'
|
||||
}
|
||||
|
||||
function Invoke-PtSharedEvent {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Signal a PowerToys named kernel event by friendly name (e.g. 'CmdPal.Show')
|
||||
or by full event path (e.g. 'Local\PowerToys_AdvancedPaste_ShowUI').
|
||||
Returns $true on success; throws if event doesn't exist or owner not running.
|
||||
.EXAMPLE
|
||||
Invoke-PtSharedEvent -Name 'CmdPal.Show'
|
||||
Invoke-PtSharedEvent -Name 'PowerLauncher.Invoke'
|
||||
Invoke-PtSharedEvent -Name 'AOT.Pin'
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory)][string]$Name)
|
||||
$eventName = if ($script:PtSharedEvents.ContainsKey($Name)) { $script:PtSharedEvents[$Name] } else { $Name }
|
||||
return [PtEv]::Signal($eventName)
|
||||
}
|
||||
|
||||
function Test-PtSharedEvent {
|
||||
[CmdletBinding()] param([Parameter(Mandatory)][string]$Name)
|
||||
$eventName = if ($script:PtSharedEvents.ContainsKey($Name)) { $script:PtSharedEvents[$Name] } else { $Name }
|
||||
return [PtEv]::Exists($eventName)
|
||||
}
|
||||
|
||||
function Get-PtSharedEventCatalog {
|
||||
$script:PtSharedEvents.GetEnumerator() | Sort-Object Name |
|
||||
ForEach-Object { [pscustomobject]@{ Name = $_.Key; Event = $_.Value } }
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
# scripts/pt-shell-verbs.ps1
|
||||
# Enumerate Windows classic shell verbs (HKCR-registered) via Shell.Application COM.
|
||||
#
|
||||
# SCOPE WARNING: this does NOT find PowerToys context-menu items on Win11. PT registers
|
||||
# PowerRename, Image Resizer, File Locksmith, New+ etc. via IExplorerCommand (Tier-1 modern
|
||||
# menu), which is invisible to Shell.Application.Verbs(). For PT-context-menu drives, use
|
||||
# `pt-explorer-contextmenu.ps1` (synthetic right-click + UIA invoke). See
|
||||
# `explorer-context-menu-flow.md` for the canonical pattern.
|
||||
#
|
||||
# Useful for: enumerating non-PT classic verbs (Open, Edit, Send-to, third-party shell extensions),
|
||||
# and as a negative check that PT verbs are NOT classic-shadowed.
|
||||
|
||||
function Get-PtShellVerbs {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Enumerate classic HKCR shell verbs on a file or folder. Returns Name + the underlying Verb COM object.
|
||||
.EXAMPLE
|
||||
Get-PtShellVerbs -Path 'D:\fixtures\image.png' | Format-Table Name
|
||||
#>
|
||||
[CmdletBinding()] param([Parameter(Mandatory)][string]$Path)
|
||||
if (-not (Test-Path $Path)) { throw "Path not found: $Path" }
|
||||
$abs = (Resolve-Path $Path).Path
|
||||
$folder = Split-Path -Parent $abs
|
||||
$leaf = Split-Path -Leaf $abs
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$ns = $shell.NameSpace($folder)
|
||||
if (-not $ns) { throw "Cannot open folder namespace: $folder" }
|
||||
$item = $ns.ParseName($leaf)
|
||||
if (-not $item) { throw "File not in folder: $leaf" }
|
||||
return @($item.Verbs()) | ForEach-Object {
|
||||
[pscustomobject]@{ Name = $_.Name; Verb = $_ }
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PtShellVerb {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Invoke a classic shell verb on a file by name-regex match. Returns $true on success.
|
||||
Does NOT work for PT Win11 modern-menu items — see SCOPE WARNING at top.
|
||||
.EXAMPLE
|
||||
Invoke-PtShellVerb -Path 'D:\fixtures\img.png' -NamePattern '^Edit$'
|
||||
#>
|
||||
[CmdletBinding()] param(
|
||||
[Parameter(Mandatory)][string]$Path,
|
||||
[Parameter(Mandatory)][string]$NamePattern
|
||||
)
|
||||
$verb = Get-PtShellVerbs -Path $Path | Where-Object { $_.Name -match $NamePattern } | Select-Object -First 1
|
||||
if (-not $verb) {
|
||||
Write-Warning "No classic shell verb matching '$NamePattern' on '$Path'. (Win11 PT modern-menu items are NOT visible here — use pt-explorer-contextmenu.ps1 instead.)"
|
||||
return $false
|
||||
}
|
||||
$verb.Verb.DoIt()
|
||||
return $true
|
||||
}
|
||||
|
||||
function Reset-PtShellComCache {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Release current Shell.Application COM instance + force a fresh one on next call.
|
||||
Use when you've installed/registered a shell handler mid-test and the cached verb list
|
||||
still reflects the old state.
|
||||
#>
|
||||
[System.Runtime.InteropServices.Marshal]::CleanupUnusedObjectsInCurrentContext()
|
||||
[System.GC]::Collect()
|
||||
[System.GC]::WaitForPendingFinalizers()
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
# scripts/pt-state.ps1
|
||||
# Common state-verification helpers: settings.json diff, runner log grep, GPO log check,
|
||||
# process spawn detection, AppX probe.
|
||||
|
||||
function Get-PtSettings {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read the master PT settings.json (enabled.<Module> flags + run_elevated + theme + language).
|
||||
#>
|
||||
$f = "$env:LOCALAPPDATA\Microsoft\PowerToys\settings.json"
|
||||
if (-not (Test-Path $f)) { return $null }
|
||||
Get-Content $f -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-PtModuleSettings {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read a single module's settings.json (e.g. AdvancedPaste, FancyZones, etc.).
|
||||
These ARE auto-reloaded by the per-module file watcher (~3s debounce).
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ModuleDir)
|
||||
$f = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
|
||||
if (-not (Test-Path $f)) { return $null }
|
||||
Get-Content $f -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-CmdPalSettings {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read CmdPal AppX settings.json (sandboxed path). Contains 19 ProviderSettings, DockSettings,
|
||||
GalleryFeedUrl, EscapeKeyBehaviorSetting, AutoGoHomeInterval, Hotkey, Aliases, etc.
|
||||
#>
|
||||
$f = "$env:LOCALAPPDATA\Packages\Microsoft.CommandPalette_8wekyb3d8bbwe\LocalState\settings.json"
|
||||
if (-not (Test-Path $f)) { return $null }
|
||||
Get-Content $f -Raw | ConvertFrom-Json
|
||||
}
|
||||
|
||||
function Get-PtRunnerLogTail {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tail the latest runner-log_<date>.log file for matching lines.
|
||||
.EXAMPLE
|
||||
Get-PtRunnerLogTail -Pattern 'hotkey is invoked' -TailLines 100
|
||||
Get-PtRunnerLogTail -Pattern 'GPO sets' -TailLines 50
|
||||
#>
|
||||
param([string]$Pattern = '.*', [int]$TailLines = 50)
|
||||
$log = Get-ChildItem "$env:LOCALAPPDATA\Microsoft\PowerToys\RunnerLogs" -Filter 'runner-log_*.log' -EA SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $log) { return @() }
|
||||
Get-Content $log.FullName -Tail $TailLines -EA SilentlyContinue | Where-Object { $_ -match $Pattern }
|
||||
}
|
||||
|
||||
function Test-PtModuleEnabled {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check whether a specific module is enabled in master settings.json.
|
||||
Note: PT Run uses the key "PowerToys Run" (with space).
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ModuleKey)
|
||||
$s = Get-PtSettings
|
||||
if (-not $s) { return $false }
|
||||
return [bool]$s.enabled.$ModuleKey
|
||||
}
|
||||
|
||||
function Test-PtModuleProcess {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the process(es) for a module exe name (e.g. 'PowerToys.AdvancedPaste').
|
||||
Returns empty array if not running.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ExeName)
|
||||
@(Get-Process $ExeName -EA SilentlyContinue)
|
||||
}
|
||||
|
||||
function Restart-PtRunner {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Kill the runner and relaunch to force fresh load of master settings.json.
|
||||
The runner does NOT auto-pickup edits to the top-level enabled.<Module> flags.
|
||||
#>
|
||||
$pt = Get-Process PowerToys -EA SilentlyContinue | Select-Object -First 1
|
||||
if ($pt) { Stop-Process -Id $pt.Id -Force; Start-Sleep -Milliseconds 800 }
|
||||
Start-Process "$env:LOCALAPPDATA\PowerToys\PowerToys.exe"
|
||||
Start-Sleep -Seconds 3
|
||||
}
|
||||
|
||||
function Backup-PtModuleSettings {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Snapshot a module's settings.json to TEMP for restore-on-exit. Returns the backup path.
|
||||
.EXAMPLE
|
||||
$bk = Backup-PtModuleSettings -ModuleDir AdvancedPaste
|
||||
try { ... mutate ... } finally { Restore-PtModuleSettings -ModuleDir AdvancedPaste -BackupPath $bk }
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$ModuleDir)
|
||||
$src = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
|
||||
if (-not (Test-Path $src)) { return $null }
|
||||
$bk = Join-Path $env:TEMP ("ptbk-$ModuleDir-$(Get-Random -Maximum 9999).json")
|
||||
Copy-Item -Path $src -Destination $bk -Force
|
||||
return $bk
|
||||
}
|
||||
|
||||
function Restore-PtModuleSettings {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ModuleDir,
|
||||
[Parameter(Mandatory)][string]$BackupPath
|
||||
)
|
||||
$dst = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleDir\settings.json"
|
||||
Copy-Item -Path $BackupPath -Destination $dst -Force
|
||||
Remove-Item $BackupPath -Force -EA SilentlyContinue
|
||||
}
|
||||
229
.github/skills/wpf-to-winui3-migration/SKILL.md
vendored
@@ -31,9 +31,7 @@ Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.U
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase-by-Phase Scope
|
||||
|
||||
Work on bounded problems, not the entire codebase at once. Each phase should compile before moving to the next.
|
||||
### Recommended Order
|
||||
|
||||
1. **Project file** — Update TFM, NuGet packages, set `<UseWinUI>true</UseWinUI>`
|
||||
2. **Data models and business logic** — No UI dependencies, migrate first
|
||||
@@ -47,213 +45,79 @@ Work on bounded problems, not the entire codebase at once. Each phase should com
|
||||
10. **Installer & build pipeline** — Update WiX, signing, build events
|
||||
11. **Tests** — Adapt for WinUI 3 runtime, async patterns
|
||||
|
||||
### Migration Contract: Prohibited Patterns
|
||||
### Key Principles
|
||||
|
||||
These rules capture human judgment and must be applied consistently across every file. Do NOT deviate.
|
||||
|
||||
**Architecture prohibitions:**
|
||||
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different lifecycle boilerplate. Merge resources and init code into the generated WinUI 3 App class.
|
||||
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. Causes phantom build artifacts.
|
||||
- **Do NOT instantiate services directly** — Use DI and CommunityToolkit.Mvvm patterns.
|
||||
- **Do NOT create a `Window` subclass for every dialog or sub-page** — use `ContentDialog` for in-app dialogs and `Frame`/`Page` navigation for sub-views. Separate `Window` classes are reserved for distinct top-level surfaces (e.g., FancyZones editor, OOBE).
|
||||
- **Do NOT omit `WindowsPackageType=None` and `WindowsAppSDKSelfContained=true`** — Both are mandatory in the csproj for every WinUI 3 module in PowerToys. Without them the app crashes at startup with `COMException: ClassFactory cannot supply requested class` because the WinUI 3 runtime DLLs are not found.
|
||||
|
||||
**XAML prohibitions:**
|
||||
- **Do NOT use `{DynamicResource}`** — Replace with `{ThemeResource}` (theme-reactive) or `{StaticResource}`.
|
||||
- **Do NOT use `{Binding}` in `Setter.Value`** — Not supported in WinUI 3. Use `{StaticResource}`.
|
||||
- **Do NOT use `{x:Static}`** — Replace with `{x:Bind}`, `x:Uid`, or code-behind.
|
||||
- **Do NOT use `{x:Type}`** — Not supported. Use `x:DataType` for DataTemplate, or code-behind.
|
||||
- **Do NOT use `clr-namespace:`** — Replace with `using:` in all xmlns declarations.
|
||||
- **Do NOT use `Style.Triggers` / `DataTrigger` / `EventTrigger`** — Replace with `VisualStateManager`.
|
||||
- **Do NOT use `MultiBinding`** — Replace with `x:Bind` function binding or computed ViewModel property.
|
||||
- **Do NOT use `Visibility="Hidden"`** — WinUI only has `Visible` and `Collapsed`. Use `Opacity="0"` if layout must be preserved.
|
||||
- **Do NOT use `IsDefault` / `IsCancel`** — Use `AccentButtonStyle` for primary button; handle Enter/Escape in code-behind.
|
||||
- **Do NOT omit `BasedOn` when overriding default styles** — Without it, your style replaces the entire default. Always use `BasedOn="{StaticResource DefaultButtonStyle}"` etc.
|
||||
- **Do NOT omit `XamlControlsResources` as first merged dictionary** — It provides default Fluent styles. Without it, controls have no visual appearance.
|
||||
|
||||
**Code-behind prohibitions:**
|
||||
- **Do NOT use `Application.Current.Dispatcher`** — Store `DispatcherQueue` in a static field explicitly.
|
||||
- **Do NOT use `Window.Current`** — Not supported. Use a custom `App.Window` static property.
|
||||
- **Do NOT put `DataContext`, `Resources`, or `VisualStateManager` on `Window`** — WinUI 3 `Window` is NOT a `DependencyObject`. Use a root `Page`/`UserControl`/`Grid`.
|
||||
- **Do NOT use tunneling/preview events** (`PreviewMouseDown`, `PreviewKeyDown`) — WinUI has no tunneling. Use bubbling equivalents with `Handled` property or `AddHandler(handledEventsToo: true)`.
|
||||
|
||||
**Resource prohibitions:**
|
||||
- **Do NOT use `Properties.Resources.MyString`** — Replace with `ResourceLoaderInstance.ResourceLoader.GetString("MyString")`.
|
||||
- **Do NOT initialize `ResourceLoader`-dependent values as static fields** — Wrap in `Lazy<T>` or null-coalescing property.
|
||||
- **Do NOT use `pack://` URIs** — Replace with `ms-appx:///` scheme.
|
||||
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different application lifecycle boilerplate. Merge your resources and initialization code into the generated WinUI 3 App class.
|
||||
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. This causes phantom build artifacts.
|
||||
- **Use `Lazy<T>` for resource-dependent statics** — `ResourceLoader` is not available at class-load time in all contexts.
|
||||
|
||||
## Quick Reference Tables
|
||||
|
||||
### Namespace Mapping
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` | Root namespace |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` | Core controls |
|
||||
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` | Low-level primitives |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` | Brushes, transforms |
|
||||
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` | Storyboard, animations |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) | Split by purpose |
|
||||
| `System.Windows.Media.Media3D` | **No equivalent** | Use Win2D or Composition APIs |
|
||||
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` | Rectangle, Ellipse, Path |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` | Pointer, keyboard, focus |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` | Binding, IValueConverter |
|
||||
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` | Limited — RichTextBlock + Paragraph |
|
||||
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` | XAML parsing, markup extensions |
|
||||
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` | Accessibility / UI Automation |
|
||||
| `System.Windows.Navigation` | **No direct equivalent** | Use `Frame.Navigate()` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` | Dispatcher → DispatcherQueue |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` / `Microsoft.UI.Xaml.Hosting` | HWND interop |
|
||||
|
||||
### Control Replacements (No 1:1 Mapping)
|
||||
|
||||
These WPF controls have no direct counterpart and require a different control or third-party package:
|
||||
|
||||
| WPF Control | WinUI 3 Replacement | Notes |
|
||||
|-------------|---------------------|-------|
|
||||
| `DataGrid` | [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) | Community library; the Toolkit `DataGrid` is no longer maintained. Legacy code may still pin v7 `CommunityToolkit.WinUI.UI.Controls.DataGrid` 7.1.2 |
|
||||
| `Ribbon` | `CommandBar` / `NavigationView`, or [Toolkit Labs Ribbon](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/Ribbon) | No first-party Ribbon in WinUI; Labs component is experimental/partial |
|
||||
| `Menu` / `MenuItem` | `MenuBar` / `MenuBarItem` / `MenuFlyout` | `MenuBar` for classic menu, `MenuFlyout` for context |
|
||||
| `ContextMenu` | `MenuFlyout` | Assign to `ContextFlyout` property |
|
||||
| `ToolBar` / `ToolBarTray` | `CommandBar` + `AppBarButton` | |
|
||||
| `StatusBar` | Custom `Grid`/`StackPanel` or `InfoBar` | No StatusBar control |
|
||||
| `TabControl` | `TabView` or `NavigationView` (top mode) | `TabView` for closeable tabs |
|
||||
| `DocumentViewer` | `WebView2` | Render PDFs/XPS inside WebView2 |
|
||||
| `FlowDocument` | `RichTextBlock` | Partial replacement only |
|
||||
| `RichTextBox` | `RichEditBox` | Rich text editing |
|
||||
| `GroupBox` | `Expander` (built-in) or `HeaderedContentControl` (Toolkit) | See [Layout & Header Controls from CommunityToolkit.WinUI](#layout--header-controls-from-communitytoolkitwinui) below |
|
||||
| `Label` | `TextBlock` | WPF `Label` is a `ContentControl`; use `TextBlock` + `AccessKey` |
|
||||
| `TreeView` | `TreeView` (native) | Available natively, but data binding model differs significantly |
|
||||
| `MessageBox` | `ContentDialog` | Must set `XamlRoot` before `ShowAsync()` |
|
||||
| `MediaElement` | `MediaPlayerElement` | Different API |
|
||||
| `AccessText` | Not available | Use `AccessKey` property on target control |
|
||||
|
||||
### Layout & Header Controls from CommunityToolkit.WinUI
|
||||
|
||||
These WPF controls have no built-in WinUI 3 equivalent — install the corresponding CommunityToolkit package. **The NuGet package id and the XAML namespace differ intentionally**: package names end in `.Primitives` / `.HeaderedControls`, but the registered XAML namespace is the shorter `CommunityToolkit.WinUI.Controls` (confirmed in the [official Microsoft Q&A](https://learn.microsoft.com/en-us/answers/questions/5746230/why-does-communitytoolkit-uwp-controls-primitive-u)).
|
||||
|
||||
| WPF Control | WinUI 3 Replacement | NuGet Package | XAML Namespace |
|
||||
|-------------|---------------------|---------------|----------------|
|
||||
| `WrapPanel` | `WrapPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `UniformGrid` | `UniformGrid` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `DockPanel` | `DockPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `GroupBox` (alt.) | `HeaderedContentControl` | `CommunityToolkit.WinUI.Controls.HeaderedControls` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
|
||||
### No Equivalent — Requires Architectural Rework
|
||||
|
||||
These WPF features have no WinUI counterpart and require redesign, not find-and-replace:
|
||||
|
||||
| WPF Feature | WinUI 3 Replacement Strategy |
|
||||
|-------------|------------------------------|
|
||||
| `Style.Triggers` / `DataTrigger` | `VisualStateManager` with `StateTrigger` — see [XAML Migration](./references/xaml-migration.md) |
|
||||
| `MultiBinding` | `x:Bind` function binding: `{x:Bind local:Converters.Format(VM.A, VM.B), Mode=OneWay}` |
|
||||
| `RoutedUICommand` / `CommandBinding` | `ICommand` / `[RelayCommand]` from CommunityToolkit.Mvvm. WinUI also has `StandardUICommand` / `XamlUICommand` for platform commands. |
|
||||
| `AdornerLayer` / `Adorner` | Depends on use case: `TeachingTip`/`InfoBar` (validation), `Popup` (overlays), `PlaceholderText` (watermarks), Canvas overlay (decorations) |
|
||||
| `Visibility.Hidden` | `Opacity="0"` with `Visibility="Visible"` (preserves layout space) |
|
||||
| `Window.Resources` / `Window.DataContext` | Move to root `Grid.Resources` / root `Page`/`UserControl` — WinUI `Window` is NOT a DependencyObject |
|
||||
| Tunneling events (`Preview*`) | Use bubbling equivalents + `Handled` property or `AddHandler(handledEventsToo: true)` |
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` |
|
||||
|
||||
### Critical API Replacements
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`), async by default |
|
||||
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`) |
|
||||
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` | Property vs method |
|
||||
| `Application.Current.Dispatcher` | Store `DispatcherQueue` in static field | See [Threading](./references/threading-and-windowing.md) |
|
||||
| `Window.Current` | Custom `App.Window` static property | Not supported in Windows App SDK |
|
||||
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
|
||||
| `MessageBox.Show()` | `ContentDialog` | Must set `XamlRoot` |
|
||||
| `System.Windows.Clipboard` | `Windows.ApplicationModel.DataTransfer.Clipboard` | Different API surface |
|
||||
| `RoutedUICommand` / `CommandBinding` | `ICommand` / `[RelayCommand]` | Remove `CommandBinding`; bind `ICommand` directly |
|
||||
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
|
||||
| `DynamicResource` | `ThemeResource` | Theme-reactive only |
|
||||
| `clr-namespace:` | `using:` | XAML namespace prefix |
|
||||
| `{x:Static props:Resources.Key}` | `x:Uid` or `ResourceLoader.GetString()` | .resx → .resw |
|
||||
| `DataType="{x:Type m:Foo}"` | `x:DataType="m:Foo"` | `x:Type` not supported |
|
||||
| `DataType="{x:Type m:Foo}"` | Remove or use code-behind | `x:Type` not supported |
|
||||
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
|
||||
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
|
||||
| `SizeToContent="Height"` | Custom `SizeToContent()` via `AppWindow.Resize()` | See [Windowing](./references/threading-and-windowing.md) |
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Mouse → Pointer events |
|
||||
| `Pack URI (pack://...)` | `ms-appx:///` | Resource URI scheme |
|
||||
| `Observable` (custom base) | `ObservableObject` + `[ObservableProperty]` | CommunityToolkit.Mvvm |
|
||||
| `RelayCommand` (custom) | `[RelayCommand]` source generator | CommunityToolkit.Mvvm |
|
||||
| `JpegBitmapEncoder` | `BitmapEncoder.CreateAsync(JpegEncoderId, stream)` | Async, unified API |
|
||||
| `encoder.QualityLevel = 85` | `BitmapPropertySet { "ImageQuality", 0.85f }` | int 1-100 → float 0-1 |
|
||||
|
||||
### Event Replacements (Mouse → Pointer)
|
||||
|
||||
| WPF Event | WinUI 3 Event | Notes |
|
||||
|-----------|--------------|-------|
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Check `IsLeftButtonPressed` on args |
|
||||
| `MouseLeftButtonUp` | `PointerReleased` | Check pointer properties |
|
||||
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` with right button check |
|
||||
| `MouseMove` | `PointerMoved` | `MouseEventArgs` → `PointerRoutedEventArgs` |
|
||||
| `MouseWheel` | `PointerWheelChanged` | Different event args |
|
||||
| `MouseEnter` / `MouseLeave` | `PointerEntered` / `PointerExited` | |
|
||||
| `MouseDoubleClick` | `DoubleTapped` | Different event args |
|
||||
| `PreviewMouseDown` | `PointerPressed` | No tunneling — use `Handled` or `AddHandler` |
|
||||
| `PreviewKeyDown` | `KeyDown` | `KeyEventArgs` → `KeyRoutedEventArgs` |
|
||||
|
||||
### Property Replacements
|
||||
|
||||
| WPF | WinUI 3 | Context |
|
||||
|-----|---------|---------|
|
||||
| `Visibility.Hidden` | `Visibility.Collapsed` or `Opacity="0"` | Use `Opacity="0"` to preserve layout |
|
||||
| `TextWrapping.WrapWithOverflow` | `TextWrapping.Wrap` | WinUI doesn't distinguish |
|
||||
| `Focusable="True"` | `IsTabStop="True"` | Different property name |
|
||||
| `ContextMenu=` | `ContextFlyout=` | On any `UIElement` |
|
||||
| `MediaElement` | `MediaPlayerElement` | Different API |
|
||||
| `SnapsToDevicePixels` | Not available | WinUI handles pixel snapping internally |
|
||||
|
||||
### NuGet Package Migration
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` | |
|
||||
| `WPF-UI` (Lepo) | **Remove** — use native WinUI 3 controls | |
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) | |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` | |
|
||||
| (none) | `Microsoft.WindowsAppSDK` | Required |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` | Required |
|
||||
| (none) | `WinUIEx` | Optional, window helpers |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Primitives` | Optional — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.HeaderedControls` | Optional — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.SettingsControls` | Optional — `SettingsCard`, `SettingsExpander` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Sizers` | Optional — `GridSplitter` |
|
||||
| (none) | `CommunityToolkit.WinUI.UI.Controls.DataGrid` | Legacy v7 — only for migrating existing `DataGrid` code; prefer `WinUI.TableView` |
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` |
|
||||
| `WPF-UI` (Lepo) | Remove — use native WinUI 3 controls |
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` |
|
||||
| (none) | `Microsoft.WindowsAppSDK` |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` |
|
||||
| (none) | `WinUIEx` (optional, for window helpers) |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` |
|
||||
|
||||
### XAML Syntax Changes
|
||||
|
||||
| WPF | WinUI 3 | Notes |
|
||||
|-----|---------|-------|
|
||||
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` | CLR → using syntax |
|
||||
| `{DynamicResource Key}` | `{ThemeResource Key}` | Re-evaluates on theme change |
|
||||
| `{StaticResource Key}` | `{StaticResource Key}` | Same — resolved once at load |
|
||||
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind | |
|
||||
| `{x:Type local:MyType}` | Not supported | Use `x:DataType` for DataTemplate |
|
||||
| `{x:Array}` | Not supported | Create collections in code-behind |
|
||||
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` | See [XAML Migration](./references/xaml-migration.md) |
|
||||
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` | |
|
||||
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` | |
|
||||
| `sys:String` / `sys:Int32` / etc. | `x:String` / `x:Int32` / etc. | XAML intrinsic types |
|
||||
| `<ui:FluentWindow>` (WPF-UI) | `<Window>` | Native + `ExtendsContentIntoTitleBar` |
|
||||
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` | |
|
||||
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` | Named style keys |
|
||||
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / KeyDown | |
|
||||
| `<AccessText>` | Not available — use `AccessKey` property | |
|
||||
| `<behaviors:Interaction.Triggers>` | Code-behind or WinUI behaviors | |
|
||||
| `Window.Resources` | Root container's `Resources` (e.g. `Grid.Resources`) | Window is not a DependencyObject |
|
||||
|
||||
### Binding: {Binding} vs {x:Bind}
|
||||
|
||||
Both work in WinUI 3. Prefer `{x:Bind}` for new/migrated code.
|
||||
|
||||
| Feature | `{Binding}` | `{x:Bind}` |
|
||||
|---------|------------|------------|
|
||||
| Default mode | `OneWay` | **`OneTime`** — add `Mode=OneWay` explicitly! |
|
||||
| Default source | `DataContext` | Page/UserControl code-behind |
|
||||
| Compile-time validation | No | Yes |
|
||||
| Function binding | No | Yes (replaces `MultiBinding`) |
|
||||
| Performance | Reflection-based | Compiled, no reflection |
|
||||
| `MultiBinding` support | No (not in WinUI) | Use function binding |
|
||||
| WPF | WinUI 3 |
|
||||
|-----|---------|
|
||||
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` |
|
||||
| `{DynamicResource Key}` | `{ThemeResource Key}` |
|
||||
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind |
|
||||
| `{x:Type local:MyType}` | Not supported |
|
||||
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` |
|
||||
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` |
|
||||
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` |
|
||||
| `<ui:FluentWindow>` / `<ui:Button>` (WPF-UI) | Native `<Window>` / `<Button>` |
|
||||
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` |
|
||||
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` |
|
||||
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / handle via KeyDown |
|
||||
| `<AccessText>` | Not available — use `AccessKey` property |
|
||||
| `<behaviors:Interaction.Triggers>` | Migrate to code-behind or WinUI behaviors |
|
||||
|
||||
## Detailed Reference Docs
|
||||
|
||||
@@ -287,8 +151,6 @@ Read only the section relevant to your current task:
|
||||
| JPEG quality value wrong after migration | WPF: int 1-100; WinRT: float 0.0-1.0 |
|
||||
| MSIX packaging fails in PreBuildEvent | Move to PostBuildEvent; artifacts not ready at PreBuild time |
|
||||
| RC file icon path with forward slashes | Use double-backslash escaping: `..\\ui\\Assets\\icon.ico` |
|
||||
| `COMException: ClassFactory cannot supply requested class` at startup | Missing `<WindowsPackageType>None</WindowsPackageType>` and/or `<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>` in csproj. Without these, the app tries to locate the Windows App SDK framework package (not installed) instead of using bundled runtime DLLs. **Both properties are mandatory for every WinUI 3 module in PowerToys.** |
|
||||
| `CombinedGeometry` not available in WinUI 3 | WinUI 3 `UIElement.Clip` only accepts `RectangleGeometry`. For overlay hole effects (exclude region), use a `Path` element with `GeometryGroup FillRule="EvenOdd"` containing two `RectangleGeometry` children — the EvenOdd rule creates a transparent hole where geometries overlap. |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -301,4 +163,3 @@ Read only the section relevant to your current task:
|
||||
| NuGet restore failures | Run `build-essentials.cmd` after adding `Microsoft.WindowsAppSDK` package |
|
||||
| `Parallel.ForEach` compilation error | Migrate to `Parallel.ForEachAsync` for async imaging operations |
|
||||
| Signing check fails on leaked artifacts | Run `generateAllFileComponents.ps1`; verify only `WinUI3Apps\\` paths in signing config |
|
||||
| `COMException` / `ClassFactory` error at app launch | Ensure csproj has `<WindowsPackageType>None</WindowsPackageType>` and `<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>`. These are required for all unpackaged WinUI 3 apps in PowerToys — without them the WinUI 3 COM runtime cannot be found. |
|
||||
|
||||
@@ -4,26 +4,24 @@ Complete reference for mapping WPF types to WinUI 3 equivalents, based on the Im
|
||||
|
||||
## Root Namespace Mapping
|
||||
|
||||
| WPF Namespace | WinUI 3 Namespace | Notes |
|
||||
|---------------|-------------------|-------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` | Root namespace |
|
||||
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` | Accessibility / UI Automation |
|
||||
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` | |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` | Core controls |
|
||||
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` | Low-level primitives |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` | Binding, IValueConverter |
|
||||
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` | Limited — RichTextBlock + Paragraph only |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` | Pointer, keyboard, focus |
|
||||
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` | XAML parsing, markup extensions |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` | Brushes, transforms |
|
||||
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` | Storyboard, animations |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` | UI display only — use `Windows.Graphics.Imaging` for processing |
|
||||
| `System.Windows.Media.Media3D` | **No equivalent** | Use Win2D or Composition APIs |
|
||||
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` | For Frame navigation events; no `NavigationService` |
|
||||
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` | Rectangle, Ellipse, Path |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` | Dispatcher → DispatcherQueue |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` | HWND interop |
|
||||
| `System.Windows.Interop` | `Microsoft.UI.Xaml.Hosting` | XAML Islands |
|
||||
| WPF Namespace | WinUI 3 Namespace |
|
||||
|---------------|-------------------|
|
||||
| `System.Windows` | `Microsoft.UI.Xaml` |
|
||||
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` |
|
||||
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` |
|
||||
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
|
||||
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` |
|
||||
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
|
||||
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` |
|
||||
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
|
||||
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` |
|
||||
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
|
||||
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` |
|
||||
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` |
|
||||
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` |
|
||||
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` |
|
||||
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
|
||||
| `System.Windows.Interop` | `WinRT.Interop` |
|
||||
|
||||
## Core Type Mapping
|
||||
|
||||
@@ -47,7 +45,7 @@ Complete reference for mapping WPF types to WinUI 3 equivalents, based on the Im
|
||||
|
||||
These controls exist in both frameworks with the same name — change `System.Windows.Controls` to `Microsoft.UI.Xaml.Controls`:
|
||||
|
||||
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
|
||||
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
|
||||
|
||||
### Controls With Different Names or Behavior
|
||||
|
||||
@@ -58,7 +56,6 @@ These controls exist in both frameworks with the same name — change `System.Wi
|
||||
| `TabControl` | `TabView` | Different API |
|
||||
| `Menu` | `MenuBar` | Different API |
|
||||
| `StatusBar` | Custom `StackPanel` layout | No built-in equivalent |
|
||||
| `ListBox` | `ListView` (or `ItemsView`) | **Deprecated in WinUI 3 — do not use.** Prefer `ListView`, or `ItemsView` (WinUI 1.5+) for modern collection scenarios |
|
||||
| `AccessText` | Not available | Use `AccessKey` property on target control |
|
||||
|
||||
### WPF-UI (Lepo) to Native WinUI 3
|
||||
@@ -112,43 +109,6 @@ Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All
|
||||
| `System.Windows.Interop.WindowInteropHelper` | `WinRT.Interop.WindowNative.GetWindowHandle()` | |
|
||||
| `System.Windows.SystemColors` | Resource keys via `ThemeResource` | No direct static class |
|
||||
| `System.Windows.SystemParameters` | Win32 API or `DisplayInformation` | No direct equivalent |
|
||||
| `System.Windows.Clipboard` | `Windows.ApplicationModel.DataTransfer.Clipboard` | Different API surface |
|
||||
| `System.Windows.Input.RoutedUICommand` | `Microsoft.UI.Xaml.Input.StandardUICommand` / `XamlUICommand` | Or use `ICommand` / `[RelayCommand]` |
|
||||
| `System.Windows.Input.CommandBinding` | **Remove** | Bind `ICommand` directly in XAML |
|
||||
|
||||
## Controls That Need Translation (No 1:1 Mapping)
|
||||
|
||||
These controls exist in WPF but require a different control, third-party library, or Community Toolkit package in WinUI 3:
|
||||
|
||||
| WPF Control | WinUI 3 Replacement | Package / Notes |
|
||||
|-------------|---------------------|-----------------|
|
||||
| `DataGrid` | [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) | Community library; the Toolkit `DataGrid` is no longer maintained. PowerToys legacy modules may still pin v7 `CommunityToolkit.WinUI.UI.Controls.DataGrid` 7.1.2 — prefer `WinUI.TableView` for new work. |
|
||||
| `Ribbon` | `CommandBar` / `NavigationView`, or [Toolkit Labs Ribbon](https://github.com/CommunityToolkit/Labs-Windows/tree/main/components/Ribbon) | No first-party Ribbon in WinUI; the Labs component is experimental and partial |
|
||||
| `Menu` / `MenuItem` | `MenuBar` / `MenuBarItem` / `MenuFlyoutItem` | `MenuBar` for classic menu, `MenuFlyout` for context |
|
||||
| `ContextMenu` | `MenuFlyout` | Assign to `ContextFlyout` property |
|
||||
| `ToolBar` / `ToolBarTray` | `CommandBar` + `AppBarButton` | |
|
||||
| `StatusBar` | Custom `Grid`/`StackPanel` or `InfoBar` | No StatusBar control |
|
||||
| `TabControl` | `TabView` or `NavigationView` (top mode) | `TabView` for closeable tabs |
|
||||
| `DocumentViewer` | `WebView2` | `Microsoft.Web.WebView2` — render PDFs/XPS |
|
||||
| `FlowDocument` | `RichTextBlock` | Partial replacement only |
|
||||
| `RichTextBox` | `RichEditBox` | Rich text editing |
|
||||
| `GroupBox` | `Expander` (built-in) or `HeaderedContentControl` (Toolkit) | See [Layout & Header Controls from CommunityToolkit.WinUI](#layout--header-controls-from-communitytoolkitwinui) below |
|
||||
| `Label` | `TextBlock` | WPF `Label` is a `ContentControl`; use `TextBlock` + `AccessKey` |
|
||||
| `TreeView` | `TreeView` (native) | Available natively, but data binding model differs significantly |
|
||||
| `MediaElement` | `MediaPlayerElement` | Different API |
|
||||
|
||||
## Layout & Header Controls from CommunityToolkit.WinUI
|
||||
|
||||
These WPF layout/header controls have no built-in WinUI 3 equivalent — install the corresponding CommunityToolkit package. **The NuGet package id and the XAML namespace differ intentionally**: package names end in `.Primitives` / `.HeaderedControls`, but both packages register their controls in the shorter `CommunityToolkit.WinUI.Controls` XAML namespace (confirmed in the [official Microsoft Q&A](https://learn.microsoft.com/en-us/answers/questions/5746230/why-does-communitytoolkit-uwp-controls-primitive-u): *"all controls live under `CommunityToolkit.WinUI.Controls` … This is intentional"*).
|
||||
|
||||
| WPF Control | WinUI 3 Replacement | NuGet Package | XAML Namespace |
|
||||
|-------------|---------------------|---------------|----------------|
|
||||
| `WrapPanel` | `WrapPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `UniformGrid` | `UniformGrid` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `DockPanel` | `DockPanel` | `CommunityToolkit.WinUI.Controls.Primitives` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
| `GroupBox` (alt.) | `HeaderedContentControl` | `CommunityToolkit.WinUI.Controls.HeaderedControls` | `using:CommunityToolkit.WinUI.Controls` |
|
||||
|
||||
Other primitives in `Primitives`: `ConstrainedBox`, `SwitchPresenter`, `WrapLayout`, `StaggeredPanel`. Other Headered controls in `HeaderedControls`: `HeaderedItemsControl`, `HeaderedTreeView`.
|
||||
|
||||
## NuGet Package Migration
|
||||
|
||||
@@ -164,11 +124,6 @@ Other primitives in `Primitives`: `ConstrainedBox`, `SwitchPresenter`, `WrapLayo
|
||||
| (none) | `WinUIEx` | Optional, window helpers |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Extensions` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Primitives` | Optional — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox`, `SwitchPresenter` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.HeaderedControls` | Optional — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.SettingsControls` | Optional — `SettingsCard`, `SettingsExpander` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Sizers` | Optional — `GridSplitter`, `PropertySizer` |
|
||||
| (none) | `CommunityToolkit.WinUI.UI.Controls.DataGrid` | Legacy v7 — only if migrating existing `DataGrid` code; prefer [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) for new work |
|
||||
| (none) | `Microsoft.Web.WebView2` | If using WebView |
|
||||
|
||||
## Project File Changes
|
||||
@@ -212,9 +167,8 @@ Other primitives in `Primitives`: `ConstrainedBox`, `SwitchPresenter`, `WrapLayo
|
||||
Key changes:
|
||||
- `UseWPF` → `UseWinUI`
|
||||
- TFM: `net8.0-windows` → `net8.0-windows10.0.19041.0`
|
||||
- **CRITICAL: Add `WindowsPackageType=None`** — marks the app as unpackaged (no MSIX). Without this, the build produces an MSIX-style package that won't run as a standalone PowerToys module.
|
||||
- **CRITICAL: Add `WindowsAppSDKSelfContained=true`** — bundles the Windows App SDK runtime DLLs (e.g. `Microsoft.UI.Xaml.dll`) into the output directory. Without this, the app throws `COMException: ClassFactory cannot supply requested class` at startup because the WinUI 3 COM classes cannot be found.
|
||||
- Add `SelfContained=true` (usually via `Common.SelfContained.props`)
|
||||
- Add `WindowsPackageType=None` for unpackaged desktop apps
|
||||
- Add `SelfContained=true` + `WindowsAppSDKSelfContained=true`
|
||||
- Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs` entry point
|
||||
- Set `ProjectPriFileName` to match your module's assembly name
|
||||
- Move icon from `Resources/` to `Assets/<Module>/`
|
||||
|
||||
@@ -127,74 +127,6 @@ If the module uses the `WPF-UI` library, replace all Lepo controls with native W
|
||||
</Window>
|
||||
```
|
||||
|
||||
#### Recommended: Use WindowEx from WinUIEx
|
||||
|
||||
> **Tip:** Prefer `WinUIEx.WindowEx` over bare `Window`. It restores many WPF-like window properties directly in XAML, avoiding boilerplate code-behind for common windowing tasks.
|
||||
|
||||
```xml
|
||||
<!-- WinUI 3 with WindowEx (preferred in PowerToys) -->
|
||||
<winuiex:WindowEx
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
x:Class="MyApp.MainWindow"
|
||||
MinWidth="480"
|
||||
MinHeight="320"
|
||||
IsShownInSwitchers="True"
|
||||
IsTitleBarVisible="True">
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
<Grid>
|
||||
...
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
```
|
||||
|
||||
Properties available on `WindowEx` that mirror WPF `Window`:
|
||||
|
||||
| WPF Window Property | WindowEx Property | Notes |
|
||||
|---------------------|-------------------|-------|
|
||||
| `MinWidth` / `MinHeight` | `MinWidth` / `MinHeight` | Set directly in XAML |
|
||||
| `Width` / `Height` | `Width` / `Height` | Initial window size |
|
||||
| `WindowState` | `WindowState` | Minimized, Maximized, Normal |
|
||||
| `Title` | `Title` or `x:Uid` | Window title |
|
||||
| `Icon` | Use `TitleBar.IconSource` | Via WinUI TitleBar control |
|
||||
| `ShowInTaskbar` | `IsShownInSwitchers` | Alt-Tab visibility |
|
||||
| `TopMost` | `IsAlwaysOnTop` | Always-on-top window |
|
||||
|
||||
NuGet: `WinUIEx` — already referenced by most PowerToys modules.
|
||||
|
||||
#### Recommended: Page-in-Window Architecture
|
||||
|
||||
> **Tip:** WinUI 3 `Window` is NOT a `FrameworkElement` — it does not support `Resources`, `DataContext`, `x:Bind`, or `VisualStateManager` directly. Place a `Page` as the Window's root content to regain these WPF-like capabilities.
|
||||
|
||||
```xml
|
||||
<!-- WinUI 3 — Window contains a Page for full FrameworkElement support -->
|
||||
<winuiex:WindowEx x:Class="MyApp.MainWindow"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
xmlns:views="using:MyApp.Views">
|
||||
<views:MainPage x:Name="mainPage" />
|
||||
</winuiex:WindowEx>
|
||||
```
|
||||
|
||||
```xml
|
||||
<!-- MainPage.xaml — has full FrameworkElement capabilities -->
|
||||
<Page x:Class="MyApp.Views.MainPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<Page.Resources>
|
||||
<!-- Resources work here (unlike on Window) -->
|
||||
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
|
||||
</Page.Resources>
|
||||
<Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<!-- VisualStateManager works here (unlike on Window) -->
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
...
|
||||
</Grid>
|
||||
</Page>
|
||||
```
|
||||
|
||||
This is the standard pattern in PowerToys (e.g., FileLocksmith, EnvironmentVariables).
|
||||
|
||||
### App.xaml Resources
|
||||
|
||||
```xml
|
||||
@@ -218,20 +150,6 @@ This is the standard pattern in PowerToys (e.g., FileLocksmith, EnvironmentVaria
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
### CommunityToolkit.WinUI — WPF Replacement Controls
|
||||
|
||||
> **Tip:** The `CommunityToolkit.WinUI` package provides many controls and helpers familiar to WPF developers that are missing from WinUI 3 out of the box. Before writing custom replacements, check whether CommunityToolkit already provides what you need.
|
||||
|
||||
Key packages (XAML namespace is `using:CommunityToolkit.WinUI.Controls` for the `Controls.*` family):
|
||||
- **`CommunityToolkit.WinUI.Controls.Primitives`** — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox`, `SwitchPresenter`
|
||||
- **`CommunityToolkit.WinUI.Controls.HeaderedControls`** — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView`
|
||||
- **`CommunityToolkit.WinUI.Controls.SettingsControls`** — `SettingsCard`, `SettingsExpander`
|
||||
- **`CommunityToolkit.WinUI.Controls.Sizers`** — `GridSplitter`, `PropertySizer`, `ContentSizer`
|
||||
- **`CommunityToolkit.WinUI.UI.Controls.DataGrid`** — legacy v7 `DataGrid` (no longer maintained); prefer [`WinUI.TableView`](https://github.com/w-ahmad/WinUI.TableView) for new work
|
||||
- **`CommunityToolkit.WinUI.Converters`** — Common value converters (`BoolToVisibilityConverter`, `StringFormatConverter`, etc.)
|
||||
- **`CommunityToolkit.WinUI.Behaviors`** — XAML behaviors for animations and interactions
|
||||
- **`CommunityToolkit.WinUI.Extensions`** — Extension methods for WinUI types
|
||||
|
||||
### Common Control Replacements
|
||||
|
||||
```xml
|
||||
@@ -269,135 +187,11 @@ Key packages (XAML namespace is `using:CommunityToolkit.WinUI.Controls` for the
|
||||
<!-- Handle Enter/Escape keys in code-behind if needed -->
|
||||
```
|
||||
|
||||
## No-Equivalent Patterns (Requires Architectural Rework)
|
||||
|
||||
These WPF features demand design changes, not find-and-replace. Read this section BEFORE attempting to migrate any file that uses these patterns.
|
||||
|
||||
### MultiBinding → x:Bind Function Binding
|
||||
|
||||
WinUI does not support `MultiBinding`. Replace with `x:Bind` function binding (most direct replacement), a computed ViewModel property, or multiple simple bindings.
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="{}{0} {1}">
|
||||
<Binding Path="FirstName" />
|
||||
<Binding Path="LastName" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<TextBlock Text="{x:Bind local:Converters.FormatFullName(ViewModel.FirstName, ViewModel.LastName), Mode=OneWay}" />
|
||||
```
|
||||
|
||||
```csharp
|
||||
public static class Converters
|
||||
{
|
||||
public static string FormatFullName(string first, string last) => $"{first} {last}";
|
||||
}
|
||||
```
|
||||
|
||||
### Adorners → Context-Dependent Replacements
|
||||
|
||||
WPF's `AdornerLayer` has no WinUI equivalent. Choose replacement by use case:
|
||||
|
||||
| Adorner Use Case | WinUI 3 Replacement |
|
||||
|------------------|---------------------|
|
||||
| Validation indicators | `TeachingTip`, `InfoBar`, or InputValidation templates |
|
||||
| Resize handles | `Popup` positioned relative to target |
|
||||
| Drag preview | `DragItemsStarting` event with custom DragUI |
|
||||
| Overlay decorations | Canvas overlay or Popup layer |
|
||||
| Watermark / Placeholder | `TextBox.PlaceholderText` (built-in) |
|
||||
|
||||
### RoutedUICommand → ICommand / RelayCommand
|
||||
|
||||
WinUI does not support routed commands or `CommandBinding`. Replace with standard `ICommand` pattern:
|
||||
|
||||
```csharp
|
||||
// CommunityToolkit.Mvvm
|
||||
[RelayCommand(CanExecute = nameof(CanSave))]
|
||||
private void Save() { /* save logic */ }
|
||||
private bool CanSave() => IsDirty;
|
||||
```
|
||||
|
||||
WinUI 3 also provides `StandardUICommand` and `XamlUICommand` for pre-defined platform commands (Cut, Copy, Paste, Delete) with built-in icons and keyboard accelerators.
|
||||
|
||||
### Tunneling / Preview Events
|
||||
|
||||
WinUI has no tunneling event model. `PreviewMouseDown`, `PreviewKeyDown`, etc. do not exist.
|
||||
|
||||
- Replace with the bubbling equivalent (`PointerPressed`, `KeyDown`)
|
||||
- If you relied on tunneling to intercept events before children, restructure using the `Handled` property
|
||||
- For must-handle scenarios, use `AddHandler` with `handledEventsToo: true`:
|
||||
```csharp
|
||||
myElement.AddHandler(UIElement.PointerPressedEvent,
|
||||
new PointerEventHandler(OnPointerPressed), handledEventsToo: true);
|
||||
```
|
||||
|
||||
## Style and Template Changes
|
||||
|
||||
### Implicit Styles — Always Use BasedOn
|
||||
|
||||
> **Warning:** In WinUI 3, always use `BasedOn` when overriding default control styles. Without it, your style **replaces the entire default style** rather than extending it.
|
||||
|
||||
```xml
|
||||
<!-- WRONG — replaces entire default style, control may lose all visual appearance -->
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Background" Value="Red" />
|
||||
</Style>
|
||||
|
||||
<!-- CORRECT — extends the default style -->
|
||||
<Style TargetType="Button" BasedOn="{StaticResource DefaultButtonStyle}">
|
||||
<Setter Property="Background" Value="Red" />
|
||||
</Style>
|
||||
```
|
||||
|
||||
### Triggers → VisualStateManager
|
||||
|
||||
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported. Two replacement approaches:
|
||||
|
||||
#### Approach 1: StateTrigger (direct DataTrigger replacement — simpler)
|
||||
|
||||
Use this for data-driven state changes. This is the closest equivalent to WPF `DataTrigger`:
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsActive}" Value="True">
|
||||
<Setter Property="Background" Value="Green" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
```
|
||||
|
||||
**WinUI 3:**
|
||||
```xml
|
||||
<Border x:Name="MyBorder">
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup>
|
||||
<VisualState x:Name="Active">
|
||||
<VisualState.StateTriggers>
|
||||
<StateTrigger IsActive="{x:Bind ViewModel.IsActive, Mode=OneWay}" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="MyBorder.Background" Value="Green" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Border>
|
||||
```
|
||||
|
||||
Note: `VisualStateManager` must be placed on a control inside the Window, NOT on the Window itself.
|
||||
|
||||
#### Approach 2: ControlTemplate (for property triggers like IsMouseOver)
|
||||
|
||||
Use this when replacing `<Trigger Property="IsMouseOver">` or similar control-state triggers:
|
||||
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
|
||||
|
||||
**WPF:**
|
||||
```xml
|
||||
@@ -406,6 +200,9 @@ Use this when replacing `<Trigger Property="IsMouseOver">` or similar control-st
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="LightBlue"/>
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
|
||||
<Setter Property="Opacity" Value="0.5"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
```
|
||||
@@ -452,42 +249,11 @@ Use this when replacing `<Trigger Property="IsMouseOver">` or similar control-st
|
||||
| `Disabled` | `Disabled` |
|
||||
| `Pressed` | `Pressed` |
|
||||
|
||||
## Visibility.Hidden — No Equivalent
|
||||
|
||||
WinUI only has `Visible` and `Collapsed`. There is no `Hidden`.
|
||||
|
||||
| WPF | WinUI 3 | Behavior |
|
||||
|-----|---------|----------|
|
||||
| `Visibility.Visible` | `Visibility.Visible` | Rendered and occupies layout space |
|
||||
| `Visibility.Hidden` | **Not available** | Use `Opacity="0"` with `Visibility="Visible"` to hide but keep layout |
|
||||
| `Visibility.Collapsed` | `Visibility.Collapsed` | Not rendered, no layout space |
|
||||
|
||||
## Resource Dictionary Changes
|
||||
|
||||
### XamlControlsResources Must Be First
|
||||
### Window.Resources → Grid.Resources
|
||||
|
||||
> **Warning:** `XamlControlsResources` must be the **first** merged dictionary in `App.xaml`. It provides the default Fluent styles. Omitting it gives you controls with no visual appearance. Resource paths use `ms-appx:///` instead of relative paths.
|
||||
|
||||
```xml
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- MUST be first -->
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Then your custom dictionaries -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
### Window.Resources → Grid.Resources (or use Page)
|
||||
|
||||
WinUI 3 `Window` is NOT a `FrameworkElement` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
|
||||
|
||||
**Preferred approach:** Use the [Page-in-Window architecture](#recommended-page-in-window-architecture) described above. A `Page` inside the `Window` gives you full `FrameworkElement` capabilities (Resources, DataContext, x:Bind, VisualStateManager).
|
||||
|
||||
**Fallback** (for simple windows without a Page):
|
||||
WinUI 3 `Window` is NOT a `DependencyObject` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
|
||||
|
||||
```xml
|
||||
<!-- WPF -->
|
||||
@@ -551,25 +317,19 @@ Both are available. Prefer `{x:Bind}` for compile-time safety and performance.
|
||||
| Performance | Reflection-based | Compiled |
|
||||
| Function binding | No | Yes |
|
||||
|
||||
### Binding Differences from WPF
|
||||
|
||||
These WPF binding patterns behave differently in WinUI 3 — review on a case-by-case basis rather than mechanically removing.
|
||||
### WPF-Specific Binding Features to Remove
|
||||
|
||||
```xml
|
||||
<!-- UpdateSourceTrigger: limited support in WinUI 3 -->
|
||||
<!-- These WPF-only features must be removed or rewritten -->
|
||||
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<!-- WinUI 3: UpdateSourceTrigger=LostFocus does NOT exist; PropertyChanged is the TextBox default.
|
||||
Prefer x:Bind, which binds with PropertyChanged semantics by default for TwoWay. -->
|
||||
<!-- WinUI 3: UpdateSourceTrigger not needed; TextBox uses PropertyChanged by default -->
|
||||
<TextBox Text="{x:Bind ViewModel.Value, Mode=TwoWay}" />
|
||||
|
||||
<!-- RelativeSource: Self and TemplatedParent ARE supported in WinUI 3 -->
|
||||
<TextBlock Text="{Binding Tag, RelativeSource={RelativeSource Self}}" />
|
||||
<!-- Works in WinUI 3. FindAncestor mode is NOT supported — use ElementName, x:Bind,
|
||||
or the CommunityToolkit FrameworkElementExtensions.Ancestor attached property to reach ancestors. -->
|
||||
{Binding RelativeSource={RelativeSource Self}, ...}
|
||||
<!-- WinUI 3: Use x:Bind which binds to the page itself, or use ElementName -->
|
||||
|
||||
<!-- {Binding} empty path: works in WinUI 3 (binds to the current DataContext) -->
|
||||
<ItemsControl ItemsSource="{Binding}" />
|
||||
<!-- This is valid. Note: x:Bind requires an explicit path — there is no empty-path x:Bind. -->
|
||||
<!-- WinUI 3: Must specify explicit path -->
|
||||
<ItemsControl ItemsSource="{x:Bind ViewModel.Items}" />
|
||||
```
|
||||
|
||||
@@ -595,64 +355,6 @@ ExtendsContentIntoTitleBar="True" <!-- Set in code-behind -->
|
||||
| `IsHitTestVisible` | `IsHitTestVisible` | Same |
|
||||
| `TextBox.VerticalScrollBarVisibility` | `ScrollViewer.VerticalScrollBarVisibility` (attached) | Attached property |
|
||||
|
||||
## Complete Find-and-Replace Reference
|
||||
|
||||
Use this table for mechanical batch translation. Apply these rules consistently to every file.
|
||||
|
||||
### XAML Attribute Replacements
|
||||
|
||||
| Find | Replace With | Context |
|
||||
|------|-------------|---------|
|
||||
| `ContextMenu=` | `ContextFlyout=` | On any UIElement |
|
||||
| `{DynamicResource ` | `{ThemeResource ` | Theme-responsive references |
|
||||
| `{x:Static prefix:Resources.Key}` | `x:Uid="Key"` (with `.resw`) | Resource string — most common WPF case; mechanical `{x:Bind}` will NOT compile here |
|
||||
| `{x:Static prefix:Type.Member}` | `{x:Bind prefix:Type.Member}` | Static field/property reference (function binding) |
|
||||
| `Visibility="Hidden"` | `Visibility="Collapsed"` | Or use `Opacity="0"` for layout |
|
||||
| `MouseLeftButtonDown` | `PointerPressed` | Event handlers |
|
||||
| `MouseLeftButtonUp` | `PointerReleased` | Event handlers |
|
||||
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` + check `IsRightButtonPressed` |
|
||||
| `MouseRightButtonUp` | `PointerReleased` + check `IsRightButtonPressed` | No direct WinUI event; use `RightTapped` only for context-menu-open semantics |
|
||||
| `MouseEnter` | `PointerEntered` | Event handlers |
|
||||
| `MouseLeave` | `PointerExited` | Event handlers |
|
||||
| `MouseMove` | `PointerMoved` | Event handlers |
|
||||
| `MouseWheel` | `PointerWheelChanged` | Event handlers |
|
||||
| `MouseDoubleClick` | `DoubleTapped` | Event handlers |
|
||||
| `PreviewMouseDown` | `PointerPressed` | No tunneling in WinUI — remove Preview |
|
||||
| `PreviewMouseUp` | `PointerReleased` | No tunneling in WinUI — remove Preview |
|
||||
| `PreviewKeyDown` | `KeyDown` | No tunneling in WinUI — remove Preview |
|
||||
| `PreviewKeyUp` | `KeyUp` | No tunneling in WinUI — remove Preview |
|
||||
| `PreviewMouseWheel` | `PointerWheelChanged` | No tunneling in WinUI — remove Preview |
|
||||
| `Focusable="True"` | `IsTabStop="True"` | Focus behavior |
|
||||
| `Focusable="False"` | `IsTabStop="False"` | Focus behavior |
|
||||
| `TextWrapping="WrapWithOverflow"` | `TextWrapping="Wrap"` | TextBlock, TextBox |
|
||||
| `MediaElement` | `MediaPlayerElement` | Media playback |
|
||||
| `clr-namespace:` | `using:` | xmlns declarations |
|
||||
| `;assembly=` | (remove) | Assembly qualification not needed |
|
||||
|
||||
### Code-Behind Replacements
|
||||
|
||||
| Find | Replace With |
|
||||
|------|-------------|
|
||||
| `using System.Windows;` | `using Microsoft.UI.Xaml;` |
|
||||
| `using System.Windows.Controls;` | `using Microsoft.UI.Xaml.Controls;` |
|
||||
| `using System.Windows.Media;` | `using Microsoft.UI.Xaml.Media;` |
|
||||
| `using System.Windows.Data;` | `using Microsoft.UI.Xaml.Data;` |
|
||||
| `using System.Windows.Input;` | `using Microsoft.UI.Xaml.Input;` |
|
||||
| `using System.Windows.Threading;` | `using Microsoft.UI.Dispatching;` |
|
||||
| `using System.Windows.Shapes;` | `using Microsoft.UI.Xaml.Shapes;` |
|
||||
| `using System.Windows.Markup;` | `using Microsoft.UI.Xaml.Markup;` |
|
||||
| `using System.Windows.Automation;` | `using Microsoft.UI.Xaml.Automation;` |
|
||||
| `using System.Windows.Media.Animation;` | `using Microsoft.UI.Xaml.Media.Animation;` |
|
||||
| `using System.Windows.Documents;` | `using Microsoft.UI.Xaml.Documents;` |
|
||||
| `using System.Windows.Navigation;` | `using Microsoft.UI.Xaml.Navigation;` |
|
||||
| `Dispatcher.Invoke(` | `DispatcherQueue.TryEnqueue(` |
|
||||
| `Dispatcher.BeginInvoke(` | `DispatcherQueue.TryEnqueue(` |
|
||||
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` |
|
||||
| `MouseEventArgs` | `PointerRoutedEventArgs` |
|
||||
| `KeyEventArgs` | `KeyRoutedEventArgs` |
|
||||
| `RoutedUICommand` | `RelayCommand` (CommunityToolkit.Mvvm) |
|
||||
| `CommandBinding` | Remove; bind ICommand directly |
|
||||
|
||||
## XAML Formatting (XamlStyler)
|
||||
|
||||
After migration, run XamlStyler to normalize formatting:
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
name: Automatic Triaging on Issue/PR Creation
|
||||
name: Automatic Triaging on Issue Creation
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize]
|
||||
# Manual trigger: go to Actions → "Automatic Triaging on Issue Creation" → Run workflow.
|
||||
# Enter one or more comma-separated issue numbers (e.g. "1234" or "1234,1235,1236")
|
||||
# to apply AI-generated area labels to existing untriaged issues.
|
||||
@@ -17,14 +15,12 @@ on:
|
||||
permissions:
|
||||
models: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
# Each workflow run gets its own concurrency group.
|
||||
# For issue events, group by issue number so a rapid close+reopen only runs once.
|
||||
# For pull request events, group by PR number so rapid updates coalesce.
|
||||
# For manual dispatch (which may cover multiple issues), use the unique run ID.
|
||||
group: ${{ github.event_name == 'issues' && format('{0}-issues-{1}', github.workflow, github.event.issue.number) || github.event_name == 'pull_request_target' && format('{0}-pr-{1}', github.workflow, github.event.pull_request.number) || github.run_id }}
|
||||
group: ${{ github.event_name == 'issues' && format('{0}-issue-{1}', github.workflow, github.event.issue.number) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -32,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Apply area labels with AI
|
||||
uses: actions/github-script@v9
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
# actions/github-script does not propagate `github-token` to
|
||||
# process.env. Expose it explicitly so the inline script can
|
||||
@@ -42,7 +38,7 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
// When triggered manually, process each supplied issue number in turn.
|
||||
// When triggered by an issue or PR event, use the event's number.
|
||||
// When triggered by an issue event, use the event's issue number.
|
||||
let issueNumbers;
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
issueNumbers = String(context.payload.inputs.issue_numbers)
|
||||
@@ -59,32 +55,23 @@ jobs:
|
||||
}
|
||||
|
||||
for (const issueNumber of issueNumbers) {
|
||||
console.log(`\n--- Processing item #${issueNumber} ---`);
|
||||
console.log(`\n--- Processing issue #${issueNumber} ---`);
|
||||
await labelIssue(issueNumber);
|
||||
}
|
||||
|
||||
async function labelIssue(issueNumber) {
|
||||
// Fetch as an issue resource; PRs are represented by issues with a pull_request field.
|
||||
// Fetch the issue so both the automatic and manual paths have the same data.
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
const itemType = issue.pull_request ? 'Pull request' : 'Issue';
|
||||
|
||||
// Skip pull requests that already have labels applied.
|
||||
if (issue.pull_request && issue.labels && issue.labels.length > 0) {
|
||||
const existingLabels = issue.labels.map(l => l.name).join(', ');
|
||||
console.log(`${itemType} #${issueNumber} already has labels (${existingLabels}); skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const title = issue.title ?? '';
|
||||
const body = issue.body ?? '';
|
||||
|
||||
if (!title && !body) {
|
||||
console.log(`${itemType} #${issueNumber} has no title or body; skipping.`);
|
||||
console.log(`Issue #${issueNumber} has no title or body; skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,8 +124,8 @@ jobs:
|
||||
'Area-Localization',
|
||||
];
|
||||
|
||||
const systemPrompt = `You are a GitHub triage assistant for the microsoft/PowerToys repository.
|
||||
Your job is to classify issues and pull requests by assigning the correct area label(s).
|
||||
const systemPrompt = `You are a GitHub issue triage assistant for the microsoft/PowerToys repository.
|
||||
Your job is to classify issues by assigning the correct area label(s).
|
||||
|
||||
Rules:
|
||||
- Only return labels from the following list, exactly as written:
|
||||
@@ -149,9 +136,9 @@ jobs:
|
||||
- Respond with ONLY a JSON array of label strings, no explanation.
|
||||
Example: ["Product-FancyZones","Product-Settings"]`;
|
||||
|
||||
const userPrompt = `${itemType} title: ${title}
|
||||
const userPrompt = `Issue title: ${title}
|
||||
|
||||
${itemType} body:
|
||||
Issue body:
|
||||
${body.slice(0, MAX_BODY_LENGTH)}`;
|
||||
|
||||
// Validate that the token is available before making the API call.
|
||||
@@ -216,25 +203,11 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: toApply,
|
||||
});
|
||||
console.log(`${itemType} #${issueNumber}: added labels: ${toApply.join(', ')}`);
|
||||
} catch (error) {
|
||||
// Some contexts (for example, restricted integrations) can deny
|
||||
// label writes even when workflow permissions request write scope.
|
||||
// Skip without failing the entire triage workflow.
|
||||
const status = error?.status;
|
||||
const message = error?.message ?? String(error);
|
||||
if (status === 403 && message.includes('Resource not accessible by integration')) {
|
||||
console.log(`${itemType} #${issueNumber}: skipping label write due to restricted token context (403).`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: toApply,
|
||||
});
|
||||
console.log(`Issue #${issueNumber}: added labels: ${toApply.join(', ')}`);
|
||||
}
|
||||
2
.github/workflows/dependency-review.yml
vendored
@@ -21,6 +21,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run GenAI Issue Deduplicator
|
||||
uses: pelikhan/action-genai-issue-dedup@v0
|
||||
|
||||
67
.github/workflows/regenerate-devdocs-website.yml
vendored
@@ -1,67 +0,0 @@
|
||||
# Builds the Dev Docs website from doc/devdocs with docmd and publishes it to GitHub Pages.
|
||||
#
|
||||
# The generated site is uploaded as a Pages artifact and deployed directly. It is never
|
||||
# committed to the repo, so doc/devdocs-website/site stays untracked (see .gitignore).
|
||||
#
|
||||
# Requires GitHub Pages to be enabled with "Source: GitHub Actions" under the repository
|
||||
# Settings -> Pages.
|
||||
name: Publish Dev Docs Website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'doc/devdocs/**'
|
||||
- 'doc/devdocs-website/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one Pages deployment at a time and let an in-progress deploy finish.
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
# Full history so docmd's git plugin can resolve per-page "last updated" dates.
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Build static site with docmd
|
||||
working-directory: doc/devdocs-website
|
||||
# docmd is pinned in package.json; dependencies are installed fresh each run.
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: doc/devdocs-website/site
|
||||
# v4+ excludes dotfiles by default; keep docmd's generated .nojekyll.
|
||||
include-hidden-files: true
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
2
.github/workflows/telemetry-pr-check.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Detect telemetry event changes and comment PR
|
||||
env:
|
||||
|
||||
6
.gitignore
vendored
@@ -19,9 +19,6 @@
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
!**/rnnoise/
|
||||
!**/rnnoise/x86/
|
||||
!**/rnnoise/x86/**
|
||||
ARM64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
@@ -378,6 +375,3 @@ installer/*/*.wxs.bk
|
||||
vcpkg_installed/
|
||||
|
||||
deps/vcpkg/
|
||||
|
||||
# Superpowers-generated docs (specs, design, plans) — local-only, not committed
|
||||
docs/superpowers/
|
||||
|
||||
@@ -4,29 +4,6 @@
|
||||
<Import Project="$(MSBuildCachePackageRoot)\build\$(MSBuildCachePackageName).targets" Condition="'$(MSBuildCacheEnabled)' == 'true'" />
|
||||
<Import Project="$(MSBuildCacheSharedCompilationPackageRoot)\build\Microsoft.MSBuildCache.SharedCompilation.targets" Condition="'$(MSBuildCacheEnabled)' == 'true'" />
|
||||
|
||||
<!--
|
||||
Onboarding guard: PowerToys has deeply nested source paths that exceed the legacy
|
||||
260-character MAX_PATH limit. Without Windows long path support enabled, the build
|
||||
fails with cryptic "path too long" / "could not find file" errors that are hard for
|
||||
new contributors to diagnose. Detect the missing registry setting up front and emit a
|
||||
clear, actionable error before the confusing failures occur.
|
||||
|
||||
- Covers both Visual Studio (Ctrl+Shift+B) and the command-line build scripts.
|
||||
- Runs only during real builds (skips design-time/IntelliSense passes).
|
||||
- Bypass with /p:SkipLongPathsCheck=true if you know what you're doing.
|
||||
See tools\build\setup-dev-environment.ps1 to enable everything automatically.
|
||||
-->
|
||||
<Target Name="EnsureLongPathsEnabled"
|
||||
BeforeTargets="PrepareForBuild"
|
||||
Condition="'$(DesignTimeBuild)' != 'true' and '$(SkipLongPathsCheck)' != 'true' and '$(OS)' == 'Windows_NT'">
|
||||
<PropertyGroup>
|
||||
<_LongPathsEnabled>$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem', 'LongPathsEnabled', null, RegistryView.Registry64))</_LongPathsEnabled>
|
||||
</PropertyGroup>
|
||||
<Error Condition="'$(_LongPathsEnabled)' != '1'"
|
||||
Code="PTLONGPATH"
|
||||
Text="Windows long path support is not enabled. PowerToys source paths exceed the 260-character MAX_PATH limit, so the build will fail with cryptic 'path too long' errors. Fix it by running (from an elevated PowerShell): .\tools\build\setup-dev-environment.ps1 -- or set HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled = 1 (DWORD) and restart Windows. To bypass this check, build with /p:SkipLongPathsCheck=true." />
|
||||
</Target>
|
||||
|
||||
<!-- Override ManifestTool to the x64 host tool under WindowsSdkDir for all projects once the SDK path is known. -->
|
||||
<PropertyGroup Label="ManifestToolOverride">
|
||||
<ManifestTool Condition="Exists('$(WindowsSdkDir)bin\x64\mt.exe')">$(WindowsSdkDir)bin\x64\mt.exe</ManifestTool>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Sizers" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
@@ -38,7 +38,7 @@
|
||||
<PackageVersion Include="Mages" Version="3.0.0" />
|
||||
<PackageVersion Include="Markdig.Signed" Version="0.34.0" />
|
||||
<!-- 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.7" />
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.102" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.9.260303001" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.8" />
|
||||
@@ -64,7 +64,7 @@
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.71.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.71.0-alpha" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.4022.49" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
|
||||
<!-- 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="10.0.8" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
@@ -76,12 +76,12 @@
|
||||
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1" />
|
||||
<PackageVersion Include="Microsoft.Windows.ImplementationLibrary" Version="1.0.250325.1"/>
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.1.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="2.2.3" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="2.0.20" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="2.0.185" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="2.0.1" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
@@ -151,4 +151,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
64
NOTICE.md
@@ -12,7 +12,6 @@ This software incorporates material from third parties.
|
||||
- Peek
|
||||
- PowerDisplay
|
||||
- Registry Preview
|
||||
- ZoomIt
|
||||
|
||||
## Utility: Color Picker
|
||||
|
||||
@@ -1550,69 +1549,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
## Utility: ZoomIt
|
||||
|
||||
### libwebp
|
||||
|
||||
ZoomIt uses libwebp to encode screenshots in the WebP image format.
|
||||
|
||||
**Source**: <https://github.com/webmproject/libwebp>
|
||||
|
||||
BSD-3-Clause License
|
||||
|
||||
Copyright (c) 2010, Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of Google nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"These implementations" means the copyrightable works that implement the WebM
|
||||
codecs distributed by Google as part of the WebM Project.
|
||||
|
||||
Google hereby grants to you a perpetual, worldwide, non-exclusive, no-charge,
|
||||
royalty-free, irrevocable (except as stated in this section) patent license to
|
||||
make, have made, use, offer to sell, sell, import, transfer, and otherwise
|
||||
run, modify and propagate the contents of these implementations of WebM, where
|
||||
such license applies only to those patent claims, both currently owned by
|
||||
Google and acquired in the future, licensable by Google that are necessarily
|
||||
infringed by these implementations of WebM. This grant does not include claims
|
||||
that would be infringed only as a consequence of further modification of these
|
||||
implementations. If you or your agent or exclusive licensee institute or order
|
||||
or agree to the institution of patent litigation or any other patent
|
||||
enforcement activity against any entity (including a cross-claim or
|
||||
counterclaim in a lawsuit) alleging that any of these implementations of WebM
|
||||
or any code incorporated within any of these implementations of WebM
|
||||
constitute direct or contributory patent infringement, or inducement of
|
||||
patent infringement, then any patent rights granted to you under this License
|
||||
for these implementations of WebM shall terminate as of the date such
|
||||
litigation is filed.
|
||||
|
||||
## NuGet Packages used by PowerToys
|
||||
|
||||
- AdaptiveCards.ObjectModel.WinUI3
|
||||
|
||||
@@ -656,10 +656,7 @@
|
||||
</Project>
|
||||
</Folder>
|
||||
<Folder Name="/modules/launcher/Tests/">
|
||||
<Project Path="src/modules/launcher/Plugins/Microsoft.Plugin.Folder.UnitTests/Microsoft.Plugin.Folder.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<File Path="src/modules/launcher/Plugins/Microsoft.Plugin.Folder.UnitTests/Microsoft.Plugin.Folder.UnitTests.csproj" />
|
||||
<Project Path="src/modules/launcher/Plugins/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest/Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
2
doc/devdocs-website/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
# docmd build output — published to GitHub Pages by CI, not committed.
|
||||
site/
|
||||
@@ -1,3 +0,0 @@
|
||||
# No lockfile: docmd is pinned in package.json and installed fresh on every build
|
||||
# (locally and in CI), so a tracked package-lock.json is unnecessary here.
|
||||
package-lock=false
|
||||
@@ -1,44 +0,0 @@
|
||||
# Dev Docs Website
|
||||
|
||||
This folder hosts the [docmd](https://docmd.io/) project that turns the PowerToys developer
|
||||
documentation in [`doc/devdocs`](../devdocs) into a static website.
|
||||
|
||||
## Generated site
|
||||
|
||||
The `site/` folder is the docmd build output. It is **not committed** to the repository — it is
|
||||
git-ignored and rebuilt on demand. You only need it locally when previewing your changes (see below).
|
||||
|
||||
Publishing is handled by the
|
||||
[Publish Dev Docs Website](../../.github/workflows/regenerate-devdocs-website.yml) GitHub Action, which
|
||||
runs whenever files under `doc/devdocs` (or this folder) change on the `main` branch — it can also
|
||||
be triggered manually from the **Actions** tab. The action builds the site and deploys it straight to
|
||||
GitHub Pages as an artifact, so nothing is written back to the repository.
|
||||
|
||||
> [!NOTE]
|
||||
> The action requires GitHub Pages to be enabled with **Source: GitHub Actions** under the repository
|
||||
> **Settings → Pages**.
|
||||
|
||||
## Editing the docs
|
||||
|
||||
To change the documentation, edit the Markdown files under [`doc/devdocs`](../devdocs). The remaining
|
||||
files in this folder are maintained by hand and are safe to edit:
|
||||
|
||||
- `docmd.config.json` — docmd configuration (title, source, output, plugins)
|
||||
- `package.json` — pins the docmd version used to build the site
|
||||
- `docmd-plugins/` — local build-time docmd plugins
|
||||
|
||||
> [!TIP]
|
||||
> Link to repository files with repo-root-relative paths such as `/src/modules/.../Foo.cpp`.
|
||||
> VS Code resolves these against the workspace root (so they open the local file), and the
|
||||
> bundled `github-source-links` plugin rewrites them to
|
||||
> `https://github.com/microsoft/PowerToys/blob/main/...` on the published site.
|
||||
|
||||
## Building locally
|
||||
|
||||
Requires [Node.js](https://nodejs.org/).
|
||||
|
||||
```powershell
|
||||
npm install # install dependencies (first time only)
|
||||
npm run dev # start a local preview server at http://localhost:3000
|
||||
npm run build # generate the static site into ./site
|
||||
```
|
||||
@@ -1,52 +0,0 @@
|
||||
// docmd plugin: github-source-links
|
||||
//
|
||||
// The dev docs link to source files with repo-root-relative paths such as
|
||||
// "/src/modules/.../Foo.cpp". VS Code resolves those against the workspace root,
|
||||
// so they stay clickable while editing locally. On the published static site,
|
||||
// however, a "/src/..." link resolves against the site origin and 404s.
|
||||
//
|
||||
// This plugin rewrites those links to absolute GitHub blob URLs so they work on
|
||||
// the published site, while the Markdown sources stay untouched (keeping local
|
||||
// VS Code navigation intact).
|
||||
//
|
||||
// It hooks markdownSetup at the Markdown token level, so it only rewrites links
|
||||
// written in the docs' content. docmd's own generated links (sidebar, breadcrumbs,
|
||||
// canonical tags) are never seen here, which matters because an internal doc route
|
||||
// like "/tools/build-tools" is otherwise indistinguishable from a repo path like
|
||||
// "/tools/BugReportTool" once rendered to HTML.
|
||||
//
|
||||
// docmd appends a trailing slash to the rewritten links (".../Foo.cpp/"); GitHub
|
||||
// resolves that to the file anyway, so it is left as-is for simplicity.
|
||||
|
||||
const REPO_BLOB_BASE = 'https://github.com/microsoft/PowerToys/blob/main';
|
||||
|
||||
export default {
|
||||
plugin: {
|
||||
name: 'github-source-links',
|
||||
version: '1.0.0',
|
||||
capabilities: ['markdown'],
|
||||
},
|
||||
|
||||
markdownSetup(md) {
|
||||
const defaultRender =
|
||||
md.renderer.rules.link_open ||
|
||||
((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options));
|
||||
|
||||
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||
const token = tokens[idx];
|
||||
const hrefIndex = token.attrIndex('href');
|
||||
|
||||
if (hrefIndex >= 0) {
|
||||
const href = token.attrs[hrefIndex][1];
|
||||
|
||||
// Only repo-root-relative links ("/src/..."). Leave protocol-relative
|
||||
// ("//host"), absolute ("https://..."), relative and anchor links alone.
|
||||
if (href.length > 1 && href[0] === '/' && href[1] !== '/') {
|
||||
token.attrs[hrefIndex][1] = REPO_BLOB_BASE + href;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultRender(tokens, idx, options, env, self);
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "docmd-plugin-github-source-links",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"description": "docmd plugin that rewrites repo-root-relative doc links to GitHub blob URLs at build time."
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"title": "PowerToys Dev Docs",
|
||||
"src": "../devdocs",
|
||||
"out": "site",
|
||||
"base": "/",
|
||||
"plugins": {
|
||||
"docmd-plugin-github-source-links": {}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"name": "powertoys-devdocs-website",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Static Dev Docs website generated from doc/devdocs with docmd.",
|
||||
"scripts": {
|
||||
"dev": "docmd dev",
|
||||
"build": "docmd build"
|
||||
},
|
||||
"dependencies": {
|
||||
"docmd-plugin-github-source-links": "file:./docmd-plugins/github-source-links"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docmd/core": "0.8.6"
|
||||
}
|
||||
}
|
||||
@@ -42,4 +42,4 @@ In the PR that adds a new plugin, reference a new issue to track the work for fu
|
||||
|
||||
- [ ] Add the resource folder to https://github.com/microsoft/PowerToys/blob/21247c0bb09a1bee3d14d6efa53d0c247f7236af/installer/PowerToysSetup/Product.wxs#L825
|
||||
- [ ] Add the resource files under the section https://github.com/microsoft/PowerToys/blob/21247c0bb09a1bee3d14d6efa53d0c247f7236af/installer/PowerToysSetup/Product.wxs#L882
|
||||
- [ ] Your plugin's executable file (DLL) has to have correct version information after building it. (This version information will be shown on the settings page.)
|
||||
- [ ] Your plugin's executable file (DLL) has to have correct version informations after building it. (This version information will be shown on the settings page.)
|
||||
|
||||
BIN
doc/images/icons/Video Conference Mute.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 258 KiB |
19
doc/planning/FancyZonesBacklog.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Backlog
|
||||
|
||||
This file captures the prioritized list of issues the FancyZones team will tackle
|
||||
|
||||
## On deck
|
||||
|
||||
## Backlog
|
||||
Add tests to the new editor [197](https://github.com/microsoft/PowerToys/issues/197)
|
||||
Cycle through windows in a Zone [175](https://github.com/microsoft/PowerToys/issues/175)
|
||||
Minimize/restore windows in a zone as a group [174](https://github.com/microsoft/PowerToys/issues/174)
|
||||
FancyZones should support custom layouts for different "environments" [177](https://github.com/microsoft/PowerToys/issues/177)
|
||||
Win+arrow is directional based on zone rect [162](https://github.com/microsoft/PowerToys/issues/162)
|
||||
Dragging a zoned window should restore size to a checkpointed size instead of current rect [166](https://github.com/microsoft/PowerToys/issues/166)
|
||||
FancyZones should merge with MTND and include zone moves in the pop-up [178](https://github.com/microsoft/PowerToys/issues/178)
|
||||
Drag to edge of screen automatically switches virtual desktops [168](https://github.com/microsoft/PowerToys/issues/168)
|
||||
Visual updates for Win+Arrow [171](https://github.com/microsoft/PowerToys/issues/171)
|
||||
Add "magnetic dragging and resizing" mode to FancyZones [181](https://github.com/microsoft/PowerToys/issues/181)
|
||||
Create layout from current windows [159](https://github.com/microsoft/PowerToys/issues/159)
|
||||
Zone sets that have a dynamic number of zones [160](https://github.com/microsoft/PowerToys/issues/160)
|
||||
24
doc/planning/PowerToysBacklog.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# PowerToys Backlog
|
||||
|
||||
The list below is the set of utilities we're considering and the rough priority order of the utilities. If you have feedback on the order of the utilities, please use the issues for each one to provide that feedback. Note that new features for existing utilities (dock / undock zone layouts for FancyZones) are tracked in the backlog for each utility.
|
||||
|
||||
## On deck
|
||||
|
||||
* Maximize to new desktop widget - The MTND widget shows a pop-up button when a user hovers over the maximize / restore button on any window. Clicking it creates a new desktop, sends the app to that desktop and maximizes the app on the new desktop.
|
||||
* [Process terminate tool](https://github.com/indierawk2k2/PowerToys-1/blob/master/specs/Terminate%20Spec.md)
|
||||
* [Animated gif screen recorder](https://github.com/indierawk2k2/PowerToys-1/blob/master/specs/GIF%20Maker%20Spec.md)
|
||||
|
||||
## Backlog
|
||||
|
||||
Please use issues and votes to guide the project to suggest new ideas and help us prioritize the list below.
|
||||
|
||||
1. [Keyboard shortcut manager](https://github.com/microsoft/PowerToys/issues/6)
|
||||
2. [Win+R replacement](https://github.com/microsoft/PowerToys/issues/44)
|
||||
3. Resource use tool (maps between a resource like a file handle to an app and vice-versa)
|
||||
4. Performance analysis over time to track which processes have been slowing down your machine
|
||||
5. Better Alt+Tab including browser tab integration and search for running apps
|
||||
6. [Battery tracker](https://github.com/microsoft/PowerToys/issues/7)
|
||||
7. [Quick resolution swaps in taskbar](https://github.com/microsoft/PowerToys/issues/27)
|
||||
8. Mouse events without focus
|
||||
9. Cmd (or PS or Bash) from here
|
||||
10. Contents menu file browsing
|
||||
13
doc/planning/ShortcutGuideBacklog.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Backlog
|
||||
|
||||
This file captures the prioritized list of issues for the Windows key shortcut guide
|
||||
|
||||
## On deck
|
||||
Windows key shortcut guide animation performance is choppy [198](https://github.com/microsoft/PowerToys/issues/198)
|
||||
Shortcut guide strings should be localized [199](https://github.com/microsoft/PowerToys/issues/199)
|
||||
|
||||
## Backlog
|
||||
Add Win+Shift+S to the WKSG (screenshot tool) [179](https://github.com/microsoft/PowerToys/issues/179)
|
||||
Replace SVG with software-generated content. [156](https://github.com/microsoft/PowerToys/issues/156)
|
||||
Shortcut sorting [154](https://github.com/microsoft/PowerToys/issues/154)
|
||||
Make shortcut descriptors clickable [152](https://github.com/microsoft/PowerToys/issues/152)
|
||||
114
doc/planning/awake.md
Normal file
@@ -0,0 +1,114 @@
|
||||
---
|
||||
last-update: 1-18-2026
|
||||
---
|
||||
|
||||
# PowerToys Awake Changelog
|
||||
|
||||
## Builds
|
||||
|
||||
The build ID can be found in `Core\Constants.cs` in the `BuildId` variable - it is a unique identifier for the current builds that allows better diagnostics (we can look up the build ID from the logs) and offers a way to triage Awake-specific issues faster independent of the PowerToys version. The build ID does not carry any significance beyond that within the PowerToys code base.
|
||||
|
||||
The build ID moniker is made up of two components - a reference to a [Halo](https://en.wikipedia.org/wiki/Halo_(franchise)) character, and the date when the work on the specific build started in the format of `MMDDYYYY`.
|
||||
|
||||
| Build ID | Build Date |
|
||||
|:-------------------------------------------------------------------|:------------------|
|
||||
| [`DIDACT_01182026`](#DIDACT_01182026-january-18-2026) | January 18, 2026 |
|
||||
| [`TILLSON_11272024`](#TILLSON_11272024-november-27-2024) | November 27, 2024 |
|
||||
| [`PROMETHEAN_09082024`](#PROMETHEAN_09082024-september-8-2024) | September 8, 2024 |
|
||||
| [`VISEGRADRELAY_08152024`](#VISEGRADRELAY_08152024-august-15-2024) | August 15, 2024 |
|
||||
| [`DAISY023_04102024`](#DAISY023_04102024-april-10-2024) | April 10, 2024 |
|
||||
| [`ATRIOX_04132023`](#ATRIOX_04132023-april-13-2023) | April 13, 2023 |
|
||||
| [`LIBRARIAN_03202022`](#librarian_03202022-march-20-2022) | March 20, 2022 |
|
||||
| `ARBITER_01312022` | January 31, 2022 |
|
||||
|
||||
### `DIDACT_01182026` (January 18, 2026)
|
||||
|
||||
>[!NOTE]
|
||||
>See pull request: [Awake - `DIDACT_01182026`](https://github.com/microsoft/PowerToys/pull/44795)
|
||||
|
||||
- [#32544](https://github.com/microsoft/PowerToys/issues/32544) Fixed an issue where Awake settings became non-functional after the PC wakes from sleep. Added `WM_POWERBROADCAST` handling to detect system resume events (`PBT_APMRESUMEAUTOMATIC`, `PBT_APMRESUMESUSPEND`) and re-apply `SetThreadExecutionState` to restore the awake state.
|
||||
- [#36150](https://github.com/microsoft/PowerToys/issues/36150) Fixed an issue where Awake would not prevent sleep when AC power is connected. Added `PBT_APMPOWERSTATUSCHANGE` handling to re-apply `SetThreadExecutionState` when the power source changes (AC/battery transitions).
|
||||
- Fixed an issue where toggling "Keep screen on" during an active timed session would disrupt the countdown timer. The display setting now updates directly without restarting the timer, preserving the exact remaining time.
|
||||
- [#41918](https://github.com/microsoft/PowerToys/issues/41918) Fixed `WM_COMMAND` message processing flaw in `TrayHelper.WndProc` that incorrectly compared enum values against enum count. Added proper bounds checking for custom tray time entries.
|
||||
- Investigated [#44134](https://github.com/microsoft/PowerToys/issues/44134) - documented that `ES_DISPLAY_REQUIRED` (used when "Keep display on" is enabled) blocks Task Scheduler idle detection, preventing scheduled maintenance tasks like SSD TRIM. Workaround: disable "Keep display on" or manually run `Optimize-Volume -DriveLetter C -ReTrim`. Additional investigation needed for potential "idle window" feature.
|
||||
- [#41738](https://github.com/microsoft/PowerToys/issues/41738) Fixed `--display-on` CLI flag default from `true` to `false` to align with documentation and PowerToys settings behavior. This is a breaking change for scripts relying on the undocumented default.
|
||||
- [#41674](https://github.com/microsoft/PowerToys/issues/41674) Fixed silent failure when `SetThreadExecutionState` fails. The monitor thread now handles the return value, logs an error, and reverts to passive mode with updated tray icon.
|
||||
- [#38770](https://github.com/microsoft/PowerToys/issues/38770) Fixed tray icon failing to appear after Windows updates. Increased retry attempts and delays for icon Add operations (10 attempts, up to ~15.5 seconds total) while keeping existing fast retry behavior for Update/Delete operations.
|
||||
- [#40501](https://github.com/microsoft/PowerToys/issues/40501) Fixed tray icon not disappearing when Awake is disabled. The `SetShellIcon` function was incorrectly requiring an icon for Delete operations, causing the `NIM_DELETE` message to never be sent.
|
||||
- [#40659](https://github.com/microsoft/PowerToys/issues/40659) Fixed potential stack overflow crash in EXPIRABLE mode. Added early return after SaveSettings when correcting past expiration times, matching the pattern used by other mode handlers to prevent reentrant execution.
|
||||
|
||||
### `TILLSON_11272024` (November 27, 2024)
|
||||
|
||||
>[!NOTE]
|
||||
>See pull request: [Awake - `TILLSON_11272024`](https://github.com/microsoft/PowerToys/pull/36049)
|
||||
|
||||
- [#35250](https://github.com/microsoft/PowerToys/issues/35250) Updates the icon retry policy, making sure that the icon consistently and correctly renders in the tray.
|
||||
- [#35848](https://github.com/microsoft/PowerToys/issues/35848) Fixed a bug where custom tray time shortcuts for longer than 24 hours would be parsed as zero hours/zero minutes.
|
||||
- [#34716](https://github.com/microsoft/PowerToys/issues/34716) Properly recover the state icon in the tray after an `explorer.exe` crash.
|
||||
- Added configuration safeguards to make sure that invalid values for timed keep-awake times do not result in exceptions.
|
||||
- Updated the tray initialization logic, making sure we wait for it to be properly created before setting icons.
|
||||
- Expanded logging capabilities to track invoking functions.
|
||||
- Added command validation logic to make sure that incorrect command line arguments display an error.
|
||||
- Display state now shown in the tray tooltip.
|
||||
- When timed mode is used, changing the display setting will no longer reset the timer.
|
||||
|
||||
### `PROMETHEAN_09082024` (September 8, 2024)
|
||||
|
||||
>[!NOTE]
|
||||
>See pull request: [Awake - `PROMETHEAN_09082024`](https://github.com/microsoft/PowerToys/pull/34717)
|
||||
|
||||
- Updating the initialization logic to make sure that settings are respected for proper group policy and single-instance detection.
|
||||
- [#34148](https://github.com/microsoft/PowerToys/issues/34148) Fixed a bug from the previous release that incorrectly synchronized threads for shell icon creation and initialized parent PID when it was not parented.
|
||||
|
||||
### `VISEGRADRELAY_08152024` (August 15, 2024)
|
||||
|
||||
>[!NOTE]
|
||||
>See pull request: [Awake - `VISEGRADRELAY_08152024`](https://github.com/microsoft/PowerToys/pull/34316)
|
||||
|
||||
- [#34148](https://github.com/microsoft/PowerToys/issues/34148) Fixes the issue where the Awake icon is not displayed.
|
||||
- [#17969](https://github.com/microsoft/PowerToys/issues/17969) Add the ability to bind the process target to the parent of the Awake launcher.
|
||||
- PID binding now correctly ignores irrelevant parameters (e.g., expiration, interval) and only works for indefinite periods.
|
||||
- Amending the native API surface to make sure that the Win32 error is set correctly.
|
||||
|
||||
### `DAISY023_04102024` (April 10, 2024)
|
||||
|
||||
>[!NOTE]
|
||||
>See pull request: [Awake Update - `DAISY023_04102024`](https://github.com/microsoft/PowerToys/pull/32378)
|
||||
|
||||
- [#33630](https://github.com/microsoft/PowerToys/issues/33630) When in the UI and you select `0` as hours and `0` as minutes in `TIMED` awake mode, the UI becomes non-responsive whenever you try to get back to timed after it rolls back to `PASSIVE`.
|
||||
- [#12714](https://github.com/microsoft/PowerToys/issues/12714) Adds the option to keep track of Awake state through tray tooltip.
|
||||
- [#11996](https://github.com/microsoft/PowerToys/issues/11996) Adds custom icons support for mode changes in Awake.
|
||||
- Removes the dependency on `System.Windows.Forms` and instead uses native Windows APIs to create the tray icon.
|
||||
- Removes redundant/unused code that impacted application performance.
|
||||
- Updates dependent packages to their latest versions (`Microsoft.Windows.CsWinRT` and `System.Reactive`).
|
||||
|
||||
### `ATRIOX_04132023` (April 13, 2023)
|
||||
|
||||
- Moves from using `Task.Run` to spin up threads to actually using a blocking queue that properly sets thread parameters on the same thread.
|
||||
- Moves back to using native Windows APIs through P/Invoke instead of using a package.
|
||||
- Move away from custom logging and to built-in logging that is consistent with the rest of PowerToys.
|
||||
- Updates `System.CommandLine` and `System.Reactive` to the latest preview versions of the package.
|
||||
|
||||
### `LIBRARIAN_03202022` (March 20, 2022)
|
||||
|
||||
- Changed the tray context menu to be following OS conventions instead of the style offered by Windows Forms. This introduces better support for DPI scaling and theming in the future.
|
||||
- Custom times in the tray can now be configured in the `settings.json` file for awake, through the `tray_times` property. The property values are representative of a `Dictionary<string, int>` and can be in the form of `"YOUR_NAME": LENGTH_IN_SECONDS`:
|
||||
|
||||
```json
|
||||
{
|
||||
"properties": {
|
||||
"awake_keep_display_on": true,
|
||||
"awake_mode": 2,
|
||||
"awake_hours": 0,
|
||||
"awake_minutes": 3,
|
||||
"tray_times": {
|
||||
"Custom length": 1800,
|
||||
"Another custom length": 3600
|
||||
}
|
||||
},
|
||||
"name": "Awake",
|
||||
"version": "1.0"
|
||||
}
|
||||
```
|
||||
|
||||
- Proper Awake background window closure was implemented to ensure that the process collects the correct handle instead of the empty one that was previously done through `System.Diagnostics.Process.GetCurrentProcess().CloseMainWindow()`. This likely can help with the Awake process that is left hanging after PowerToys itself closes.
|
||||
@@ -79,4 +79,4 @@ Below are community created plugins that target a website or software. They are
|
||||
| [Linear](https://github.com/vednig/powertoys-linear) | [vednig](https://github.com/vednig) | Create Linear Issues directly from Powertoys Run |
|
||||
| [PerplexitySearchShortcut](https://github.com/0x6f677548/PowerToys-Run-PerplexitySearchShortcut) | [0x6f677548](https://github.com/0x6f677548) | Search Perplexity |
|
||||
| [SpeedTest](https://github.com/ruslanlap/PowerToysRun-SpeedTest) | [ruslanlap](https://github.com/ruslanlap) | One-command internet speed tests with real-time results, modern UI, and shareable links. |
|
||||
| [DiskAnalyzer](https://github.com/valley-soft/powertoys-diskanalyzer) | [ValleySoft](https://github.com/valley-soft) | Scan folders, find the largest files, and view drive space usage. |
|
||||
| [DiskAnalyzer](https://github.com/thetsaw/PowerToys.Plugin) | [thetsaw](https://github.com/thetsaw) | Scan folders, find the largest files, and view drive space usage with visual progress bars. |
|
||||
|
||||
37
doc/unofficialInstallMethods.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Unofficial community driven install methods
|
||||
|
||||
These are community driven alternative install methods to Windows Package Manager (WinGet) and GitHub. The PowerToys teams does not update or manage these install methods.
|
||||
|
||||
These will be listed in alphabetical order.
|
||||
|
||||
## Chocolatey
|
||||
|
||||
Download and upgrade PowerToys from [Chocolatey](https://chocolatey.org). If you have any issues when installing/upgrading the package please go to the [package page](https://chocolatey.org/packages/powertoys) and follow the [Chocolatey triage process](https://chocolatey.org/docs/package-triage-process)
|
||||
|
||||
To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
```powershell
|
||||
choco install powertoys
|
||||
```
|
||||
|
||||
To upgrade PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
```powershell
|
||||
choco upgrade powertoys
|
||||
```
|
||||
|
||||
## Scoop
|
||||
|
||||
Download and update PowerToys from [Scoop](https://scoop.sh).
|
||||
|
||||
To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
```powershell
|
||||
scoop install powertoys
|
||||
```
|
||||
|
||||
To update PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
```powershell
|
||||
scoop update powertoys
|
||||
```
|
||||
@@ -95,9 +95,6 @@ std::optional<fs::path> CopySelfToTempDir()
|
||||
return dst_path;
|
||||
}
|
||||
|
||||
// The installer filename read from UpdateState.json is validated by
|
||||
// updating::IsSafeDownloadedInstallerFilename (common/updating/updateLifecycle.h)
|
||||
// so it can be unit-tested. See ObtainInstaller below for how it's used.
|
||||
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
{
|
||||
using namespace updating;
|
||||
@@ -110,25 +107,7 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
|
||||
// so we don't need a GitHub API call (which may fail if offline).
|
||||
if (state.state == UpdateState::readyToInstall)
|
||||
{
|
||||
if (!IsSafeDownloadedInstallerFilename(state.downloadedInstallerFilename))
|
||||
{
|
||||
Logger::error(L"Ignoring unexpected downloadedInstallerFilename from update state: {}", state.downloadedInstallerFilename);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const fs::path updatesDir = get_pending_updates_path();
|
||||
fs::path installer{ updatesDir / state.downloadedInstallerFilename };
|
||||
|
||||
// Make sure the resolved path actually stays within the Updates directory.
|
||||
std::error_code ec;
|
||||
const fs::path normalizedInstaller = fs::weakly_canonical(installer, ec);
|
||||
const fs::path normalizedUpdatesDir = fs::weakly_canonical(updatesDir, ec);
|
||||
if (ec || normalizedInstaller.parent_path() != normalizedUpdatesDir)
|
||||
{
|
||||
Logger::error(L"Resolved installer path is outside the updates directory: {}", installer.native());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
|
||||
if (fs::is_regular_file(installer))
|
||||
{
|
||||
return std::move(installer);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
|
||||
</packages>
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
</packages>
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<ClCompile>
|
||||
<!-- We use MultiThreadedDebug, rather than MultiThreadedDebugDLL, to avoid DLL dependencies on VCRUNTIME140d.dll and MSVCP140d.dll. -->
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<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
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Backdrops;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="SystemBackdrop"/> that renders desktop acrylic and stays in
|
||||
/// the active visual state even when the hosting window is not activated.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The built-in <see cref="DesktopAcrylicBackdrop"/> tracks the host window's
|
||||
/// <c>IsInputActive</c> state and falls back to a solid color whenever the
|
||||
/// window is not the foreground window. That makes it unusable for transient,
|
||||
/// non-activating surfaces such as toasts or popups created with
|
||||
/// <c>SW_SHOWNA</c> / <c>WS_EX_TRANSPARENT</c>, where the window is never
|
||||
/// activated by design.
|
||||
///
|
||||
/// This backdrop drives a <see cref="DesktopAcrylicController"/> with a
|
||||
/// <see cref="SystemBackdropConfiguration"/> whose <c>IsInputActive</c> is
|
||||
/// permanently <see langword="true"/>, so the native acrylic effect is always
|
||||
/// rendered.
|
||||
/// </remarks>
|
||||
public sealed partial class AlwaysActiveDesktopAcrylicBackdrop : SystemBackdrop
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="Kind"/> dependency property.
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty KindProperty = DependencyProperty.Register(
|
||||
nameof(Kind),
|
||||
typeof(DesktopAcrylicKind),
|
||||
typeof(AlwaysActiveDesktopAcrylicBackdrop),
|
||||
new PropertyMetadata(DesktopAcrylicKind.Default, OnKindChanged));
|
||||
|
||||
private readonly Dictionary<ICompositionSupportsSystemBackdrop, BackdropTarget> _targets = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desktop acrylic material variant to render. Defaults to
|
||||
/// <see cref="DesktopAcrylicKind.Default"/> (the standard, more opaque
|
||||
/// acrylic); <see cref="DesktopAcrylicKind.Thin"/> renders a lighter, more
|
||||
/// translucent material and <see cref="DesktopAcrylicKind.Base"/> the base
|
||||
/// material. Changing this updates any live backdrop targets immediately.
|
||||
/// </summary>
|
||||
public DesktopAcrylicKind Kind
|
||||
{
|
||||
get => (DesktopAcrylicKind)GetValue(KindProperty);
|
||||
set => SetValue(KindProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnTargetConnected(ICompositionSupportsSystemBackdrop connectedTarget, XamlRoot xamlRoot)
|
||||
{
|
||||
base.OnTargetConnected(connectedTarget, xamlRoot);
|
||||
|
||||
var configuration = new SystemBackdropConfiguration
|
||||
{
|
||||
IsInputActive = true,
|
||||
Theme = ResolveTheme(xamlRoot),
|
||||
};
|
||||
|
||||
var controller = new DesktopAcrylicController
|
||||
{
|
||||
Kind = Kind,
|
||||
};
|
||||
controller.SetSystemBackdropConfiguration(configuration);
|
||||
controller.AddSystemBackdropTarget(connectedTarget);
|
||||
|
||||
var target = new BackdropTarget(controller, configuration, xamlRoot);
|
||||
_targets[connectedTarget] = target;
|
||||
|
||||
if (xamlRoot.Content is FrameworkElement rootElement)
|
||||
{
|
||||
rootElement.ActualThemeChanged += target.OnActualThemeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTargetDisconnected(ICompositionSupportsSystemBackdrop disconnectedTarget)
|
||||
{
|
||||
base.OnTargetDisconnected(disconnectedTarget);
|
||||
|
||||
if (_targets.Remove(disconnectedTarget, out var target))
|
||||
{
|
||||
if (target.XamlRoot.Content is FrameworkElement rootElement)
|
||||
{
|
||||
rootElement.ActualThemeChanged -= target.OnActualThemeChanged;
|
||||
}
|
||||
|
||||
target.Controller.RemoveSystemBackdropTarget(disconnectedTarget);
|
||||
target.Controller.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnKindChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var self = (AlwaysActiveDesktopAcrylicBackdrop)d;
|
||||
var kind = (DesktopAcrylicKind)e.NewValue;
|
||||
|
||||
foreach (var target in self._targets.Values)
|
||||
{
|
||||
target.Controller.Kind = kind;
|
||||
}
|
||||
}
|
||||
|
||||
private static SystemBackdropTheme ResolveTheme(XamlRoot xamlRoot) =>
|
||||
xamlRoot.Content is FrameworkElement rootElement
|
||||
? rootElement.ActualTheme switch
|
||||
{
|
||||
ElementTheme.Dark => SystemBackdropTheme.Dark,
|
||||
ElementTheme.Light => SystemBackdropTheme.Light,
|
||||
_ => SystemBackdropTheme.Default,
|
||||
}
|
||||
: SystemBackdropTheme.Default;
|
||||
|
||||
private sealed class BackdropTarget
|
||||
{
|
||||
public BackdropTarget(DesktopAcrylicController controller, SystemBackdropConfiguration configuration, XamlRoot xamlRoot)
|
||||
{
|
||||
Controller = controller;
|
||||
Configuration = configuration;
|
||||
XamlRoot = xamlRoot;
|
||||
}
|
||||
|
||||
public DesktopAcrylicController Controller { get; }
|
||||
|
||||
public SystemBackdropConfiguration Configuration { get; }
|
||||
|
||||
public XamlRoot XamlRoot { get; }
|
||||
|
||||
public void OnActualThemeChanged(FrameworkElement sender, object args)
|
||||
{
|
||||
Configuration.Theme = ResolveTheme(XamlRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="WinUIEx" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:backdrops="using:Microsoft.PowerToys.Common.UI.Controls.Backdrops"
|
||||
xmlns:local="using:Microsoft.PowerToys.Common.UI.Controls">
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultTransientSurfaceStyle}" TargetType="local:TransientSurface" />
|
||||
|
||||
<Style x:Key="DefaultTransientSurfaceStyle" TargetType="local:TransientSurface">
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource SurfaceStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:TransientSurface">
|
||||
<Grid
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
Translation="0,0,24">
|
||||
<Grid.Shadow>
|
||||
<ThemeShadow />
|
||||
</Grid.Shadow>
|
||||
<SystemBackdropElement CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<SystemBackdropElement.SystemBackdrop>
|
||||
<backdrops:AlwaysActiveDesktopAcrylicBackdrop Kind="{TemplateBinding AcrylicKind}" />
|
||||
</SystemBackdropElement.SystemBackdrop>
|
||||
</SystemBackdropElement>
|
||||
<ContentPresenter
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,467 +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 CommunityToolkit.WinUI;
|
||||
using CommunityToolkit.WinUI.Animations;
|
||||
using Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A floating, self-animating "pseudo window" surface for transient PowerToys
|
||||
/// overlays (toasts, banners, indicators). It looks like a control but behaves
|
||||
/// like a lightweight window: it provides the PowerToys-standard chrome — 1 px
|
||||
/// border in <c>SurfaceStrokeColorDefaultBrush</c>, 8 px corner radius, a
|
||||
/// <c>ThemeShadow</c>, and an always-active desktop acrylic backdrop — and owns
|
||||
/// its own show/hide animations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Designed to be declared as the root content of a
|
||||
/// <see cref="TransparentWindow"/>, which stays animation-agnostic. Call
|
||||
/// <see cref="SubscribeTo"/> once (e.g. from the hosting window's constructor)
|
||||
/// to wire this surface to the window's <see cref="TransparentWindow.Showing"/> /
|
||||
/// <see cref="TransparentWindow.Hiding"/> events. From then on the surface
|
||||
/// animates itself in/out whenever the window is shown or hidden, and uses the
|
||||
/// <see cref="TransparentWindow.Hiding"/> deferral to keep the window visible
|
||||
/// until its out-animation finishes.</para>
|
||||
/// <para>The show transition comes from the window's
|
||||
/// <see cref="TransparentWindow.Show(Transition)"/> call (or from
|
||||
/// <see cref="ShowTransition"/> when shown without one); the hide transition
|
||||
/// always comes from <see cref="HideTransition"/>. Animations target the
|
||||
/// surface itself, so the entire surface (border, acrylic, shadow, inner
|
||||
/// content) animates as one. Apps that want a different look supply their own
|
||||
/// <c>Style TargetType="TransientSurface"</c> in resources — the standard WinUI
|
||||
/// restyle path.</para>
|
||||
/// </remarks>
|
||||
public sealed partial class TransientSurface : ContentControl
|
||||
{
|
||||
private const float ShadowDepth = 32f;
|
||||
private const double SlideInOffset = 24;
|
||||
private const double SlideOutOffset = 12;
|
||||
|
||||
// "Pop" transition: scale between 96% and 100% (a subtle 4% grow). Following
|
||||
// Fluent motion guidance the scale uses a decelerate (EaseOut) curve; the
|
||||
// fade is kept fast so the surface reads as an instant, light pop.
|
||||
//
|
||||
// The fade must run at least as long as the scale: if the scale outlasted the
|
||||
// fade, the surface would reach full opacity while still visibly growing,
|
||||
// which reads as a "resize" rather than a pop. Keeping the fade >= the scale
|
||||
// hides the growth under the opacity ramp, so by the time it is fully opaque
|
||||
// it is already at 100% size.
|
||||
private const float PopScaleFrom = 0.96f;
|
||||
private const double PopFadeShowMs = 180;
|
||||
private const double PopScaleShowMs = 150;
|
||||
private const double PopFadeHideMs = 120;
|
||||
private const double PopScaleHideMs = 120;
|
||||
|
||||
public static readonly DependencyProperty ShowTransitionProperty = DependencyProperty.Register(
|
||||
nameof(ShowTransition),
|
||||
typeof(Transition),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(Transition.None, OnTransitionChanged));
|
||||
|
||||
public static readonly DependencyProperty HideTransitionProperty = DependencyProperty.Register(
|
||||
nameof(HideTransition),
|
||||
typeof(Transition),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(Transition.None, OnTransitionChanged));
|
||||
|
||||
public static readonly DependencyProperty AcrylicKindProperty = DependencyProperty.Register(
|
||||
nameof(AcrylicKind),
|
||||
typeof(DesktopAcrylicKind),
|
||||
typeof(TransientSurface),
|
||||
new PropertyMetadata(DesktopAcrylicKind.Thin));
|
||||
|
||||
private readonly DispatcherQueueTimer _hideCompletedTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
|
||||
private readonly ImplicitAnimationSet _noAnimations = new();
|
||||
|
||||
private ImplicitAnimationSet _showAnimations = new();
|
||||
private ImplicitAnimationSet _hideAnimations = new();
|
||||
private bool _hasCustomShowAnimations;
|
||||
private bool _hasCustomHideAnimations;
|
||||
private Action? _abandonPendingHide;
|
||||
|
||||
public TransientSurface()
|
||||
{
|
||||
DefaultStyleKey = typeof(TransientSurface);
|
||||
|
||||
RebuildDefaultAnimations();
|
||||
|
||||
// Pin the scale center to the surface's center so the "Pop" transition
|
||||
// grows/shrinks from the middle, not the top-left corner. An expression
|
||||
// animation bound to the visual's own size keeps the center correct from
|
||||
// the very first frame (a SizeChanged handler would race the show
|
||||
// animation and let the first pop scale from 0,0).
|
||||
PinScaleCenter();
|
||||
|
||||
// Start hidden so the first Show() animates in from the configured pose.
|
||||
Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised after <see cref="Hide"/> once the longest animation in
|
||||
/// <see cref="HideAnimations"/> (delay + duration) has completed.
|
||||
/// </summary>
|
||||
public event EventHandler? HideCompleted;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transition played when the surface is shown without an
|
||||
/// explicit one (see <see cref="Show()"/>). Defaults to
|
||||
/// <see cref="Transition.None"/>, which plays no animation at all (the
|
||||
/// surface appears instantly); a directional value adds a fade plus a slide
|
||||
/// in from that edge, and <see cref="Transition.Pop"/> a fade plus a subtle
|
||||
/// scale-up. Changing this regenerates the default <see cref="ShowAnimations"/>
|
||||
/// unless it has been set explicitly.
|
||||
/// </summary>
|
||||
public Transition ShowTransition
|
||||
{
|
||||
get => (Transition)GetValue(ShowTransitionProperty);
|
||||
set => SetValue(ShowTransitionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transition played when the surface is hidden (see
|
||||
/// <see cref="Hide"/>). Defaults to <see cref="Transition.None"/>, which
|
||||
/// plays no animation at all (the surface disappears instantly); a
|
||||
/// directional value adds a fade plus a slide out toward that edge, and
|
||||
/// <see cref="Transition.Pop"/> a fade plus a subtle scale-down. Changing
|
||||
/// this regenerates the default <see cref="HideAnimations"/> unless it has
|
||||
/// been set explicitly.
|
||||
/// </summary>
|
||||
public Transition HideTransition
|
||||
{
|
||||
get => (Transition)GetValue(HideTransitionProperty);
|
||||
set => SetValue(HideTransitionProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desktop acrylic material variant painted behind the
|
||||
/// surface. Defaults to <see cref="DesktopAcrylicKind.Thin"/> (a lighter,
|
||||
/// more translucent material); set <see cref="DesktopAcrylicKind.Default"/>
|
||||
/// for the standard, more opaque acrylic or <see cref="DesktopAcrylicKind.Base"/>
|
||||
/// for the base material. Has no effect when a custom template without the
|
||||
/// default acrylic backdrop is applied.
|
||||
/// </summary>
|
||||
public DesktopAcrylicKind AcrylicKind
|
||||
{
|
||||
get => (DesktopAcrylicKind)GetValue(AcrylicKindProperty);
|
||||
set => SetValue(AcrylicKindProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played when <see cref="Show()"/> flips the
|
||||
/// surface to <see cref="Visibility.Visible"/>. Defaults to the animation
|
||||
/// derived from <see cref="ShowTransition"/>. Assigning a value marks the set
|
||||
/// as custom so <see cref="ShowTransition"/> no longer overwrites it.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet ShowAnimations
|
||||
{
|
||||
get => _showAnimations;
|
||||
set
|
||||
{
|
||||
_showAnimations = value ?? new ImplicitAnimationSet();
|
||||
_hasCustomShowAnimations = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the animations played when <see cref="Hide"/> flips the
|
||||
/// surface to <see cref="Visibility.Collapsed"/>. Defaults to the animation
|
||||
/// derived from <see cref="HideTransition"/>. Assigning a value marks the set
|
||||
/// as custom so <see cref="HideTransition"/> no longer overwrites it.
|
||||
/// </summary>
|
||||
public ImplicitAnimationSet HideAnimations
|
||||
{
|
||||
get => _hideAnimations;
|
||||
set
|
||||
{
|
||||
_hideAnimations = value ?? new ImplicitAnimationSet();
|
||||
_hasCustomHideAnimations = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wires this surface to a hosting <see cref="TransparentWindow"/> so it
|
||||
/// animates itself in and out in response to the window's
|
||||
/// <see cref="TransparentWindow.Showing"/> / <see cref="TransparentWindow.Hiding"/>
|
||||
/// events. Call this once after the surface has been set as (or placed within)
|
||||
/// the window's content.
|
||||
/// </summary>
|
||||
/// <param name="host">The window whose show/hide transitions drive this surface.</param>
|
||||
public void SubscribeTo(TransparentWindow host)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(host);
|
||||
|
||||
host.Showing += OnHostShowing;
|
||||
host.Hiding += OnHostHiding;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the surface to its hidden pose and flips it to
|
||||
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays,
|
||||
/// using <paramref name="transition"/> as the show transition.
|
||||
/// </summary>
|
||||
/// <param name="transition">The transition to play when showing.</param>
|
||||
public void Show(Transition transition)
|
||||
{
|
||||
ShowTransition = transition;
|
||||
Show();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the surface to its hidden pose and flips it to
|
||||
/// <see cref="Visibility.Visible"/> so <see cref="ShowAnimations"/> plays.
|
||||
/// Repeated calls re-trigger the show animation cleanly and cancel any
|
||||
/// pending <see cref="HideCompleted"/> notification.
|
||||
/// </summary>
|
||||
public void Show()
|
||||
{
|
||||
_hideCompletedTimer.Stop();
|
||||
|
||||
// If a hide from a previous cycle is still in flight, abandon it: drop its
|
||||
// pending HideCompleted handler so the outstanding deferral is never
|
||||
// completed. We are showing again, so the host must keep the window
|
||||
// visible instead of later hiding it for this interrupted cycle.
|
||||
_abandonPendingHide?.Invoke();
|
||||
_abandonPendingHide = null;
|
||||
|
||||
// Attach the show animation and detach any hide animation: when Show() is
|
||||
// called while the surface is still visible, the Collapsed -> Visible
|
||||
// restart below would otherwise play the hide animation (a fade/scale out)
|
||||
// immediately before the show, producing a visible flash. The real hide
|
||||
// animation is re-attached just-in-time in Hide().
|
||||
Implicit.SetShowAnimations(this, _showAnimations);
|
||||
Implicit.SetHideAnimations(this, _noAnimations);
|
||||
|
||||
// Reset to the hidden pose so the show animation always animates from the
|
||||
// configured starting frame.
|
||||
Visibility = Visibility.Collapsed;
|
||||
Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips the surface to <see cref="Visibility.Collapsed"/> so
|
||||
/// <see cref="HideAnimations"/> plays, then raises <see cref="HideCompleted"/>
|
||||
/// once the longest animation in <see cref="HideAnimations"/> (delay +
|
||||
/// duration) has completed.
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
// Attach the hide animation just before collapsing (Show() detaches it to
|
||||
// avoid a flash when re-showing an already-visible surface).
|
||||
Implicit.SetHideAnimations(this, _hideAnimations);
|
||||
|
||||
Visibility = Visibility.Collapsed;
|
||||
|
||||
_hideCompletedTimer.Debounce(
|
||||
() => HideCompleted?.Invoke(this, EventArgs.Empty),
|
||||
interval: GetAnimationSetTotalDuration(_hideAnimations),
|
||||
immediate: false);
|
||||
}
|
||||
|
||||
private static void OnTransitionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
((TransientSurface)d).RebuildDefaultAnimations();
|
||||
}
|
||||
|
||||
private static TimeSpan GetAnimationSetTotalDuration(ImplicitAnimationSet set)
|
||||
{
|
||||
TimeSpan longest = TimeSpan.Zero;
|
||||
foreach (var animation in set)
|
||||
{
|
||||
if (animation is Animation anim)
|
||||
{
|
||||
var total = (anim.Delay ?? TimeSpan.Zero) + (anim.Duration ?? TimeSpan.Zero);
|
||||
if (total > longest)
|
||||
{
|
||||
longest = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return longest;
|
||||
}
|
||||
|
||||
private static (string? ShowFrom, string? HideTo) GetSlideOffsets(Transition transition) => transition switch
|
||||
{
|
||||
Transition.Bottom => ($"0,{SlideInOffset},{ShadowDepth}", $"0,{SlideOutOffset},{ShadowDepth}"),
|
||||
Transition.Top => ($"0,{-SlideInOffset},{ShadowDepth}", $"0,{-SlideOutOffset},{ShadowDepth}"),
|
||||
Transition.Left => ($"{-SlideInOffset},0,{ShadowDepth}", $"{-SlideOutOffset},0,{ShadowDepth}"),
|
||||
Transition.Right => ($"{SlideInOffset},0,{ShadowDepth}", $"{SlideOutOffset},0,{ShadowDepth}"),
|
||||
_ => (null, null),
|
||||
};
|
||||
|
||||
private void OnHostShowing(TransparentWindow sender, ShowingEventArgs e)
|
||||
{
|
||||
if (e.Transition is Transition transition)
|
||||
{
|
||||
Show(transition);
|
||||
}
|
||||
else
|
||||
{
|
||||
Show();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHostHiding(TransparentWindow sender, HidingEventArgs e)
|
||||
{
|
||||
// Take a deferral so the host keeps its window visible until our
|
||||
// out-animation has finished, then complete it from HideCompleted.
|
||||
var deferral = e.GetDeferral();
|
||||
|
||||
void OnHideCompleted(object? s, EventArgs args)
|
||||
{
|
||||
HideCompleted -= OnHideCompleted;
|
||||
_abandonPendingHide = null;
|
||||
deferral.Complete();
|
||||
}
|
||||
|
||||
// Let a subsequent Show() cancel this hide cleanly: unsubscribe the
|
||||
// handler so the deferral is never completed (the window stays visible)
|
||||
// rather than firing AppWindow.Hide for an interrupted cycle.
|
||||
_abandonPendingHide = () => HideCompleted -= OnHideCompleted;
|
||||
|
||||
HideCompleted += OnHideCompleted;
|
||||
Hide();
|
||||
}
|
||||
|
||||
private void RebuildDefaultAnimations()
|
||||
{
|
||||
if (!_hasCustomShowAnimations)
|
||||
{
|
||||
_showAnimations = BuildShowAnimations(ShowTransition);
|
||||
}
|
||||
|
||||
if (!_hasCustomHideAnimations)
|
||||
{
|
||||
_hideAnimations = BuildHideAnimations(HideTransition);
|
||||
}
|
||||
}
|
||||
|
||||
private void PinScaleCenter()
|
||||
{
|
||||
var visual = ElementCompositionPreview.GetElementVisual(this);
|
||||
var center = visual.Compositor.CreateExpressionAnimation("Vector3(this.Target.Size.X * 0.5, this.Target.Size.Y * 0.5, 0)");
|
||||
visual.StartAnimation("CenterPoint", center);
|
||||
}
|
||||
|
||||
private static ImplicitAnimationSet BuildShowAnimations(Transition transition)
|
||||
{
|
||||
var animations = new ImplicitAnimationSet();
|
||||
|
||||
if (transition == Transition.None)
|
||||
{
|
||||
// No animation at all.
|
||||
return animations;
|
||||
}
|
||||
|
||||
if (transition == Transition.Pop)
|
||||
{
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(PopFadeShowMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new ScaleAnimation
|
||||
{
|
||||
From = $"{PopScaleFrom},{PopScaleFrom},1",
|
||||
To = "1,1,1",
|
||||
Duration = TimeSpan.FromMilliseconds(PopScaleShowMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
var (slideFrom, _) = GetSlideOffsets(transition);
|
||||
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 0,
|
||||
To = 1.0,
|
||||
Duration = TimeSpan.FromMilliseconds(200),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new TranslationAnimation
|
||||
{
|
||||
From = slideFrom,
|
||||
To = $"0,0,{ShadowDepth}",
|
||||
Duration = TimeSpan.FromMilliseconds(250),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseOut,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
private static ImplicitAnimationSet BuildHideAnimations(Transition transition)
|
||||
{
|
||||
var animations = new ImplicitAnimationSet();
|
||||
|
||||
if (transition == Transition.None)
|
||||
{
|
||||
// No animation at all.
|
||||
return animations;
|
||||
}
|
||||
|
||||
if (transition == Transition.Pop)
|
||||
{
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromMilliseconds(PopFadeHideMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new ScaleAnimation
|
||||
{
|
||||
From = "1,1,1",
|
||||
To = $"{PopScaleFrom},{PopScaleFrom},1",
|
||||
Duration = TimeSpan.FromMilliseconds(PopScaleHideMs),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
|
||||
var (_, slideTo) = GetSlideOffsets(transition);
|
||||
|
||||
animations.Add(new OpacityAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0,
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
animations.Add(new TranslationAnimation
|
||||
{
|
||||
From = $"0,0,{ShadowDepth}",
|
||||
To = slideTo,
|
||||
Duration = TimeSpan.FromMilliseconds(180),
|
||||
EasingMode = Microsoft.UI.Xaml.Media.Animation.EasingMode.EaseIn,
|
||||
EasingType = EasingType.Cubic,
|
||||
});
|
||||
|
||||
return animations;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +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 Microsoft.PowerToys.Common.UI.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// A show or hide transition a surface (e.g. <see cref="TransientSurface"/>)
|
||||
/// plays when it is shown or hidden. The directional values describe an edge —
|
||||
/// interpreted as <em>in from</em> that edge on show and <em>out toward</em> it
|
||||
/// on hide — while <see cref="None"/> and <see cref="Pop"/> are non-directional.
|
||||
/// </summary>
|
||||
public enum Transition
|
||||
{
|
||||
/// <summary>No animation; the surface appears and disappears instantly.</summary>
|
||||
None,
|
||||
|
||||
/// <summary>Slide from the left edge (in from on show, out toward on hide).</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Slide from the top edge (in from on show, out toward on hide).</summary>
|
||||
Top,
|
||||
|
||||
/// <summary>Slide from the right edge (in from on show, out toward on hide).</summary>
|
||||
Right,
|
||||
|
||||
/// <summary>Slide from the bottom edge (in from on show, out toward on hide).</summary>
|
||||
Bottom,
|
||||
|
||||
/// <summary>
|
||||
/// A subtle "pop": a quick fade combined with a small scale between 96% and
|
||||
/// 100% from the surface's center. Stays in place — no slide.
|
||||
/// </summary>
|
||||
Pop,
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using Microsoft.UI.Windowing;
|
||||
using Windows.Graphics;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Flyout;
|
||||
|
||||
/// <summary>
|
||||
/// Shared helper for positioning and sizing flyout-style WinUI 3 windows
|
||||
@@ -187,13 +187,16 @@ public static partial class FlyoutWindowHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move and resize <paramref name="window"/> to <paramref name="finalRect"/> (absolute
|
||||
/// screen physical-pixel coordinates) on <paramref name="targetDisplay"/>. Performs a
|
||||
/// two-step move that avoids WM_DPICHANGED double-scaling: first a 1×1 teleport into the
|
||||
/// target display (invisible at that size), then the real position+size while the window
|
||||
/// is already on that monitor. Skips the teleport when already on the target display.
|
||||
/// Two-step move that avoids WM_DPICHANGED double-scaling. First teleports a 1×1
|
||||
/// window into the target display (which may trigger an auto-rescale, but on a 1×1
|
||||
/// rect the effect is invisible). Then sets the real position+size while the window
|
||||
/// is already on the target monitor — no DPI boundary crossing, so WinUI's auto
|
||||
/// handler doesn't fire and overwrite our computed rect.
|
||||
///
|
||||
/// Skips the teleport when the window is already on the target display, since there
|
||||
/// is no boundary to cross.
|
||||
/// </summary>
|
||||
public static void MoveAndResizeOnDisplay(WindowEx window, DisplayArea targetDisplay, RectInt32 finalRect)
|
||||
private static void MoveAndResizeOnDisplay(WindowEx window, DisplayArea targetDisplay, RectInt32 finalRect)
|
||||
{
|
||||
var currentDisplay = DisplayArea.GetFromWindowId(window.AppWindow.Id, DisplayAreaFallback.Nearest);
|
||||
bool needsTeleport = currentDisplay is null || currentDisplay.DisplayId.Value != targetDisplay.DisplayId.Value;
|
||||
@@ -4,6 +4,5 @@
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/TransientSurface/TransientSurface.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Deferral = global::Windows.Foundation.Deferral;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Data for <see cref="TransparentWindow.Hiding"/>. Supports deferrals so an
|
||||
/// animated surface can keep the window visible until its out-animation has
|
||||
/// finished. If no handler takes a deferral, the window hides immediately.
|
||||
/// </summary>
|
||||
public sealed class HidingEventArgs : EventArgs
|
||||
{
|
||||
private int _outstanding;
|
||||
private bool _raised;
|
||||
private Action? _continuation;
|
||||
|
||||
/// <summary>
|
||||
/// Requests that the window stay visible until the returned deferral is
|
||||
/// completed. Call <see cref="Deferral.Complete"/> once the out-animation
|
||||
/// has finished.
|
||||
/// </summary>
|
||||
/// <returns>A deferral that must be completed to allow the window to hide.</returns>
|
||||
public Deferral GetDeferral()
|
||||
{
|
||||
Interlocked.Increment(ref _outstanding);
|
||||
return new Deferral(OnDeferralCompleted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the window after raising the event to register what should run
|
||||
/// once every outstanding deferral has completed (or immediately if none
|
||||
/// were taken).
|
||||
/// </summary>
|
||||
internal void RunWhenComplete(Action continuation)
|
||||
{
|
||||
_continuation = continuation;
|
||||
_raised = true;
|
||||
TryComplete();
|
||||
}
|
||||
|
||||
private void OnDeferralCompleted()
|
||||
{
|
||||
Interlocked.Decrement(ref _outstanding);
|
||||
TryComplete();
|
||||
}
|
||||
|
||||
private void TryComplete()
|
||||
{
|
||||
if (_raised && Volatile.Read(ref _outstanding) == 0)
|
||||
{
|
||||
var continuation = _continuation;
|
||||
_continuation = null;
|
||||
continuation?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +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;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Data for <see cref="TransparentWindow.Showing"/>. Carries the transition the
|
||||
/// content should play, or <see langword="null"/> to let the content use its own
|
||||
/// configured show transition.
|
||||
/// </summary>
|
||||
public sealed class ShowingEventArgs : EventArgs
|
||||
{
|
||||
public ShowingEventArgs(Transition? transition)
|
||||
{
|
||||
Transition = transition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transition the content should play, or <see langword="null"/> to
|
||||
/// use the content's own configured show transition.
|
||||
/// </summary>
|
||||
public Transition? Transition { get; }
|
||||
}
|
||||
@@ -1,164 +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.Runtime.InteropServices;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Windows.Foundation;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable transparent host window for transient overlays
|
||||
/// (toasts, banners, indicators) that should not steal foreground.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The constructor applies all of the boilerplate that PowerToys overlays
|
||||
/// currently hand-roll:</para>
|
||||
/// <list type="bullet">
|
||||
/// <item>Strip the native frame and caption (<c>WS_THICKFRAME</c> etc.).</item>
|
||||
/// <item>Disable the Win11 1-pixel DWM border and corner rounding.</item>
|
||||
/// <item>Mark the window as a tool window so it stays out of the taskbar and Alt-Tab.</item>
|
||||
/// <item>Extend content into the title bar and collapse the title bar.</item>
|
||||
/// <item>Apply a <see cref="TransparentTintBackdrop"/> so the HWND is fully
|
||||
/// see-through and the visible chrome can be drawn by the content.</item>
|
||||
/// </list>
|
||||
/// <para>This window is intentionally animation-agnostic: it does not own any
|
||||
/// chrome or motion. Consumers supply their own content (typically a
|
||||
/// <see cref="TransientSurface"/>) which draws the acrylic, border, corners and
|
||||
/// shadow, and animates itself. <see cref="Show()"/> and <see cref="Hide"/>
|
||||
/// coordinate <c>SW_SHOWNA</c> (no-activate) with the
|
||||
/// <see cref="Showing"/> / <see cref="Hiding"/> events: a content surface
|
||||
/// subscribes to those (e.g. via <see cref="TransientSurface.SubscribeTo"/>)
|
||||
/// and plays its in/out animation. The <see cref="Hiding"/> event supports
|
||||
/// deferrals, so the underlying
|
||||
/// <see cref="Microsoft.UI.Windowing.AppWindow.Hide"/> is delayed until the
|
||||
/// content has finished animating out. With no listener the window simply shows
|
||||
/// or hides immediately.</para>
|
||||
/// </remarks>
|
||||
public partial class TransparentWindow : WinUIEx.WindowEx
|
||||
{
|
||||
private const uint DwmwaColorNone = 0xFFFFFFFE;
|
||||
private const int DwmwaWindowCornerPreference = 33;
|
||||
private const int DwmwaBorderColor = 34;
|
||||
private const int DwmwcpDoNotRound = 1;
|
||||
|
||||
private const int GwlExStyle = -20;
|
||||
private const int WsExToolWindow = 0x00000080;
|
||||
|
||||
private const int SwShowNa = 8;
|
||||
|
||||
private readonly nint _hwnd;
|
||||
|
||||
public TransparentWindow()
|
||||
{
|
||||
AppWindow.Hide();
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
|
||||
_hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
|
||||
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
|
||||
|
||||
unsafe
|
||||
{
|
||||
uint borderColor = DwmwaColorNone;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaBorderColor, &borderColor, sizeof(uint));
|
||||
|
||||
int cornerPref = DwmwcpDoNotRound;
|
||||
_ = DwmSetWindowAttribute(_hwnd, DwmwaWindowCornerPreference, &cornerPref, sizeof(int));
|
||||
}
|
||||
|
||||
ApplyExStyleBit(WsExToolWindow, true);
|
||||
|
||||
SystemBackdrop = new TransparentTintBackdrop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised (without activation) when <see cref="Show()"/> makes the window
|
||||
/// visible. A content surface subscribes to this to play its in-animation,
|
||||
/// using <see cref="ShowingEventArgs.Transition"/>.
|
||||
/// </summary>
|
||||
public event TypedEventHandler<TransparentWindow, ShowingEventArgs>? Showing;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when <see cref="Hide"/> begins dismissing the window. A content
|
||||
/// surface subscribes to this to play its out-animation, taking a deferral
|
||||
/// (<see cref="HidingEventArgs.GetDeferral"/>) so the underlying window stays
|
||||
/// visible until the animation completes.
|
||||
/// </summary>
|
||||
public event TypedEventHandler<TransparentWindow, HidingEventArgs>? Hiding;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
|
||||
/// <see cref="Showing"/> without a transition, so subscribed content animates
|
||||
/// in using its own configured show transition.
|
||||
/// </summary>
|
||||
public void Show() => RaiseShow(null);
|
||||
|
||||
/// <summary>
|
||||
/// Shows the window without activation (<c>SW_SHOWNA</c>) and raises
|
||||
/// <see cref="Showing"/> so subscribed content animates in using
|
||||
/// <paramref name="transition"/>, overriding its configured show transition.
|
||||
/// </summary>
|
||||
/// <param name="transition">The transition the content should play.</param>
|
||||
public void Show(Transition transition) => RaiseShow(transition);
|
||||
|
||||
private void RaiseShow(Transition? transition)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
_ = ShowWindow(_hwnd, SwShowNa);
|
||||
Showing?.Invoke(this, new ShowingEventArgs(transition));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises <see cref="Hiding"/> so subscribed content animates out, then hides
|
||||
/// the underlying <see cref="Microsoft.UI.Windowing.AppWindow"/> once every
|
||||
/// deferral taken by a handler has completed (immediately if none were taken).
|
||||
/// </summary>
|
||||
public void Hide()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
var args = new HidingEventArgs();
|
||||
Hiding?.Invoke(this, args);
|
||||
args.RunWhenComplete(AppWindow.Hide);
|
||||
});
|
||||
}
|
||||
|
||||
private void ApplyExStyleBit(int bit, bool set)
|
||||
{
|
||||
if (_hwnd == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nint exStyle = GetWindowLongPtr(_hwnd, GwlExStyle);
|
||||
nint updated = set ? exStyle | bit : exStyle & ~(nint)bit;
|
||||
if (updated != exStyle)
|
||||
{
|
||||
_ = SetWindowLongPtr(_hwnd, GwlExStyle, updated);
|
||||
}
|
||||
}
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "GetWindowLongPtrW")]
|
||||
private static partial nint GetWindowLongPtr(nint hWnd, int nIndex);
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
|
||||
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool ShowWindow(nint hWnd, int nCmdShow);
|
||||
|
||||
[LibraryImport("dwmapi.dll")]
|
||||
private static unsafe partial int DwmSetWindowAttribute(nint hwnd, int dwAttribute, void* pvAttribute, int cbAttribute);
|
||||
}
|
||||
@@ -58,7 +58,7 @@ namespace Microsoft.Interop.Tests
|
||||
ClientPipe.Start();
|
||||
|
||||
// Test can be flaky as the pipes are still being set up and we end up receiving no message. Wait for a bit to avoid that.
|
||||
Thread.Sleep(500);
|
||||
Thread.Sleep(100);
|
||||
|
||||
ClientPipe.Send(testString);
|
||||
|
||||
|
||||
@@ -676,61 +676,4 @@ namespace UpdatingUnitTests
|
||||
LocalFree(argv);
|
||||
}
|
||||
};
|
||||
|
||||
// Tests for IsSafeDownloadedInstallerFilename: the updater reads
|
||||
// downloadedInstallerFilename from the persisted UpdateState.json and must only
|
||||
// accept a plain filename so it never looks outside the Updates folder when the
|
||||
// cached state is stale, corrupted, or otherwise unexpected.
|
||||
TEST_CLASS(IsSafeDownloadedInstallerFilenameTests)
|
||||
{
|
||||
public:
|
||||
// Normal installer asset names are bare filenames and must be accepted,
|
||||
// so the regular update flow does not regress.
|
||||
TEST_METHOD(NormalInstallerFilenamesAreAccepted)
|
||||
{
|
||||
Assert::IsTrue(updating::IsSafeDownloadedInstallerFilename(L"PowerToysSetup-0.95.0-x64.exe"));
|
||||
Assert::IsTrue(updating::IsSafeDownloadedInstallerFilename(L"PowerToysUserSetup-0.95.0-arm64.exe"));
|
||||
Assert::IsTrue(updating::IsSafeDownloadedInstallerFilename(L"PowerToysSetup-1.0.0-x64.msi"));
|
||||
}
|
||||
|
||||
// Empty values must be rejected (no installer to run).
|
||||
TEST_METHOD(EmptyFilenameIsRejected)
|
||||
{
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L""));
|
||||
}
|
||||
|
||||
// Relative parent-directory components must be rejected.
|
||||
TEST_METHOD(ParentDirectoryComponentsAreRejected)
|
||||
{
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"..\\..\\setup.msi"));
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"../../setup.exe"));
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L".."));
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"."));
|
||||
}
|
||||
|
||||
// Any directory component (even without "..") must be rejected — the value
|
||||
// must be a single bare filename.
|
||||
TEST_METHOD(NestedPathComponentsAreRejected)
|
||||
{
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"sub\\setup.exe"));
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"sub/setup.exe"));
|
||||
}
|
||||
|
||||
// Absolute paths, drive-relative paths and UNC paths must be rejected,
|
||||
// because fs::path's operator/ would let them replace the Updates directory.
|
||||
TEST_METHOD(AbsoluteAndUncPathsAreRejected)
|
||||
{
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"C:\\setup.msi"));
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"C:setup.msi"));
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"\\setup.msi"));
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"\\\\server\\share\\setup.exe"));
|
||||
}
|
||||
|
||||
// A bare filename that merely contains a ".." substring is rejected as a
|
||||
// conservative measure (real asset names never contain "..").
|
||||
TEST_METHOD(EmbeddedDotDotSubstringIsRejected)
|
||||
{
|
||||
Assert::IsFalse(updating::IsSafeDownloadedInstallerFilename(L"setup..name.exe"));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -44,43 +44,4 @@ namespace updating
|
||||
// args[0]=exe, args[1]=action, args[2]=installer, args[3]=installDir
|
||||
return argCount >= 4;
|
||||
}
|
||||
|
||||
// Returns true when the value read from UpdateState.json is a plain installer
|
||||
// filename that can be combined with the pending-updates directory. UpdateState.json
|
||||
// is persisted state that may be stale, corrupted, or otherwise unexpected, so the
|
||||
// cached filename could contain path separators or an absolute/drive-relative path.
|
||||
// Only a single bare filename (the form produced by the download step) is accepted;
|
||||
// anything else is rejected so the updater never looks outside the Updates folder.
|
||||
inline bool IsSafeDownloadedInstallerFilename(const std::wstring& filename)
|
||||
{
|
||||
if (filename.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject any path separators or parent-directory tokens outright. Installer
|
||||
// asset filenames never contain these.
|
||||
if (filename.find(L'/') != std::wstring::npos ||
|
||||
filename.find(L'\\') != std::wstring::npos ||
|
||||
filename.find(L"..") != std::wstring::npos)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const fs::path candidate{ filename };
|
||||
|
||||
// Must be a single path component: no drive/root and no directory portion.
|
||||
if (candidate.has_root_name() || candidate.has_root_directory() || candidate.has_parent_path())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto name = candidate.filename().wstring();
|
||||
if (name != filename || name == L"." || name == L"..")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ namespace powertoys_gpo
|
||||
}
|
||||
}
|
||||
|
||||
// No list exists for machine and user, or no value was found in the list, or an error occurred while reading the value.
|
||||
// No list exists for machine and user, or no value was found in the list, or an error ocurred while reading the value.
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,6 @@ internal sealed class IntegrationTestUserSettings : IUserSettings
|
||||
|
||||
public bool ShowCustomPreview => false;
|
||||
|
||||
public bool ShowAIPaste => true;
|
||||
|
||||
public bool CloseAfterLosingFocus => false;
|
||||
|
||||
public bool EnableClipboardPreview => true;
|
||||
|
||||
@@ -251,8 +251,7 @@
|
||||
Margin="20,0,20,0"
|
||||
x:FieldModifier="public"
|
||||
IsEnabled="{x:Bind ViewModel.IsCustomAIServiceEnabled, Mode=OneWay}"
|
||||
TabIndex="0"
|
||||
Visibility="{x:Bind ViewModel.ShowAIPasteSection, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}">
|
||||
TabIndex="0">
|
||||
<controls:PromptBox.Footer>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
|
||||
@@ -17,8 +17,6 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool ShowCustomPreview { get; }
|
||||
|
||||
public bool ShowAIPaste { get; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
|
||||
public bool EnableClipboardPreview { get; }
|
||||
|
||||
@@ -38,8 +38,6 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool ShowCustomPreview { get; private set; }
|
||||
|
||||
public bool ShowAIPaste { get; private set; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; private set; }
|
||||
|
||||
public bool EnableClipboardPreview { get; private set; }
|
||||
@@ -56,7 +54,6 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
IsAIEnabled = false;
|
||||
ShowCustomPreview = true;
|
||||
ShowAIPaste = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
EnableClipboardPreview = true;
|
||||
PasteAIConfiguration = new PasteAIConfiguration();
|
||||
@@ -112,7 +109,6 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
IsAIEnabled = properties.IsAIEnabled;
|
||||
ShowCustomPreview = properties.ShowCustomPreview;
|
||||
ShowAIPaste = properties.ShowAIPaste;
|
||||
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
|
||||
EnableClipboardPreview = properties.EnableClipboardPreview;
|
||||
PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration();
|
||||
|
||||
@@ -234,8 +234,6 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
public bool ShowClipboardHistoryButton => ClipboardHistoryEnabled;
|
||||
|
||||
public bool ShowAIPasteSection => _userSettings.ShowAIPaste && IsAllowedByGPO;
|
||||
|
||||
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
|
||||
|
||||
private PasteFormats CustomAIFormat =>
|
||||
@@ -322,7 +320,6 @@ namespace AdvancedPaste.ViewModels
|
||||
OnPropertyChanged(nameof(AIProviders));
|
||||
OnPropertyChanged(nameof(AllowedAIProviders));
|
||||
OnPropertyChanged(nameof(ShowClipboardPreview));
|
||||
OnPropertyChanged(nameof(ShowAIPasteSection));
|
||||
|
||||
NotifyActiveProviderChanged();
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"properties":{"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"ShowAIPaste":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}},"paste-ai-configuration":{"active-provider-id":"","providers":[],"use-shared-credentials":true}},"name":"AdvancedPaste","version":"1"}
|
||||
{"properties":{"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}},"paste-ai-configuration":{"active-provider-id":"","providers":[],"use-shared-credentials":true}},"name":"AdvancedPaste","version":"1"}
|
||||
@@ -29,7 +29,7 @@
|
||||
* Paste as custom format using AI
|
||||
- [] Open Settings, navigate to Enable Paste with AI and set OpenAI key.
|
||||
- [] Copy some text to clipboard. Any text.
|
||||
- [] Open Advanced Paste window using hotkey, and confirm that Custom input text box is now enabled. Write "Insert smiley after every word" and press Enter. Observe that result preview shows copied text with smileys between words. Press Enter to paste the result and observe that it is pasted.
|
||||
- [] Open Advanced Paste window using hotkey, and confirm that Custom intput text box is now enabled. Write "Insert smiley after every word" and press Enter. Observe that result preview shows coppied text with smileys between words. Press Enter to paste the result and observe that it is pasted.
|
||||
- [] Open Advanced Paste window using hotkey. Input some query (any, feel free to play around) and press Enter. When result is shown, click regenerate button, to see if new result is generated. Select one of the results and paste. Observe that correct result is pasted.
|
||||
- [] Create few custom actions. Set up hotkey for custom actions and confirm they work. Enable/disable custom actions and confirm that the change is reflected in Advanced Paste UI - custom action is not listed. Try different ctrl + <num> in-app shortcuts for custom actions. Try moving custom actions up/down and confirm that the change is reflected in Advanced Paste UI.
|
||||
- [] Open Settings and disable Custom format preview. Open Advanced Paste window with hotkey, enter some query and press enter. Observe that result is now pasted right away, without showing the preview first.
|
||||
|
||||
@@ -144,19 +144,17 @@ namespace EnvironmentVariablesUILib.Helpers
|
||||
set.Variables = new System.Collections.ObjectModel.ObservableCollection<Variable>(sortedList.Values);
|
||||
}
|
||||
|
||||
// Profiles override User variables only (see doc/devdocs/modules/environmentvariables.md), so
|
||||
// every registry write driven by a profile targets the current-user environment regardless of a
|
||||
// variable's ParentType. These helpers centralize that behavior for the apply/unapply/edit paths.
|
||||
internal static bool SetProfileVariableWithoutNotify(Variable variable)
|
||||
internal static bool SetVariableWithoutNotify(Variable variable)
|
||||
{
|
||||
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine: false);
|
||||
bool fromMachine = variable.ParentType switch
|
||||
{
|
||||
VariablesSetType.Profile => false,
|
||||
VariablesSetType.User => false,
|
||||
VariablesSetType.System => true,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool UnsetProfileVariableWithoutNotify(Variable variable)
|
||||
{
|
||||
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine: false);
|
||||
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -177,6 +175,21 @@ namespace EnvironmentVariablesUILib.Helpers
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool UnsetVariableWithoutNotify(Variable variable)
|
||||
{
|
||||
bool fromMachine = variable.ParentType switch
|
||||
{
|
||||
VariablesSetType.Profile => false,
|
||||
VariablesSetType.User => false,
|
||||
VariablesSetType.System => true,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool UnsetVariable(Variable variable)
|
||||
{
|
||||
bool fromMachine = variable.ParentType switch
|
||||
|
||||
@@ -35,6 +35,8 @@ namespace EnvironmentVariablesUILib.Models
|
||||
{
|
||||
foreach (var variable in Variables)
|
||||
{
|
||||
var applyToSystem = variable.ApplyToSystem;
|
||||
|
||||
// Get existing variable with the same name if it exist
|
||||
var variableToOverride = EnvironmentVariablesHelper.GetExisting(variable.Name);
|
||||
|
||||
@@ -44,13 +46,13 @@ namespace EnvironmentVariablesUILib.Models
|
||||
variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, this.Name);
|
||||
|
||||
// Backup the variable
|
||||
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variableToOverride))
|
||||
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride))
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to set backup variable.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variable))
|
||||
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variable))
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to set profile variable.");
|
||||
}
|
||||
@@ -76,7 +78,7 @@ namespace EnvironmentVariablesUILib.Models
|
||||
public void UnapplyVariable(Variable variable)
|
||||
{
|
||||
// Unset the variable
|
||||
if (!EnvironmentVariablesHelper.UnsetProfileVariableWithoutNotify(variable))
|
||||
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(variable))
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to unset variable.");
|
||||
}
|
||||
@@ -91,12 +93,12 @@ namespace EnvironmentVariablesUILib.Models
|
||||
{
|
||||
var variableToRestore = new Variable(originalName, backupVariable.Values, backupVariable.ParentType);
|
||||
|
||||
if (!EnvironmentVariablesHelper.UnsetProfileVariableWithoutNotify(backupVariable))
|
||||
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable))
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to unset backup variable.");
|
||||
}
|
||||
|
||||
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variableToRestore))
|
||||
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore))
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to restore backup variable.");
|
||||
}
|
||||
|
||||
@@ -143,12 +143,12 @@ namespace EnvironmentVariablesUILib.Models
|
||||
{
|
||||
var variableToRestore = new Variable(clone.Name, backupVariable.Values, backupVariable.ParentType);
|
||||
|
||||
if (!EnvironmentVariablesHelper.UnsetProfileVariableWithoutNotify(backupVariable))
|
||||
if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable))
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to unset backup variable.");
|
||||
}
|
||||
|
||||
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variableToRestore))
|
||||
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore))
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to restore backup variable.");
|
||||
}
|
||||
@@ -169,7 +169,7 @@ namespace EnvironmentVariablesUILib.Models
|
||||
if (EnvironmentVariablesHelper.GetExisting(variableToOverride.Name) == null)
|
||||
{
|
||||
// Backup the variable
|
||||
if (!EnvironmentVariablesHelper.SetProfileVariableWithoutNotify(variableToOverride))
|
||||
if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride))
|
||||
{
|
||||
LoggerInstance.Logger.LogError("Failed to set backup variable.");
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 433 B |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 433 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 328 B |
@@ -6,7 +6,6 @@ using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
|
||||
@@ -21,20 +20,7 @@ namespace PowerToys.FileLocksmithUI.Converters
|
||||
|
||||
if (!string.IsNullOrEmpty(y))
|
||||
{
|
||||
try
|
||||
{
|
||||
icon = Icon.ExtractAssociatedIcon(y);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// The process image path can be non-empty but no longer exist on disk
|
||||
// (e.g. self-updating software that deletes its old versioned directory while
|
||||
// the old process is still running). ExtractAssociatedIcon then throws and,
|
||||
// because this converter runs per-row during ListView virtualization, the
|
||||
// exception would otherwise reach App_UnhandledException and fast-fail the app.
|
||||
// Fall through to the placeholder icon instead of crashing.
|
||||
Logger.LogWarning($"Couldn't extract the icon for '{y}'. {ex}");
|
||||
}
|
||||
icon = Icon.ExtractAssociatedIcon(y);
|
||||
}
|
||||
|
||||
if (icon != null)
|
||||
|
||||