Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
d6d5967b69 Use $conditionalImport variable consistently in EnableModule.ps1 Add-Content
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/95c97d85-c99b-4437-95df-c971b16491cd

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-04-18 17:44:33 +00:00
copilot-swe-agent[bot]
129f0a33a0 Fix Command Not Found crash in non-interactive PowerShell sessions (e.g. VS Code tasks)
Only import the Microsoft.WinGet.CommandNotFound module in interactive
PowerShell sessions. The module's WarmUp background task can crash with
PipelineStoppedException when PowerShell exits quickly (e.g. when running
VS Code shell tasks with -Command). Since Command Not Found is a feedback
provider only useful in interactive sessions, skip loading it when
PowerShell is invoked with -Command, -File, or -NonInteractive flags.

Also auto-upgrade existing unconditional imports when the Settings page
checks requirements.

Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/95c97d85-c99b-4437-95df-c971b16491cd

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
2026-04-18 17:43:42 +00:00
copilot-swe-agent[bot]
2cea99965b Initial plan 2026-04-18 17:32:34 +00:00
1025 changed files with 15224 additions and 80227 deletions

View File

@@ -57,7 +57,6 @@ body:
- Environment Variables
- FancyZones
- FancyZones Editor
- Grab And Move
- File Locksmith
- "File Explorer: Preview Pane"
- "File Explorer: Thumbnail preview"
@@ -70,7 +69,6 @@ body:
- Mouse Without Borders
- New+
- Peek
- Power Display
- PowerRename
- PowerToys Run
- Quick Accent

View File

@@ -51,7 +51,6 @@ resw
resx
srt
Stereolithography
taskmgr
terabyte
UYVY
xbf
@@ -128,7 +127,6 @@ HOLDSPACE
HOLDBACKSPACE
IDIGNORE
KBDLLHOOKSTRUCT
keydowns
keyevent
LAlt
LBUTTON
@@ -187,12 +185,6 @@ xmlutil
# Prefix
pcs
# EXPRTK / C++ MATH
ifunction
isinf
isnan
# User32.SYSTEM_METRICS_INDEX.cs
CLEANBOOT
@@ -308,11 +300,8 @@ pwa
AOT
Aot
cswinrt
ify
rsp
TFM
RTIID
# YML
onefuzz
@@ -320,7 +309,6 @@ onefuzz
# NameInCode
leilzh
mengyuanchen
contoso
# DllName
testhost
@@ -340,38 +328,19 @@ MRUCMPPROC
MRUINFO
REGSTR
#Xaml
NVI
Storyboards
# Misc Win32 APIs and PInvokes
DEFAULTTONEAREST
INVOKEIDLIST
LCMAP
MEMORYSTATUSEX
ABE
Mdt
HTCAPTION
POSCHANGED
QPC
QUERYPOS
SETAUTOHIDEBAR
ULW
WINDOWPOS
WINEVENTPROC
WORKERW
FULLSCREENAPP
ACLO
CACLI
DOENVSUBST
FILESYSONLY
URLIS
WAITTIMEOUT
DEFAULTTONEAREST
DWRITE
LWIN
VCENTER
VREDRAW
# COM/WinRT interface prefixes and type fragments
BAlt
@@ -406,12 +375,6 @@ YYY
# Unicode
precomposed
# names of characters
zwsp
# mermaid
autonumber
# GitHub issue/PR commands
azp
feedbackhub
@@ -428,10 +391,3 @@ Nonpaged
# XAML
Untargeted
# Program names
SEARCHHOST
SHELLEXPERIENCEHOST
SHELLHOST
STARTMENUEXPERIENCEHOST
WIDGETBOARD

View File

@@ -209,7 +209,6 @@ Bilibili
BVID
capturevideosample
cmdow
contoso
Contoso
Controlz
cortana

View File

@@ -7,9 +7,7 @@ AUDCLNT
axisdefer
axisflip
axisstart
BGRX
bitmaps
blits
BREAKSCR
BUFFERFLAGS
Cands
@@ -19,7 +17,6 @@ CLASSW
coeffs
coprime
CREATEDIBSECTION
CREATESTRUCTW
crossfades
Ctl
CTLCOLOR
@@ -28,10 +25,7 @@ CTLCOLORDLG
CTLCOLOREDIT
CTLCOLORLISTBOX
CTrim
DBuffer
ddx
ddy
DEVSOURCE
DFCS
dlg
dlu
@@ -47,14 +41,11 @@ dupsegments
DWLP
EDITCONTROL
ENABLEHOOK
ENDOFSTREAM
expectedlock
fabsf
fastscroll
FDE
GETCHANNELRECT
GETCHECK
GETCOUNT
GETSCREENSAVEACTIVE
GETSCREENSAVETIMEOUT
GETTHUMBRECT
@@ -66,7 +57,6 @@ HTHEME
htol
ICONINFORMATION
ICONWARNING
igc
Inj
jumprecover
KSDATAFORMAT
@@ -88,11 +78,9 @@ manualdrop
maskcache
maxstep
MENUINFO
MFSTARTUP
mfxhw
mic
middledrop
MJPEG
middledrop
MMRESULT
momentumreversal
mrate
@@ -104,7 +92,6 @@ nduplicates
niterations
nmonitor
NONCLIENTMETRICS
NONOTIFY
nonvle
nredraw
nstop
@@ -115,22 +102,16 @@ osc
OWNERDRAW
PBGRA
periodictrap
pillarbox
pfdc
playhead
pointerreuse
PSWA
pwfx
qpc
Qpc
quantums
RCZOOMITSCR
readback
READERF
realcapture
REFKNOWNFOLDERID
reposted
RETURNCMD
SCREENSAVE
SCRNSAVE
SCRNSAVECONFIGURE
@@ -150,7 +131,6 @@ shortlist
slowthenfast
smallstart
SNIPOCR
sqrtf
ssi
startuprecovery
stf
@@ -159,7 +139,6 @@ STREAMFLAGS
submix
sxx
sxy
synthesising
syy
tallportal
tci
@@ -175,7 +154,6 @@ vaddvq
vandq
vcgeq
vdup
VIDCAP
vld
vle
Vle
@@ -191,9 +169,6 @@ vsync
WASAPI
WAVEFORMATEX
WAVEFORMATEXTENSIBLE
webcam
Webcam
webcams
wfopen
wideportal
wil

View File

@@ -1,3 +1,6 @@
# D2D
#D?2D
# Repeated letters
\b([a-z])\g{-1}{2,}\b
@@ -11,7 +14,7 @@
^.*\b[Cc][Ss][Pp][Ee][Ll]{2}:\s*[Dd][Ii][Ss][Aa][Bb][Ll][Ee]-[Ll][Ii][Nn][Ee]\b
# copyright
Copyright (?:\([Cc]\)|©|)(?:[-\d, ]|and)+(?: [A-Z][a-z]+ [A-Z][a-z]+,?)+
Copyright (?:\([Cc]\)|)(?:[-\d, ]|and)+(?: [A-Z][a-z]+ [A-Z][a-z]+,?)+
# patch hunk comments
^@@ -\d+(?:,\d+|) \+\d+(?:,\d+|) @@ .*
@@ -19,10 +22,10 @@ Copyright (?:\([Cc]\)|©|)(?:[-\d, ]|and)+(?: [A-Z][a-z]+ [A-Z][a-z]+,?)+
index (?:[0-9a-z]{7,40},|)[0-9a-z]{7,40}\.\.[0-9a-z]{7,40}
# file permissions
(?:^|['"`\s])(?!-+\s)[-bcdLlpsw](?:[-r][-w][-Ssx]){2}[-r][-w][-SsTtx]\+?['"`\s]
['"`\s][-bcdLlpsw](?:[-r][-w][-Ssx]){2}[-r][-w][-SsTtx]\+?['"`\s]
# css fonts
\bfont(?:-family(?:[-\w+]*)|):[^;}]+
\bfont(?:-family|):[^;}]+
# css url wrappings
\burl\([^)]+\)
@@ -87,9 +90,6 @@ arn:aws:[-/:\w]+
# AWS VPC
vpc-\w+
# Azure AD
\baad\.\w{48}\b
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
# YouTube url
\b(?:(?:www\.|)youtube\.com|youtu.be)/(?:channel/|embed/|user/|playlist\?list=|watch\?v=|v/|)[-a-zA-Z0-9?&=_%]*
@@ -171,7 +171,7 @@ themes\.googleusercontent\.com/static/fonts/[^/\s"]+/v\d+/[^.]+.
GHSA(?:-[0-9a-z]{4}){3}
# GitHub actions
\buses:\s+(['"]?)[-\w.]+/[-\w./]+@[-\w.]+\g{-1}
\buses:\s+[-\w.]+/[-\w./]+@[-\w.]+
# GitLab commit
\bgitlab\.[^/\s"]*/\S+/\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\b
@@ -240,7 +240,7 @@ accounts\.binance\.com/[a-z/]*oauth/authorize\?[-0-9a-zA-Z&%]*
\bmedium\.com/@?[^/\s"]+/[-\w]+
# microsoft
\b(?:https?://|)(?:(?:(?:blogs|download\.visualstudio|docs|msdn2?|research)\.|)microsoft|blogs\.msdn)\.co(?:m|\.\w\w)/[-_a-zA-Z0-9()=./%?#]*
\b(?:https?://|)(?:(?:(?:blogs|download\.visualstudio|docs|msdn2?|research)\.|)microsoft|blogs\.msdn)\.co(?:m|\.\w\w)/[-_a-zA-Z0-9()=./%]*
# powerbi
\bapp\.powerbi\.com/reportEmbed/[^"' ]*
# vs devops
@@ -414,7 +414,7 @@ ipfs://[0-9a-zA-Z]{3,}
\bgetopts\s+(?:"[^"]+"|'[^']+')
# ANSI color codes
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[(?:\d+(?:;\d+)*|)m
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+)*m
# URL escaped characters
%[0-9A-F][A-F](?=[A-Za-z])
@@ -431,7 +431,7 @@ sha\d+:[0-9a-f]*?[a-f]{3,}[0-9a-f]*
# sha-... -- uses a fancy capture
(\\?['"]|&quot;)[0-9a-f]{40,}\g{-1}
# hex runs
\b(?=(?:[a-fA-F]{0,2}\d)*[a-fA-F]{3})[0-9a-fA-F]{16,}\b
\b[0-9a-fA-F]{16,}\b
# hex in url queries
=[0-9a-fA-F]*?(?:[A-F]{3,}|[a-f]{3,})[0-9a-fA-F]*?&
# ssh
@@ -455,11 +455,7 @@ LS0tLS1CRUdJT.*
# uuid:
\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\b
# unicode escaped characters (4)
\\u[0-9a-fA-F]{4}
# hex digits including css/html color classes
# hex digits including css/html color classes:
(?:[\\0][xX]|\\u\{?|[uU]\+|#x?|%23|&H)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|[iu]\d+)\b
# integrity
@@ -482,7 +478,7 @@ Name\[[^\]]+\]=.*
(?:(?:\b|_|(?<=[a-z]))I|(?:\b|_)(?:nsI|isA))(?=(?:[A-Z][a-z]{2,})+(?:[A-Z\d]|\b))
# python
#\b(?i)py(?!gment|gmy|lon|ramid|ro|th)(?=[a-z]{2,})
#\b(?i)py(?!gments|gmy|lon|ramid|ro|th)(?=[a-z]{2,})
# crypt
(['"])\$2[ayb]\$.{56}\g{-1}
@@ -502,21 +498,12 @@ Name\[[^\]]+\]=.*
# go.sum
\bh1:\S+
# golang print-f-style functions
#(?i)(?<=append|comma|debug|equal|err|error|exit|fatal|format|info|log|name|panic|print|skip|scan|string|trace|true|warn|warning|wrap|write)(?:f|ln)(?:[ (]|$)
# golang regular expression
(?<!")\br".+?"
# imports
^import\s+(?:(?:static|type)\s+|)(?:[\w.]|\{\s*\w*?(?:,\s*(?:\w*|\*))+\s*\})+(?:\s+from (['"]).*?\g{-1}|)
^import\s+(?:(?:static|type)\s+|)(?:[\w.]|\{\s*\w*?(?:,\s*(?:\w*|\*))+\s*\})+
# scala modules
#("[^"]+"\s*%%?\s*){2,3}"[^"]+"
# Dataframes / NumPy
#\b(?:df|np)\.\w{3,}
# container images
image: [-\w./:@]+
@@ -546,18 +533,12 @@ content: (['"])[-a-zA-Z=;:/0-9+]*=\g{-1}
# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings
(?<!['"])\b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)['"](?=[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})
# Regular expression for word breaks
#\\b(?=[a-z]{2})
# Regular expressions for (P|p)assword
\([A-Z]\|[a-z]\)[a-z]+
# Java regular expressions
Pattern\.(?:compile|matches)\(".*"
# JavaScript regular expressions
# javascript exec/test regex
/.{3,}?/[gim]*\.(?:exec|test)\(
# javascript test regex
/.{3,}/[gim]*\.test\(
# javascript match regex
\.match\(/[^/\s"]{3,}/[gim]*\s*
# javascript match regex
@@ -584,7 +565,7 @@ perl(?:\s+-[a-zA-Z]\w*)+
regexp?\.MustCompile\((?:`[^`]*`|".*"|'.*')\)
# regex choice
#\((?:\?:|)[^)|]+(?<! )\|(?!(?:jq|xargs)\b)[^)| ][^)]*\)
# \(\?:[^)]+\|[^)]+\)
# proto
^\s*(\w+)\s\g{-1} =
@@ -607,9 +588,6 @@ urn:shemas-jetbrains-com
# Debian changelog severity
[-\w]+ \(.*\) (?:\w+|baseline|unstable|experimental); urgency=(?:low|medium|high|emergency|critical)\b
# Red Hat Package management spec file dependencies
^(?:Build|)Requires: [-.\w]+
# kubernetes pod status lists
# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase
\w+(?:-\w+)+\s+\d+/\d+\s+(?:Running|Pending|Succeeded|Failed|Unknown)\s+
@@ -664,8 +642,6 @@ PrependWithABINamepsace
>[-a-zA-Z=;:/0-9+]{3,}=</
# base64 encoded content, possibly wrapped in mime
#(?:^|[\s=;:?])[-a-zA-Z=;:/0-9+]{50,}(?:[\s=;:?]|$)
# jwt
(?:\be[wy][-a-zA-Z=;:/0-9+]+\.){2}[-_\w]+
# base64 encoded json
\beyJ[-a-zA-Z=;:/0-9+]+
# base64 encoded pkcs
@@ -703,9 +679,9 @@ systemd.*?running in system mode \([-+].*\)$
# Non-English
# Even repositories expecting pure English content can unintentionally have Non-English content... People will occasionally mistakenly enter [homoglyphs](https://en.wikipedia.org/wiki/Homoglyph) which are essentially typos, and using this pattern will mean check-spelling will not complain about them.
# .
#
# If the content to be checked should be written in English and the only Non-English items will be people's names, then you can consider adding this.
# .
#
# Alternatively, if you're using check-spelling v0.0.25+, and you would like to _check_ the Non-English content for spelling errors, you can. For information on how to do so, see:
# https://docs.check-spelling.dev/Feature:-Configurable-word-characters.html#unicode
[a-zA-Z]*[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*|[a-zA-Z]{3,}[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]|[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3,}
@@ -717,7 +693,7 @@ systemd.*?running in system mode \([-+].*\)$
# This corpus only had capital letters, but you probably want lowercase ones as well.
\b[LN]'+[a-z]{2,}\b
# LaTeX
# latex (check-spelling >= 0.0.22)
\\\w{2,}\{
# American Mathematical Society (AMS) / Doxygen
@@ -744,6 +720,7 @@ nolint:\s*[\w,]+
# cygwin paths
/cygdrive/[a-zA-Z]/(?:Program Files(?: \(.*?\)| ?)(?:/[-+.~\\/()\w ]+)*|[-+.~\\/()\w])+
# in check-spelling@v0.0.22+, printf markers aren't automatically consumed
# printf markers
#(?<!\\)\\[nrt](?=[a-z]{2,})
# alternate printf markers if you run into latex and friends
@@ -772,12 +749,12 @@ W/"[^"]+"
# Compiler flags (Unix, Java/Scala)
# Use if you have things like `-Pdocker` and want to treat them as `docker`
#(?:^|[\t ,>"'`=\[(#])-(?:(?:J-|)[DPWXY]|[Llf])(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
#(?:^|[\t ,>"'`=(#])-(?:(?:J-|)[DPWXY]|[Llf])(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})
# Compiler flags (Windows / PowerShell)
# This is a subset of the more general compiler flags pattern.
# It avoids matching `-Path` to prevent it from being treated as `ath`
#(?:^|[\t ,"'`=\[(#])-(?:[DPL](?=[A-Z]{2,})|[WXYlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}))
#(?:^|[\t ,"'`=(#])-(?:[DPL](?=[A-Z]{2,})|[WXYlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}))
# Compiler flags (linker)
,-B
@@ -785,7 +762,7 @@ W/"[^"]+"
# Library prefix
# e.g., `lib`+`archive`, `lib`+`raw`, `lib`+`unwind`
# (ignores some words that happen to start with `lib`)
(?:\b|_)[Ll]ib(?!era[lt])(?:re(?=office)|era|)(?!ero|erty|rar(?:i(?:an|es)|y))(?=[a-z])
(?:\b|_)[Ll]ib(?:re(?=office)|)(?!era[lt]|ero|erty|rar(?:i(?:an|es)|y))(?=[a-z])
# iSCSI iqn (approximate regex)
\biqn\.[0-9]{4}-[0-9]{2}(?:[\.-][a-z][a-z0-9]*)*\b
@@ -796,9 +773,9 @@ W/"[^"]+"
# curl arguments
\b(?:\\n|)curl(?:\.exe|)(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)*
# set arguments
\b(?:bash|(?<!\.)sh|set)(?:\s+[-+][abefimouxE]{1,2})*\s+[-+][abefimouxE]{3,}(?:\s+[-+][abefimouxE]+)*
\b(?:bash|sh|set)(?:\s+[-+][abefimouxE]{1,2})*\s+[-+][abefimouxE]{3,}(?:\s+[-+][abefimouxE]+)*
# tar arguments
\b(?:\\n|)g?tar(?:\.exe|)(?:\s-C \S+|(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
# tput arguments -- https://man7.org/linux/man-pages/man5/terminfo.5.html -- technically they can be more than 5 chars long...
\btput\s+(?:(?:-[SV]|-T\s*\w+)\s+)*\w{3,5}\b
# macOS temp folders

View File

@@ -105,13 +105,13 @@
^src/common/ManagedCommon/ColorFormatHelper\.cs$
^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$
^src/common/sysinternals/Eula/
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherComparisonTests.cs$
^src/modules/cmdpal/Tests/Microsoft\.CommandPalette\.Extensions\.Toolkit\.UnitTests/FuzzyMatcherDiacriticsTests.cs$
^doc/devdocs/modules/cmdpal/initial-sdk-spec/list-elements-mock-002\.pdn$
^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$
^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$
@@ -143,4 +143,3 @@ ignore$
src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage.cs
^src/modules/powerrename/unittests/testdata/avif_test\.avif$
^src/modules/powerrename/unittests/testdata/heif_test\.heic$
^deps/spdlog-msvc-fix/

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,30 +1,10 @@
# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns
Inno Setup
FFmpeg
# https://github.com/MicrosoftEdge/edge-launcher
MIcrosoftEdgeLauncherCsharp
# x64
(?:(?<=[a-df-z])x|(?<=[A-Z]X))64
# reversed irreversible binomials
\b(?:mouse down and up|low and high)\b
# marker to ignore all code on line
^.*/\* #no-spell-check-line \*/.*$
# marker for ignoring a comment to the end of the line
// #no-spell-check.*$
# JavaScript regex literals that start with \b can be reported as "b..." words.
# Example: /\bclass\s+.../
^\s*/\\[b].{3,}?/[gim]*\s*(?:\)(?:;|$)|,$)
# GitHub API header token used in code (not natural language).
\bx-ratelimit-reset\b
# Gaelic
Gàidhlig
@@ -91,14 +71,11 @@ StringComparer.OrdinalIgnoreCase\) \{.*\}
# the last line of mimetype="application/x-microsoft.net.object.bytearray.base64" things in .resx files
^\s*[-a-zA-Z=;:/0-9+]*[-a-zA-Z;:/0-9+][-a-zA-Z=;:/0-9+]*=$
# DateTime Formats
Get-Date -Format \w+|DateTime\.Now(?::|\.ToString\(")\w+
# Automatically suggested patterns
# hit-count: 5402 file-count: 1339
# IServiceProvider / isAThing
(?:(?:\b|_|(?<=[a-z]))[A-Z]|(?:\b|_)(?:nsI|isA))(?=(?:[A-Z][a-z]{2,})+(?:[A-Z\d]|\b))
(?:(?:\b|_|(?<=[a-z]))[IT]|(?:\b|_)(?:nsI|isA))(?=(?:[A-Z][a-z]{2,})+(?:[A-Z\d]|\b))
# hit-count: 2073 file-count: 842
# #includes
@@ -182,10 +159,6 @@ aka\.ms/[a-zA-Z0-9]+
# kubernetes crd patterns
^\s*pattern: .*$
# hit-count: 7 file-count: 3
# unicode escaped characters (4)
\\u[0-9a-fA-F]{4}
# hit-count: 5 file-count: 3
# URL escaped characters
%[0-9A-F][A-F](?=[A-Za-z])
@@ -198,10 +171,6 @@ aka\.ms/[a-zA-Z0-9]+
# medium
\bmedium\.com/@?[^/\s"]+/[-\w:/*.]+
# hit-count: 2 file-count: 2
# tar arguments
\b(?:\\n|)g?tar(?:\.exe|)(?:\s-C \S+|(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
# hit-count: 2 file-count: 1
# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there
# YouTube url
@@ -215,6 +184,10 @@ aka\.ms/[a-zA-Z0-9]+
# curl arguments
\b(?:\\n|)curl(?:\.exe|)(?:\s+-[a-zA-Z]{1,2}\b)*(?:\s+-[a-zA-Z]{3,})(?:\s+-[a-zA-Z]+)*
# hit-count: 1 file-count: 1
# tar arguments
\b(?:\\n|)g?tar(?:\.exe|)(?:(?:\s+--[-a-zA-Z]+|\s+-[a-zA-Z]+|\s[ABGJMOPRSUWZacdfh-pr-xz]+\b)(?:=[^ ]*|))+
# #pragma lib
^\s*#pragma comment\(lib, ".*?"\)
@@ -237,15 +210,13 @@ RegExp\(@?([`'"]).*?\g{-1}\)|(?:escapes|regEx):\s*(?:/.*/|([`'"]).*?\g{-1})|retu
# mount
\bmount\s+-t\s+(\w+)\s+\g{-1}\b
# C types and repeated CSS values
\s(auto|await|buffalo|center|div|inherit|long|LONG|none|normal|solid|thin|transparent|very)(?:\s\g{-1})+\s
\s(auto|buffalo|center|div|inherit|long|LONG|none|normal|solid|thin|transparent|very)(?:\s\g{-1})+\s
# C enum and struct
\b(?:enum|struct)\s+(\w+)\s+\g{-1}\b
# go templates
\s(\w+)\s+\g{-1}\s+\`(?:graphql|inject|json|yaml):
# doxygen / javadoc / .net
(?:[\\@](?:brief|defgroup|groupname|link|t?param|return|retval)|(?:public|private|\[Parameter(?:\(.+\)|)\])(?:\s+(?:static|override|readonly|required|virtual))*)(?:\s+\{\w+\}|)\s+(\w+)\s+\g{-1}\s
# C# getter/setter
\s(\w+)\s+\g{-1}\s*\{\s*[gs]et;
# macOS file path
(?:Contents\W+|(?!iOS)/)MacOS\b

View File

@@ -1,30 +1,23 @@
attache
aroynt.*
bellows?
^attache$
^bellows?$
benefitting
occurences?
.*dnt
dependan.*
developement
developp?e
Devers?
devex.*
devide
Devinn?[ae]
devisals?
devisors?
diables?
hasta?
hastat.*
immediatly
inisle
inital
linge
oer
^dependan.*
^develope$
^developement$
^developpe
^Devers?$
^devex
^devide
^Devinn?[ae]
^devisal
^devisor
^diables?$
^oer$
Sorce
[Ss]pae.*
Teh
untill
untilling
venders?
wether.*
^[Ss]pae.*
^Teh$
^untill$
^untilling$
^venders?$
^wether.*

View File

@@ -266,5 +266,16 @@ configuration:
- addReply:
reply: Hi! Your last comment indicates to our system, that you might want to contribute to this feature/fix this bug. Thank you! Please make us aware on our ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769), as we don't see all the comments. <br /><br />_I'm a bot (beep!) so please excuse any mistakes I may make_
description:
- if:
- payloadType: Issues
- isAction:
action: Opened
- bodyContains:
pattern: 'Area\(s\) with issue\?\s*\nWorkspaces'
isRegex: True
then:
- addLabel:
label: Product-Workspaces
description:
onFailure:
onSuccess:

View File

@@ -1,324 +0,0 @@
#!/usr/bin/env node
/**
* Detects telemetry-event additions/modifications in a pull request and
* posts (or updates) a PR comment when telemetry-related changes are found.
*
* This script is executed by .github/workflows/telemetry-pr-check.yml.
* Keep both files aligned when changing trigger behavior, env usage, or messaging.
*/
const fs = require('node:fs');
const COMMENT_MARKER = '<!-- telemetry-event-check -->';
const COMMENT_BODY_WITH_PRIVACY_UPDATE = `${COMMENT_MARKER}
THIS IS A TEST | @chatasweetie is testing this functionality
Thanks for contributing to PowerToys. This change might include a new or modified telemetry event, and we want to help make sure you can get your data end to end.
1. Reach out to Jessica (@chatasweetie) to follow up on the next steps to add these telemetry events to our pipelines.`;
const COMMENT_BODY_WITHOUT_PRIVACY_UPDATE = `${COMMENT_MARKER}
THIS IS A TEST | @chatasweetie is testing this functionality
Thanks for contributing to PowerToys. This change might include a new or modified telemetry event, and we want to help make sure you can get your data end to end.
1. Make sure to add your telemetry events to DATA_AND_PRIVACY.md.
2. Reach out to Jessica (@chatasweetie) to follow up on the next steps to add these telemetry events to our pipelines.`;
const TELEMETRY_PATH_PATTERNS = [
/(^|\/)trace\.(h|hpp|cpp|cs)$/i,
/(^|\/)telemetry\//i,
/(^|\/)events\/.+event\.cs$/i,
/^src\/common\/Telemetry\//i,
/^src\/common\/ManagedTelemetry\//i,
/^src\/runner\/trace\.(h|cpp)$/i,
/^src\/settings-ui\/.+\/Telemetry\//i,
];
const TELEMETRY_LINE_PATTERNS = [
/TraceLoggingWriteWrapper\s*\(/,
/\bTraceLoggingWrite\s*\(/,
/\bTRACELOGGING_DEFINE_PROVIDER\b/,
/\bTraceLoggingOptionProjectTelemetry\b/,
/\bProjectTelemetryPrivacyDataTag\b/,
/\bPROJECT_KEYWORD_MEASURE\b/,
/\bRegisterProvider\s*\(/,
/\bUnregisterProvider\s*\(/,
/\bPowerToysTelemetry\.Log\.WriteEvent\s*\(/,
/\bclass\s+\w+\s*:\s*EventBase\s*,\s*IEvent\b/,
/\bclass\s+\w+\s*:\s*TelemetryBase\b/,
/\bPartA_PrivTags\b/,
/\[EventData\]/,
/\bEventName\b/,
];
function requireEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required environment variable: ${name}`);
}
return value;
}
function validateRepository(repository) {
if (!/^[^/]+\/[^/]+$/.test(repository)) {
throw new Error(
`GITHUB_REPOSITORY must be in owner/repo format, received: ${JSON.stringify(repository)}`
);
}
}
function readEventPayload(eventPath) {
let raw;
try {
raw = fs.readFileSync(eventPath, 'utf8');
} catch (error) {
throw new Error(`Failed to read event payload at ${eventPath}: ${error.message}`);
}
try {
return JSON.parse(raw);
} catch (error) {
throw new Error(`Failed to parse JSON from ${eventPath}: ${error.message}`);
}
}
function resolvePullNumber(event) {
const fromPullRequest = event?.pull_request?.number;
const fromWorkflowDispatch = event?.inputs?.pr_number;
const rawPullNumber = fromPullRequest ?? fromWorkflowDispatch;
if (rawPullNumber === undefined || rawPullNumber === null || rawPullNumber === '') {
throw new Error(
'Unable to determine pull request number from event payload. Expected pull_request.number or inputs.pr_number.'
);
}
const pullNumber = Number.parseInt(String(rawPullNumber), 10);
if (!Number.isInteger(pullNumber) || pullNumber <= 0) {
throw new Error(`Invalid pull request number: ${JSON.stringify(rawPullNumber)}`);
}
return pullNumber;
}
function isTelemetryPath(filePath) {
return TELEMETRY_PATH_PATTERNS.some((pattern) => pattern.test(filePath));
}
function changedLinesFromPatch(patch) {
if (!patch) {
return [];
}
return patch
.split('\n')
.filter((line) => {
if (line.startsWith('+++') || line.startsWith('---')) {
return false;
}
return line.startsWith('+') || line.startsWith('-');
})
.map((line) => line.slice(1));
}
function hasTelemetryLineSignal(lines) {
return lines.some((line) => TELEMETRY_LINE_PATTERNS.some((pattern) => pattern.test(line)));
}
async function apiRequest(url, method = 'GET', body) {
const token = requireEnv('GITHUB_TOKEN');
let response;
try {
response = await fetch(url, {
method,
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
} catch (error) {
throw new Error(`Network error during ${method} ${url}: ${error.message}`);
}
if (!response.ok) {
const text = await response.text();
const rateLimitReset = response.headers.get('x-ratelimit-reset');
const rateLimitHint =
response.status === 403 && rateLimitReset
? ` (rate limit reset at epoch ${rateLimitReset})`
: '';
throw new Error(`${method} ${url} failed (${response.status})${rateLimitHint}: ${text}`);
}
if (response.status === 204) {
return null;
}
try {
return await response.json();
} catch (error) {
throw new Error(`Failed to parse JSON response for ${method} ${url}: ${error.message}`);
}
}
async function getAllPullFiles(apiBaseUrl, repository, pullNumber) {
const files = [];
let page = 1;
while (true) {
const url = `${apiBaseUrl}/repos/${repository}/pulls/${pullNumber}/files?per_page=100&page=${page}`;
const batch = await apiRequest(url);
if (!Array.isArray(batch)) {
throw new Error(`Unexpected response while listing PR files on page ${page}.`);
}
if (batch.length === 0) {
break;
}
files.push(...batch);
if (batch.length < 100) {
break;
}
page += 1;
}
return files;
}
async function findExistingTelemetryComment(apiBaseUrl, repository, pullNumber) {
let page = 1;
while (true) {
const commentsUrl = `${apiBaseUrl}/repos/${repository}/issues/${pullNumber}/comments?per_page=100&page=${page}`;
const comments = await apiRequest(commentsUrl);
if (!Array.isArray(comments)) {
throw new Error(`Unexpected response while listing issue comments on page ${page}.`);
}
const existing = comments.find(
(comment) => typeof comment.body === 'string' && comment.body.includes(COMMENT_MARKER)
);
if (existing) {
return existing;
}
if (comments.length < 100) {
return null;
}
page += 1;
}
}
function detectTelemetryChanges(files) {
const matches = [];
for (const file of files) {
const filename = file.filename || '';
const telemetryPath = isTelemetryPath(filename);
const changedLines = changedLinesFromPatch(file.patch);
const telemetryLineSignal = hasTelemetryLineSignal(changedLines);
// Some large diffs omit patch content. If the file path is telemetry-centric,
// treat it as a telemetry modification to avoid false negatives.
const patchUnavailable = !file.patch && telemetryPath;
if (telemetryPath || telemetryLineSignal || patchUnavailable) {
matches.push({
filename,
telemetryPath,
telemetryLineSignal,
patchUnavailable,
});
}
}
return matches;
}
function hasDataAndPrivacyChange(files) {
return files.some((file) => {
const filename = (file.filename || '').toLowerCase();
return filename === 'data_and_privacy.md';
});
}
async function upsertPrComment(apiBaseUrl, repository, pullNumber, body) {
const existing = await findExistingTelemetryComment(apiBaseUrl, repository, pullNumber);
if (existing) {
const updateUrl = `${apiBaseUrl}/repos/${repository}/issues/comments/${existing.id}`;
await apiRequest(updateUrl, 'PATCH', { body });
console.log(`Updated existing telemetry comment (id: ${existing.id}).`);
return;
}
const createUrl = `${apiBaseUrl}/repos/${repository}/issues/${pullNumber}/comments`;
await apiRequest(createUrl, 'POST', { body });
console.log('Created telemetry comment on PR.');
}
async function main() {
const eventPath = requireEnv('GITHUB_EVENT_PATH');
const repository = requireEnv('GITHUB_REPOSITORY');
const apiBaseUrl = process.env.GITHUB_API_URL || 'https://api.github.com';
validateRepository(repository);
let parsedApiBaseUrl;
try {
parsedApiBaseUrl = new URL(apiBaseUrl);
} catch {
throw new Error(`Invalid GITHUB_API_URL: ${JSON.stringify(apiBaseUrl)}`);
}
const event = readEventPayload(eventPath);
const pullNumber = resolvePullNumber(event);
console.log(`Event name: ${process.env.GITHUB_EVENT_NAME || 'unknown'}`);
console.log(`Repository: ${repository}`);
console.log(`PR number: ${pullNumber}`);
const files = await getAllPullFiles(parsedApiBaseUrl.origin, repository, pullNumber);
if (files.length === 0) {
console.log('No changed files found for PR; skipping telemetry comment update.');
return;
}
const matches = detectTelemetryChanges(files);
const dataAndPrivacyChanged = hasDataAndPrivacyChange(files);
console.log(`Scanned ${files.length} changed files.`);
console.log(`Telemetry matches found: ${matches.length}.`);
console.log(`DATA_AND_PRIVACY.md changed: ${dataAndPrivacyChanged}.`);
if (matches.length === 0) {
console.log('No telemetry-related additions/modifications detected.');
return;
}
for (const match of matches) {
console.log(
`- ${match.filename} (telemetryPath=${match.telemetryPath}, telemetryLineSignal=${match.telemetryLineSignal}, patchUnavailable=${match.patchUnavailable})`
);
}
const commentBody = dataAndPrivacyChanged
? COMMENT_BODY_WITH_PRIVACY_UPDATE
: COMMENT_BODY_WITHOUT_PRIVACY_UPDATE;
await upsertPrComment(apiBaseUrl, repository, pullNumber, commentBody);
}
main().catch((error) => {
console.error('Telemetry PR check failed.');
console.error(error instanceof Error ? error.stack || error.message : error);
process.exit(1);
});

View File

@@ -1,12 +1,12 @@
---
name: release-note-generation
description: Toolkit for generating PowerToys release notes from GitHub milestone PRs or commit ranges. Use when asked to create release notes, summarize milestone PRs, generate changelog, prepare release documentation, generate PR review summaries locally for release notes, update README for a new release, manage PR milestones, collect PRs between commits/tags, or prepare release assets (download installers and compute installer hashes).
description: Toolkit for generating PowerToys release notes from GitHub milestone PRs or commit ranges. Use when asked to create release notes, summarize milestone PRs, generate changelog, prepare release documentation, request Copilot reviews for PRs, update README for a new release, manage PR milestones, or collect PRs between commits/tags. Supports PR collection by milestone or commit range, milestone assignment, grouping by label, summarization with external contributor attribution, and README version bumping.
license: Complete terms in LICENSE.txt
---
# Release Note Generation Skill
Generate professional release notes for PowerToys milestones by collecting merged PRs, summarizing each PR with the local CLI agent, grouping by label, and producing user-facing summaries.
Generate professional release notes for PowerToys milestones by collecting merged PRs, requesting Copilot code reviews, grouping by label, and producing user-facing summaries.
## Output Directory
@@ -26,17 +26,16 @@ Generated Files/ReleaseNotes/
- Generate release notes for a milestone
- Summarize PRs merged in a release
- Generate per-PR review summaries locally for release-notes copy
- Request Copilot reviews for milestone PRs
- Assign milestones to PRs missing them
- Collect PRs between two commits/tags
- Update README.md for a new version
- Prepare GitHub release assets (download installers/symbols + compute hashes)
## Prerequisites
- **GitHub CLI (`gh`) installed and authenticated** — The collection script uses `gh pr view` and `gh api graphql` to fetch PR metadata and co-author information. Run `gh auth status` to verify; if not logged in, run `gh auth login` first. See [Step 1.0.0](./references/step1-collection.md) for details.
- MCP Server: github-mcp-server installed (used to fetch PR diffs/files for the local-agent review step)
- For [prepare-release-assets.ps1](./scripts/prepare-release-assets.ps1) only: **Azure CLI** authenticated against the Microsoft tenant (`az login`) with the `azure-devops` extension; access to the `microsoft/Dart` ADO project
- MCP Server: github-mcp-server installed
- GitHub Copilot code review enabled for the org/repo
## Required Variables
@@ -66,12 +65,12 @@ Generated Files/ReleaseNotes/
└────────────────────────────────┘
┌────────────────────────────────┐
│ 3.1 Local-agent PR summaries
│ (writes CopilotSummary) │
│ 3.1 Request Reviews (Copilot)
└────────────────────────────────┘
┌────────────────────────────────┐
│ 3.2 (Optional) Refresh PR data │
│ 3.2 Refresh PR data
│ (CopilotSummary) │
└────────────────────────────────┘
┌────────────────────────────────┐
@@ -94,7 +93,7 @@ Generated Files/ReleaseNotes/
| 1.1 | Collect PRs | From previous release tag on `stable` branch → `sorted_prs.csv` |
| 1.2 | Assign Milestones | Ensure all PRs have correct milestone |
| 2.12.4 | Label PRs | Auto-suggest + human label low-confidence |
| 3.13.3 | Reviews & Grouping | Local agent summarizes each PR diff into `CopilotSummary` → (optional refresh) → group by label |
| 3.13.3 | Reviews & Grouping | Request Copilot reviews → refresh → group by label |
| 4.14.2 | Summaries & Final | Generate grouped summaries, then consolidate |
## Detailed workflow docs
@@ -115,7 +114,6 @@ Do not read all steps at once—only read the step you are executing.
| [group-prs-by-label.ps1](./scripts/group-prs-by-label.ps1) | Group PRs into CSVs |
| [collect-or-apply-milestones.ps1](./scripts/collect-or-apply-milestones.ps1) | Assign milestones |
| [diff_prs.ps1](./scripts/diff_prs.ps1) | Incremental PR diff |
| [prepare-release-assets.ps1](./scripts/prepare-release-assets.ps1) | Download installers + symbols from an ADO build, compute SHA256, emit the "Installer Hashes" markdown table for the GitHub release page |
## References
@@ -135,6 +133,5 @@ Do not read all steps at once—only read the step you are executing.
|-------|----------|
| `gh` command not found | Install GitHub CLI and add to PATH |
| No PRs returned | Verify milestone title matches exactly |
| Empty `CopilotSummary` for many PRs | Run Step 3.1 (local-agent summaries). Do **not** use `mcp_github_request_copilot_review` from a CLI/coding agent — the GitHub API rejects bot-initiated review requests, so the column will stay empty. |
| Empty CopilotSummary | Request Copilot reviews first, then re-run dump |
| Many unlabeled PRs | Return to labeling step before grouping |
| `prepare-release-assets.ps1` fails with "Failed to acquire ADO access token" | Run `az login` and ensure you have access to the `microsoft/Dart` ADO project |

View File

@@ -1,40 +1,22 @@
# Step 3: Local Agent Reviews and Grouping
# Step 3: Copilot Reviews and Grouping
## 3.0 To-do
- 3.1 Generate PR Summaries with the Local Agent
- 3.2 (Optional) Refresh PR Data
- 3.1 Request Copilot Reviews (Agent Mode)
- 3.2 Refresh PR Data
- 3.3 Group PRs by Label
## 3.1 Generate PR Summaries with the Local Agent
## 3.1 Request Copilot Reviews (Agent Mode)
> ⚠️ **Do not use `mcp_github_request_copilot_review` (or any "request Copilot review" tool that calls the GitHub API).**
> When this skill is driven from a CLI / coding agent, the request is made from a bot identity and the GitHub API rejects it ("Bot reviewers cannot be requested"). The PR ends up with no Copilot review and `CopilotSummary` stays empty.
>
> Instead, **the local agent that is running this skill performs the review itself** and writes the summary directly into `sorted_prs.csv`.
Use MCP tools to request Copilot reviews for all PRs in `Generated Files/ReleaseNotes/sorted_prs.csv`:
For every PR listed in `Generated Files/ReleaseNotes/sorted_prs.csv` whose `CopilotSummary` is empty:
1. Fetch the PR diff using a tool that does **not** post anything back to GitHub. Any of these works:
- `mcp_github_pull_request_read` with `method: get_diff`
- `mcp_github_pull_request_read` with `method: get_files` (when the diff is large)
- `gh pr diff <PR_NUMBER> --repo microsoft/PowerToys`
2. Read the PR title, body, and diff. Produce a 13 sentence, user-facing summary in the same style as a Copilot PR review (focus on observable behavior change, not implementation details).
3. Write the summary into the `CopilotSummary` column for that PR row in `Generated Files/ReleaseNotes/sorted_prs.csv`. Preserve all other columns and the existing row order.
**Batching guidance**
- Process PRs in the order they appear in `sorted_prs.csv`.
- Generate summaries for **all** PRs in one pass before continuing to Step 3.3, so the human reviewer can validate them together.
- For very large diffs, summarize from `get_files` (filenames + per-file patches) rather than the full diff.
- Skip PRs that already have a non-empty `CopilotSummary` (e.g. PRs where a human reviewer already pasted one). Do not overwrite existing summaries.
**Why not post the summary back to the PR?** Posting a comment from the agent's identity would not be picked up by `dump-prs-since-commit.ps1` (which only matches Copilot bot authors), and it adds noise to the PR. Writing straight into the CSV keeps the artifact self-contained.
- Use `mcp_github_request_copilot_review` for each PR ID
- Do NOT generate or run scripts for this step
---
## 3.2 (Optional) Refresh PR Data
## 3.2 Refresh PR Data
Only re-run the collection script if PR metadata on GitHub has changed (new labels, retitled PRs, etc.) since Step 1.1. **Skip this step if you only want to preserve the locally generated `CopilotSummary` values from Step 3.1**, because re-running the dump will overwrite the CSV.
Re-run the collection script to capture Copilot review summaries into the `CopilotSummary` column:
```powershell
pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1 `
@@ -42,8 +24,6 @@ pwsh ./.github/skills/release-note-generation/scripts/dump-prs-since-commit.ps1
-OutputDir 'Generated Files/ReleaseNotes'
```
If you do refresh, redo Step 3.1 afterwards to repopulate `CopilotSummary`.
---
## 3.3 Group PRs by Label
@@ -55,4 +35,3 @@ pwsh ./.github/skills/release-note-generation/scripts/group-prs-by-label.ps1 -Cs
Creates `Generated Files/ReleaseNotes/grouped_csv/` with one CSV per label combination.
**Validation:** The `Unlabeled.csv` file should be minimal (ideally empty). If many PRs remain unlabeled, return to Step 2 (see [step2-labeling.md](./step2-labeling.md)).

View File

@@ -16,7 +16,7 @@ For each CSV in `Generated Files/ReleaseNotes/grouped_csv/`, create a markdown f
- Use the “Verb-ed + Scenario + Impact” sentence structure—make readers think, “Thats exactly what I need” or “Yes, thats an awesome fix.”; The "impact" can be end-user focused (written to convey user excitement) or technical (performance/stability) when user-facing impact is minimal.
- If nothing special on impact or unclear impact, mark as needing human summary
- Source from Title, Body, and CopilotSummary (prefer CopilotSummary when available)
- The `NeedThanks` column contains a comma-separated list of external contributor usernames who should be credited (empty = no attribution needed, all authors are core team). For each non-empty `NeedThanks` value, append a `by` attribution that lists **every** contributor, matching GitHub's standard contributor-attribution style: `by [@user1](https://github.com/user1)` for a single contributor, `by [@user1](https://github.com/user1) and [@user2](https://github.com/user2)` for two, or `by [@user1](https://github.com/user1), [@user2](https://github.com/user2), and [@user3](https://github.com/user3)` for three or more. In the final consolidated release notes (Step 4.2), the attribution follows the PR link, e.g. `…in [#1234](url) by [@user](url)`. Do not use "Thanks @user!" phrasing.
- The `NeedThanks` column contains a comma-separated list of external contributor usernames who should be thanked (empty = no thanks needed, all authors are core team). For each non-empty `NeedThanks` value, append thanks for **every** listed contributor: `Thanks [@user1](https://github.com/user1)!` for a single contributor, or `Thanks [@user1](https://github.com/user1) and [@user2](https://github.com/user2)!` for two, or `Thanks [@user1](https://github.com/user1), [@user2](https://github.com/user2), and [@user3](https://github.com/user3)!` for three or more.
- Do NOT include PR numbers in bullet lines
- Do NOT mention “security” or “privacy” issues, since these are not known and could be leveraged by attackers in earlier versions. Instead, describe the user-facing scenario, usage, or impact.
- If confidence < 70%, write: `Human Summary Needed: <PR full link>`

View File

@@ -1,334 +0,0 @@
<#
.SYNOPSIS
Prepares the binary assets for a PowerToys GitHub release: downloads the
four installers (per-user/per-machine x x64/arm64) and the symbol archives
from an ADO pipeline build, computes SHA256 hashes, and emits the
"Installer Hashes" markdown table.
.DESCRIPTION
Given an ADO Dart pipeline build id (e.g. from
https://microsoft.visualstudio.com/Dart/_build/results?buildId=NNN),
downloads the four installer EXEs and the per-arch symbol zips into a
single per-version folder, then writes a hashes.md alongside them with a
markdown table ready to paste into the GitHub release notes.
Requires: az login (Azure CLI authenticated), az devops extension.
.EXAMPLE
.\prepare-release-assets.ps1 -BuildId 145505247
.\prepare-release-assets.ps1 -BuildId 145505247 -OutputFolder D:\Releases
#>
param(
[Parameter(Mandatory = $true)]
[int]$BuildId,
[string]$OutputFolder = "$env:USERPROFILE\Downloads",
[string]$Organization = "https://dev.azure.com/microsoft",
[string]$Project = "Dart",
[string]$GitHubRepo = "microsoft/PowerToys"
)
$ErrorActionPreference = "Stop"
$env:AZURE_CORE_NO_PROMPT = "true"
# --- Helpers -----------------------------------------------------------------
# Invoke an `az` CLI command and capture stderr in $script:LastAzError so
# callers can surface the underlying message (expired login, blocked extension,
# tenant policy, ...) instead of swallowing it with `2>$null`.
function Invoke-Az {
$tmpErr = [System.IO.Path]::GetTempFileName()
try {
$output = & az @args 2>$tmpErr
# Get-Content -Raw returns $null for an empty file, and calling .Trim()
# on $null throws under $ErrorActionPreference = 'Stop' -- which would
# turn every successful (no-stderr) az call into a fatal error. Guard
# explicitly so $script:LastAzError is always a (possibly empty) string.
$rawErr = Get-Content $tmpErr -Raw -ErrorAction SilentlyContinue
$script:LastAzError = if ($null -eq $rawErr) { '' } else { $rawErr.Trim() }
return $output
}
finally {
Remove-Item $tmpErr -Force -ErrorAction SilentlyContinue
}
}
# Build an ADO artifact download URL from scratch instead of regex-replacing
# the URL returned by `az pipelines runs artifact list`. Preserves any other
# query parameters and only swaps `format` and `subPath`, so we don't break if
# the upstream URL shape ever changes.
function Get-ArtifactDownloadUrl {
param(
[Parameter(Mandatory)][string]$BaseUrl,
[Parameter(Mandatory)][string]$SubPath,
[Parameter(Mandatory)][ValidateSet('file', 'zip')][string]$Format
)
$encodedSubPath = [Uri]::EscapeDataString($SubPath)
$idx = $BaseUrl.IndexOf('?')
if ($idx -lt 0) {
return "${BaseUrl}?format=${Format}&subPath=${encodedSubPath}"
}
$base = $BaseUrl.Substring(0, $idx)
$kept = $BaseUrl.Substring($idx + 1) -split '&' | Where-Object {
$_ -and -not ($_ -match '^(format|subPath)=')
}
$kept = @($kept) + @("format=$Format", "subPath=$encodedSubPath")
return "${base}?$($kept -join '&')"
}
# Download a single ADO artifact file with bearer auth and a small retry/backoff
# loop. A transient network blip on a ~200 MB installer or symbol zip otherwise
# aborts the entire release-prep run.
function Invoke-AdoDownload {
param(
[Parameter(Mandatory)][string]$Url,
[Parameter(Mandatory)][string]$DestPath,
[Parameter(Mandatory)][string]$Token,
[int]$MaxAttempts = 3
)
$lastError = $null
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
$webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("Authorization", "Bearer $Token")
try {
$webClient.DownloadFile($Url, $DestPath)
return
}
catch {
$lastError = $_
if (Test-Path $DestPath) {
Remove-Item $DestPath -Force -ErrorAction SilentlyContinue
}
if ($attempt -lt $MaxAttempts) {
$backoffSec = [int][Math]::Pow(2, $attempt) # 2, 4, 8 ...
Write-Host " Attempt $attempt failed: $($_.Exception.Message). Retrying in ${backoffSec}s..." -ForegroundColor Yellow
Start-Sleep -Seconds $backoffSec
}
}
finally {
$webClient.Dispose()
}
}
throw "Download failed after $MaxAttempts attempts. Last error: $($lastError.Exception.Message)`nURL: $Url"
}
# -----------------------------------------------------------------------------
# Work around broken az extensions: if the default extension dir has
# inaccessible files, redirect to a clean directory.
$defaultExtDir = "$env:USERPROFILE\.azure\cliextensions"
if (-not $env:AZURE_EXTENSION_DIR -and (Test-Path $defaultExtDir)) {
$broken = Get-ChildItem "$defaultExtDir\*\*.dist-info" -Directory -ErrorAction SilentlyContinue | Where-Object {
try { [System.IO.Directory]::GetFiles($_.FullName) | Out-Null; $false } catch { $true }
}
if ($broken) {
$cleanDir = "$env:USERPROFILE\.azure\cliextensions_clean"
Write-Host " Detected broken az extension, redirecting to $cleanDir" -ForegroundColor Yellow
$env:AZURE_EXTENSION_DIR = $cleanDir
if (-not (Test-Path $cleanDir)) { New-Item -ItemType Directory -Path $cleanDir -Force | Out-Null }
}
}
# Ensure azure-devops extension is installed
$ext = Invoke-Az extension list --query "[?name=='azure-devops']" -o tsv
if (-not $ext) {
Write-Host "Installing azure-devops extension..." -ForegroundColor Yellow
Invoke-Az extension add --name azure-devops --yes | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to install azure-devops extension. (az: $script:LastAzError)"
exit 1
}
}
# Configure az devops defaults
Invoke-Az devops configure --defaults organization=$Organization project=$Project | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to configure az devops defaults. (az: $script:LastAzError)"
exit 1
}
# --- Step 1: Get build info to determine version ---
Write-Host "Fetching build $BuildId info..." -ForegroundColor Cyan
$buildJson = Invoke-Az pipelines build show --id $BuildId --output json
if (-not $buildJson) {
Write-Error "Could not fetch build $BuildId. Are you logged in (az login)? (az: $script:LastAzError)"
exit 1
}
$build = $buildJson | ConvertFrom-Json
$versionParam = $build.templateParameters.VersionNumber
if (-not $versionParam) {
Write-Error "Could not determine version from build $BuildId"
exit 1
}
Write-Host " Version: $versionParam" -ForegroundColor DarkGray
# --- Step 2: Get artifact metadata once ---
Write-Host "Fetching artifact metadata..." -ForegroundColor Cyan
$artifactsJson = Invoke-Az pipelines runs artifact list --run-id $BuildId --output json
if (-not $artifactsJson) {
Write-Error "Could not list artifacts for build $BuildId. (az: $script:LastAzError)"
exit 1
}
$artifacts = $artifactsJson | ConvertFrom-Json
# --- Step 3: Prepare destination folder ---
$destFolder = Join-Path $OutputFolder "PowerToys-v$versionParam"
if (-not (Test-Path $destFolder)) {
New-Item -ItemType Directory -Path $destFolder -Force | Out-Null
}
Write-Host " Destination: $destFolder" -ForegroundColor DarkGray
# --- Step 4: Get an ADO access token once ---
$token = Invoke-Az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" --query accessToken -o tsv
if (-not $token) {
Write-Error "Failed to acquire ADO access token. Run 'az login' first. (az: $script:LastAzError)"
exit 1
}
# --- Step 5: Define the four installers to download ---
$targets = @(
[pscustomobject]@{ Description = "Per user - x64"; Scope = "perUser"; Arch = "x64"; Artifact = "build-x64-Release"; FileName = "PowerToysUserSetup-$versionParam-x64.exe" }
[pscustomobject]@{ Description = "Per user - ARM64"; Scope = "perUser"; Arch = "arm64"; Artifact = "build-arm64-Release"; FileName = "PowerToysUserSetup-$versionParam-arm64.exe" }
[pscustomobject]@{ Description = "Machine wide - x64"; Scope = "perMachine"; Arch = "x64"; Artifact = "build-x64-Release"; FileName = "PowerToysSetup-$versionParam-x64.exe" }
[pscustomobject]@{ Description = "Machine wide - ARM64"; Scope = "perMachine"; Arch = "arm64"; Artifact = "build-arm64-Release"; FileName = "PowerToysSetup-$versionParam-arm64.exe" }
)
# --- Step 6: Download each installer (skip if already present) ---
foreach ($t in $targets) {
$destPath = Join-Path $destFolder $t.FileName
if (Test-Path $destPath) {
$sizeMB = [math]::Round((Get-Item $destPath).Length / 1MB, 1)
Write-Host "[skip] $($t.FileName) already exists ($sizeMB MB)" -ForegroundColor DarkGray
continue
}
$artifact = $artifacts | Where-Object { $_.name -eq $t.Artifact }
if (-not $artifact) {
Write-Error "Artifact '$($t.Artifact)' not found in build $BuildId. Available: $(($artifacts | ForEach-Object name) -join ', ')"
exit 1
}
$fileUrl = Get-ArtifactDownloadUrl -BaseUrl $artifact.resource.downloadUrl -SubPath "/$($t.FileName)" -Format file
Write-Host "Downloading $($t.FileName) ..." -ForegroundColor Cyan
try {
Invoke-AdoDownload -Url $fileUrl -DestPath $destPath -Token $token
}
catch {
Write-Error "Download failed for $($t.FileName): $_"
exit 1
}
$sizeMB = [math]::Round((Get-Item $destPath).Length / 1MB, 1)
Write-Host " Saved ($sizeMB MB)" -ForegroundColor Green
}
# --- Step 6b: Download symbols (one zip per arch) ---
$symbolTargets = @(
[pscustomobject]@{ Arch = "x64"; Artifact = "build-x64-Release"; SubPath = "/symbols-x64" }
[pscustomobject]@{ Arch = "arm64"; Artifact = "build-arm64-Release"; SubPath = "/symbols-arm64" }
)
foreach ($s in $symbolTargets) {
$finalZip = Join-Path $destFolder "symbols-$($s.Arch).zip"
if (Test-Path $finalZip) {
$sizeMB = [math]::Round((Get-Item $finalZip).Length / 1MB, 1)
Write-Host "[skip] symbols-$($s.Arch).zip already exists ($sizeMB MB)" -ForegroundColor DarkGray
continue
}
$artifact = $artifacts | Where-Object { $_.name -eq $s.Artifact }
if (-not $artifact) {
Write-Error "Artifact '$($s.Artifact)' not found in build $BuildId."
exit 1
}
# Symbols are downloaded as a folder => keep format=zip and append subPath
$symbolsUrl = Get-ArtifactDownloadUrl -BaseUrl $artifact.resource.downloadUrl -SubPath $s.SubPath -Format zip
$tmpZip = Join-Path ([System.IO.Path]::GetTempPath()) ("ptsym-$($s.Arch)-$([Guid]::NewGuid().ToString('N')).zip")
$tmpExtract = Join-Path ([System.IO.Path]::GetTempPath()) ("ptsym-$($s.Arch)-$([Guid]::NewGuid().ToString('N'))")
$stageRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("ptsym-stage-$([Guid]::NewGuid().ToString('N'))")
try {
Write-Host "Downloading symbols-$($s.Arch).zip ..." -ForegroundColor Cyan
try {
Invoke-AdoDownload -Url $symbolsUrl -DestPath $tmpZip -Token $token
}
catch {
Write-Error "Symbols download failed for $($s.Arch): $_"
exit 1
}
Write-Host " Extracting..." -ForegroundColor DarkGray
Expand-Archive -Path $tmpZip -DestinationPath $tmpExtract -Force
# Walk down while the current dir holds exactly one subfolder and no files.
$current = Get-Item $tmpExtract
while ($true) {
$children = Get-ChildItem -LiteralPath $current.FullName -Force
$subDirs = @($children | Where-Object { $_.PSIsContainer })
$files = @($children | Where-Object { -not $_.PSIsContainer })
if ($subDirs.Count -eq 1 -and $files.Count -eq 0) {
$current = $subDirs[0]
}
else {
break
}
}
# Stage to a folder named symbols-<arch> so the zip extracts to that name.
$stageInner = Join-Path $stageRoot "symbols-$($s.Arch)"
New-Item -ItemType Directory -Path $stageInner -Force | Out-Null
Get-ChildItem -LiteralPath $current.FullName -Force | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $stageInner -Recurse -Force
}
Write-Host " Repacking to $finalZip ..." -ForegroundColor DarkGray
if (Test-Path $finalZip) { Remove-Item $finalZip -Force }
Compress-Archive -Path "$stageInner\*" -DestinationPath $finalZip -CompressionLevel Optimal
$sizeMB = [math]::Round((Get-Item $finalZip).Length / 1MB, 1)
Write-Host " Saved symbols-$($s.Arch).zip ($sizeMB MB)" -ForegroundColor Green
}
catch {
# Don't leave a half-built zip behind if anything in the pipeline blew up.
if (Test-Path $finalZip) { Remove-Item $finalZip -Force -ErrorAction SilentlyContinue }
throw
}
finally {
Remove-Item $tmpZip -Force -ErrorAction SilentlyContinue
Remove-Item $tmpExtract -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $stageRoot -Recurse -Force -ErrorAction SilentlyContinue
}
}
# --- Step 7: Compute SHA256 and build markdown ---
Write-Host "`nComputing SHA256 hashes..." -ForegroundColor Cyan
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine("## Installer Hashes")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("| Description | Filename | sha256 hash |")
[void]$sb.AppendLine("| --- | --- | --- |")
foreach ($t in $targets) {
$destPath = Join-Path $destFolder $t.FileName
$hash = (Get-FileHash -Path $destPath -Algorithm SHA256).Hash.ToUpper()
[void]$sb.AppendLine("| $($t.Description) | $($t.FileName) | $hash |")
Write-Host " $($t.FileName) $hash" -ForegroundColor DarkGray
}
$markdown = $sb.ToString()
$mdPath = Join-Path $destFolder "hashes.md"
Set-Content -Path $mdPath -Value $markdown -Encoding UTF8
Write-Host "`nMarkdown written to: $mdPath" -ForegroundColor Green
Write-Host "`n----- Installer Hashes -----`n" -ForegroundColor Yellow
Write-Host $markdown
Write-Host "Draft a new GitHub release at: https://github.com/$GitHubRepo/releases/new?tag=v$versionParam" -ForegroundColor Green

View File

@@ -1,213 +0,0 @@
name: Automatic Triaging on Issue Creation
on:
issues:
types: [opened, reopened]
# 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.
workflow_dispatch:
inputs:
issue_numbers:
description: 'Comma-separated issue number(s) to label (e.g. 1234 or 1234,1235)'
required: true
permissions:
models: read
issues: 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 manual dispatch (which may cover multiple issues), use the unique 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:
label:
runs-on: ubuntu-latest
steps:
- name: Apply area labels with AI
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
# authenticate against the GitHub Models inference endpoint.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// When triggered manually, process each supplied issue number in turn.
// 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)
.split(',')
.map(s => parseInt(s.trim(), 10))
.filter(n => Number.isFinite(n) && n > 0);
} else {
issueNumbers = [context.issue.number];
}
if (issueNumbers.length === 0) {
console.log('No valid issue numbers to process; skipping.');
return;
}
for (const issueNumber of issueNumbers) {
console.log(`\n--- Processing issue #${issueNumber} ---`);
await labelIssue(issueNumber);
}
async function labelIssue(issueNumber) {
// 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 title = issue.title ?? '';
const body = issue.body ?? '';
if (!title && !body) {
console.log(`Issue #${issueNumber} has no title or body; skipping.`);
return;
}
// Truncation limit for issue body sent to the model. Keeps the
// prompt within the model's context window and avoids high token usage.
const MAX_BODY_LENGTH = 4000;
// Upper bound on model response tokens. A JSON array of label strings
// is compact; 200 tokens is more than enough for any realistic response.
const MAX_TOKENS = 200;
// All valid Product-* and Area-* labels the agent may choose from.
const VALID_LABELS = [
'Product-Advanced Paste',
'Product-Always On Top',
'Product-Awake',
'Product-Color Picker',
'Product-CommandNotFound',
'Product-Command Palette',
'Product-CropAndLock',
'Product-Environment Variables',
'Product-FancyZones',
'Product-File Explorer',
'Product-File Locksmith',
'Product-Find My Mouse',
'Product-Grab And Move',
'Product-Hosts File Editor',
'Product-Image Resizer',
'Product-Keyboard Manager',
'Product-LightSwitch',
'Product-Mouse Highlighter',
'Product-Mouse Jump',
'Product-Mouse Pointer Crosshairs',
'Product-Mouse Utilities',
'Product-Mouse Without Borders',
'Product-New+',
'Product-Peek',
'Product-PowerDisplay',
'Product-PowerRename',
'Product-PowerToys Run',
'Product-Quick Accent',
'Product-Registry Preview',
'Product-Screen Ruler',
'Product-Settings',
'Product-Shortcut Guide',
'Product-Text Extractor',
'Product-Workspaces',
'Product-ZoomIt',
'Area-Setup/Install',
'Area-Localization',
];
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:
${VALID_LABELS.map(l => ` • ${l}`).join('\n')}
- Choose only the labels that clearly match the issue content.
- If the issue mentions multiple areas, include a label for each one.
- If no label fits, return an empty array.
- Respond with ONLY a JSON array of label strings, no explanation.
Example: ["Product-FancyZones","Product-Settings"]`;
const userPrompt = `Issue title: ${title}
Issue body:
${body.slice(0, MAX_BODY_LENGTH)}`;
// Validate that the token is available before making the API call.
const token = process.env.GITHUB_TOKEN;
if (!token) {
console.log('GITHUB_TOKEN is not set; skipping.');
return;
}
// Call the GitHub Models inference endpoint (OpenAI-compatible).
const response = await fetch(
'https://models.inference.ai.azure.com/chat/completions',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
max_tokens: MAX_TOKENS,
// temperature: 0 ensures deterministic, consistent label
// classification across similar issues.
temperature: 0,
}),
}
);
if (!response.ok) {
const errorBody = await response.text();
console.log(`GitHub Models API error: ${response.status} ${response.statusText} — ${errorBody}`);
return;
}
const data = await response.json();
const text = data.choices?.[0]?.message?.content?.trim() ?? '';
console.log(`Model response: ${text}`);
let suggested;
try {
suggested = JSON.parse(text);
} catch {
console.log('Could not parse model response as JSON; skipping.');
return;
}
if (!Array.isArray(suggested) || suggested.length === 0) {
console.log('No labels suggested by the model.');
return;
}
// Only apply labels that are in the allow-list.
const validSet = new Set(VALID_LABELS);
const toApply = [...new Set(suggested.filter(l => validSet.has(l)))];
if (toApply.length === 0) {
console.log('Model returned no valid labels.');
return;
}
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(', ')}`);
}

View File

@@ -55,7 +55,7 @@ name: Spell checking
# spelling:
# # remove `security-events: write` and `use_sarif: 1`
# # remove `experimental_apply_changes_via_bot: 1`
# ... otherwise, adjust the `with:` as you wish
# ... otherwise adjust the `with:` as you wish
on:
push:
@@ -74,8 +74,6 @@ on:
types:
- "created"
permissions: {}
jobs:
spelling:
name: Check Spelling
@@ -87,7 +85,7 @@ jobs:
outputs:
followup: ${{ steps.spelling.outputs.followup }}
runs-on: ubuntu-latest
if: ${{ (contains(github.event_name, 'pull_request') && github.event.pull_request.state == 'open') || github.event_name == 'push' }}
if: ${{ contains(github.event_name, 'pull_request') || github.event_name == 'push' }}
concurrency:
group: spelling-${{ github.event.pull_request.number || github.ref }}
# note: If you use only_check_changed_files, you do not want cancel-in-progress
@@ -142,7 +140,7 @@ jobs:
comment-push:
name: Report (Push)
# If your workflow isn't running on push, you can remove this job
runs-on: ubuntu-slim
runs-on: ubuntu-latest
needs: spelling
permissions:
actions: read
@@ -152,21 +150,24 @@ jobs:
- name: comment
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
with:
spell_check_this: microsoft/PowerToys@main
task: ${{ needs.spelling.outputs.followup }}
comment-pr:
name: Report (PR)
# If you workflow isn't running on pull_request*, you can remove this job
runs-on: ubuntu-slim
runs-on: ubuntu-latest
needs: spelling
permissions:
actions: read
contents: read
pull-requests: write
if: (success() || failure()) && needs.spelling.outputs.followup && contains(github.event_name, 'pull_request')
steps:
- name: comment
uses: check-spelling/check-spelling@cfb6f7e75bbfc89c71eaa30366d0c166f1bd9c8c # v0.0.26
with:
spell_check_this: check-spelling/spell-check-this@prerelease
task: ${{ needs.spelling.outputs.followup }}
experimental_apply_changes_via_bot: ${{ github.repository_owner != 'microsoft' && 1 }}
@@ -176,13 +177,12 @@ jobs:
contents: write
pull-requests: write
actions: read
runs-on: ubuntu-slim
runs-on: ubuntu-latest
if: ${{
github.repository_owner != 'microsoft' &&
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '@check-spelling-bot') &&
contains(github.event.comment.body, 'apply') &&
contains(github.event.comment.body, '@check-spelling-bot apply') &&
contains(github.event.comment.body, 'https://')
}}
concurrency:

View File

@@ -1,35 +0,0 @@
# NOTE: This workflow depends on .github/scripts/telemetry-pr-check.js for telemetry detection and PR comments.
# Keep this workflow and script behavior in sync when making changes.
name: Telemetry PR Check
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
workflow_dispatch:
inputs:
pr_number:
description: "Pull Request Number to test against"
required: true
type: string
permissions:
contents: read
pull-requests: write
concurrency:
group: telemetry-pr-check-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
detect-telemetry-events:
if: ${{ github.event.pull_request.draft == false }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Detect telemetry event changes and comment PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node .github/scripts/telemetry-pr-check.js

6
.gitignore vendored
View File

@@ -365,8 +365,6 @@ installer/*/*.wxs.bk
**/.claude/settings.local.json
# Squad / Copilot agents — local-only, not committed
.copilot
.squad
.squad/
.squad-workstream
.github/agents/**squad**.md
.github/workflows/**squad**.yml
.github/agents/

View File

@@ -243,12 +243,8 @@
"WinUI3Apps\\PowerToys.RegistryPreview.dll",
"WinUI3Apps\\PowerToys.RegistryPreview.exe",
"WinUI3Apps\\PowerToys.ShortcutGuide.exe",
"WinUI3Apps\\PowerToys.ShortcutGuide.dll",
"WinUI3Apps\\PowerToys.ShortcutGuideModuleInterface.dll",
"WinUI3Apps\\PowerToys.ShortcutGuide.IndexYmlGenerator.dll",
"WinUI3Apps\\PowerToys.ShortcutGuide.IndexYmlGenerator.exe",
"WinUI3Apps\\ShortcutGuide.CPPProject.dll",
"PowerToys.ShortcutGuide.exe",
"PowerToys.ShortcutGuideModuleInterface.dll",
"PowerToys.ZoomIt.exe",
"PowerToys.ZoomItModuleInterface.dll",
@@ -268,8 +264,8 @@
"Workspaces.ModuleServices.dll",
"Microsoft.CommandPalette.Extensions.dll",
"Microsoft.CommandPalette.Extensions.Toolkit.dll",
"WinUI3Apps\\Microsoft.CmdPal.Ext.PowerToys.dll",
"WinUI3Apps\\Microsoft.CmdPal.Ext.PowerToys.exe",
"Microsoft.CmdPal.Ext.PowerToys.dll",
"Microsoft.CmdPal.Ext.PowerToys.exe",
"*Microsoft.CmdPal.UI_*.msix",
"PowerToys.DSC.dll",
@@ -387,11 +383,6 @@
"ColorCode.Core.dll",
"Microsoft.SemanticKernel.Connectors.Ollama.dll",
"OllamaSharp.dll",
"WinUI3Apps\\Google.Apis.dll",
"WinUI3Apps\\Google.Apis.Auth.dll",
"WinUI3Apps\\Google.Apis.Core.dll",
"WinUI3Apps\\Google.GenAI.dll",
"WinUI3Apps\\YamlDotNet.dll",
"boost_regex-vc143-mt-gd-x32-1_87.dll",
"boost_regex-vc143-mt-gd-x64-1_87.dll",

View File

@@ -16,7 +16,6 @@ pr:
include:
- main
- stable
drafts: false
# paths:
# exclude:
# - '**.md'

View File

@@ -70,7 +70,7 @@ parameters:
default: false
- name: winAppSDKVersionNumber
type: string
default: '2.0'
default: 1.6
- name: useExperimentalVersion
type: boolean
default: false
@@ -200,7 +200,7 @@ jobs:
- template: steps-ensure-dotnet-version.yml
parameters:
sdk: true
version: '10.0'
version: '9.0'
- ${{ if eq(parameters.runTests, true) }}:
- task: VisualStudioTestPlatformInstaller@1
@@ -418,7 +418,7 @@ jobs:
/p:VCRTForwarders-IncludeDebugCRT=false
/p:PowerToysRoot=$(Build.SourcesDirectory)
/p:PublishProfile=InstallationPublishProfile.pubxml
/p:TargetFramework=net10.0-windows10.0.26100.0
/p:TargetFramework=net9.0-windows10.0.26100.0
/bl:$(LogOutputDirectory)\publish-${{ join('_',split(project, '/')) }}.binlog
$(RestoreAdditionalProjectSourcesArg)
platform: $(BuildPlatform)

View File

@@ -64,7 +64,7 @@ jobs:
- template: steps-ensure-dotnet-version.yml
parameters:
sdk: true
version: '10.0'
version: '9.0'
- template: .\steps-restore-nuget.yml

View File

@@ -30,7 +30,7 @@ parameters:
default: false
- name: winAppSDKVersionNumber
type: string
default: '2.0'
default: 1.6
- name: useExperimentalVersion
type: boolean
default: false

View File

@@ -27,7 +27,6 @@ stages:
name: SHINE-INT-L
${{ else }}:
name: SHINE-OSS-L
demands: ImageOverride -equals SHINE-VS18-Latest
buildPlatforms:
- ${{ parameters.platform }}
uiTestModules: ${{ parameters.uiTestModules }}

View File

@@ -1,7 +1,7 @@
parameters:
- name: version
type: string
default: "10.0"
default: "9.0"
- name: sdk
type: boolean
default: false

View File

@@ -1,7 +1,7 @@
parameters:
- name: versionNumber
type: string
default: '2.0'
default: 1.6
- name: useExperimentalVersion
type: boolean
default: false

View File

@@ -48,11 +48,6 @@ foreach ($csprojFile in $csprojFilesArray) {
continue
}
# The PowerAccent.Common project does not target WinRT, so skip it
if ($csprojFile -like '*PowerAccent.Common.csproj') {
continue
}
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
if (!$importExists) {
Write-Output "$csprojFile need to import 'Common.Dotnet.CsWinRT.props'."

View File

@@ -22,7 +22,7 @@ $totalList = $projFiles | ForEach-Object -Parallel {
#Workaround for preventing exit code from dotnet process from reflecting exit code in PowerShell
$procInfo = New-Object System.Diagnostics.ProcessStartInfo -Property @{
FileName = "dotnet.exe";
Arguments = "list $csproj package --no-restore";
Arguments = "list $csproj package";
RedirectStandardOutput = $true;
RedirectStandardError = $true;
}

View File

@@ -13,6 +13,5 @@
{
"file": ".github/prompts/create-pr-summary.prompt.md"
}
],
"sarif-viewer.connectToGithubCodeScanning": "on"
]
}

View File

@@ -242,13 +242,6 @@ Thank you for using PowerToys!
| Microsoft.PowerToys.FindMyMouse_EnableFindMyMouse | Triggered when Find My Mouse is enabled. |
| Microsoft.PowerToys.FindMyMouse_MousePointerFocused | Occurs when the mouse pointer is focused using Find My Mouse, including the activation method (double-tap left/right Ctrl, shake mouse, or shortcut). |
### Grab And Move
| Event Name | Description |
| --- | --- |
| Microsoft.PowerToys.GrabAndMove_EnableGrabAndMove | Triggered when Grab And Move is enabled or disabled. |
| Microsoft.PowerToys.GrabAndMove_ShortcutUse | Logs an attempt to move or resize a window via the Alt+Drag shortcut, including whether it succeeded, the action type (move or resize), and the reason (e.g., started, blocked by game mode). |
### Hosts File Editor
| Event Name | Description |
@@ -369,15 +362,6 @@ Thank you for using PowerToys!
| Microsoft.PowerToys.Peek_Settings | Triggered when the settings for Peek are modified. |
| Microsoft.PowerToys.Peek_SpaceModeEnabled | Triggered when the Space key activation mode is enabled or disabled in Peek. |
### Power Display
| Event Name | Description |
| --- | --- |
| Microsoft.PowerToys.PowerDisplay_EnablePowerDisplay | Triggered when Power Display is enabled or disabled. |
| Microsoft.PowerToys.PowerDisplay_Activate | Triggered when Power Display is activated via hotkey or tray toggle. |
| Microsoft.PowerToys.PowerDisplay_Start | Triggered when the Power Display application process starts. |
| Microsoft.PowerToys.PowerDisplay_Settings | Periodic snapshot of Power Display settings, including whether the hotkey and tray icon are enabled, the number of detected monitors, and the number of saved profiles. |
### PowerRename
| Event Name | Description |

View File

@@ -171,12 +171,6 @@
$(USERPROFILE)\AppData\LocalLow\Microsoft\PowerToys\**;
</MSBuildCacheAllowFileAccessAfterProjectFinishFilePatterns>
<!-- dotnet.exe seems to access files after builds. Temporarily putting in this entry for testing if we get further. This looks to be related to a .NET Roslyn Analyzer in .NET 10-->
<MSBuildCacheAllowFileAccessAfterProjectFinishProcessPatterns>
$(MSBuildCacheAllowFileAccessAfterProjectFinishProcessPatterns);
\**\dotnet\dotnet.exe;
\**\vbcscompiler.exe;
</MSBuildCacheAllowFileAccessAfterProjectFinishProcessPatterns>
<!--
This repo uses a common output directory with many projects writing duplicate outputs. Allow everything, but note this costs some performance in the form of requiring
the cache to use copies instead of hardlinks when pulling from cache.

View File

@@ -65,20 +65,4 @@
<!-- Note: For C++ skipped test projects, build is effectively skipped by removing all compile items above.
We don't define empty Build/Rebuild/Clean targets here because MSBuild Target definitions with Condition
on the Target element still override the default targets even when condition is false. -->
<!-- Clean up unused VC++ runtime DLLs that CopyCppRuntimeToOutputDir copies from the full
VCRedist tree (MFC, C++ AMP, OpenMP). No PowerToys binary links against these — verified
with dumpbin /dependents across all installed binaries. -->
<Target Name="RemoveUnusedVCRuntimeDlls"
AfterTargets="Build"
Condition="'$(CopyCppRuntimeToOutputDir)' == 'true' and '$(MSBuildProjectExtension)' == '.vcxproj'">
<ItemGroup>
<_UnusedVCRuntimeDlls Include="$(OutDir)mfc140*.dll" />
<_UnusedVCRuntimeDlls Include="$(OutDir)mfcm140*.dll" />
<_UnusedVCRuntimeDlls Include="$(OutDir)vcamp140*.dll" />
<_UnusedVCRuntimeDlls Include="$(OutDir)vcomp140*.dll" />
</ItemGroup>
<Delete Files="@(_UnusedVCRuntimeDlls)" Condition="'@(_UnusedVCRuntimeDlls)' != ''" />
<Message Importance="normal" Text="Cleaned up unused VC runtime DLLs: @(_UnusedVCRuntimeDlls)" Condition="'@(_UnusedVCRuntimeDlls)' != ''" />
</Target>
</Project>

View File

@@ -1,4 +1,4 @@
<Project>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
@@ -39,37 +39,36 @@
<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.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.7" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.7" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.250303.1" />
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="10.2.0" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="10.0.1-preview.1.25571.5" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.7" />
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.10" />
<PackageVersion Include="Microsoft.AI.Foundry.Local" Version="0.3.0" />
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.71.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.71.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.71.0-beta" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.71.0-alpha" />
<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.SemanticKernel" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.OpenAI" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.AzureAIInference" Version="1.66.0-beta" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Google" Version="1.66.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.MistralAI" Version="1.66.0-alpha" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Ollama" Version="1.66.0-alpha" />
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3719.77" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3179.45" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="10.0.7" />
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.10" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="10.0.7" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.269" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.10" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.183" />
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
<!--
TODO: in Common.Dotnet.CsWinRT.props, on upgrade, verify RemoveCsWinRTPackageAnalyzer is no longer needed.
@@ -78,10 +77,10 @@
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<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.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.WindowsAppSDK" Version="1.8.260209005" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Foundation" Version="1.8.260203002" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.47" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.260209005" />
<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" />
@@ -90,11 +89,11 @@
<PackageVersion Include="MSTest" Version="$(MSTestVersion)" />
<PackageVersion Include="MSTest.TestFramework" Version="$(MSTestVersion)" />
<PackageVersion Include="NJsonSchema" Version="11.4.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
<PackageVersion Include="OpenAI" Version="2.7.0" />
<PackageVersion Include="OpenAI" Version="2.5.0" />
<PackageVersion Include="Polly.Core" Version="8.6.5" />
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
@@ -106,28 +105,31 @@
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.CodeDom" Version="10.0.7" />
<PackageVersion Include="System.CodeDom" Version="9.0.10" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.ComponentModel.Composition" Version="10.0.7" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="10.0.7" />
<PackageVersion Include="System.Data.OleDb" Version="10.0.7" />
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.10" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
<PackageVersion Include="System.Data.OleDb" Version="9.0.10" />
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
<!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.Diagnostics.EventLog" Version="10.0.7" />
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.10" />
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="10.0.7" />
<PackageVersion Include="System.ClientModel" Version="1.8.1" />
<PackageVersion Include="System.Drawing.Common" Version="10.0.7" />
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.10" />
<PackageVersion Include="System.ClientModel" Version="1.7.0" />
<PackageVersion Include="System.Drawing.Common" Version="9.0.10" />
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
<PackageVersion Include="System.Management" Version="10.0.7" />
<PackageVersion Include="System.Management" Version="9.0.10" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Numerics.Tensors" Version="10.0.2" />
<PackageVersion Include="System.Numerics.Tensors" Version="9.0.11" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="10.0.7" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="10.0.7" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="10.0.7" />
<PackageVersion Include="System.Text.Json" Version="10.0.7" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.10" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="ToolGood.Words.Pinyin" Version="3.1.0.3" />
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
@@ -135,8 +137,8 @@
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="WinUIEx" Version="2.8.0" />
<PackageVersion Include="WmiLight" Version="6.14.0" />
<PackageVersion Include="WPF-UI" Version="3.0.5" />
<PackageVersion Include="WyHash" Version="1.0.5" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
<PackageVersion Include="WixToolset.Firewall.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.Util.wixext" Version="5.0.2" />

View File

@@ -1587,6 +1587,7 @@ SOFTWARE.
- NLog.Extensions.Logging
- NLog.Schema
- OpenAI
- Polly.Core
- ReverseMarkdown
- ScipBe.Common.Office.OneNote
- SharpCompress
@@ -1600,5 +1601,5 @@ SOFTWARE.
- UTF.Unknown
- WinUIEx
- WmiLight
- WPF-UI
- WyHash
- YamlDotNet

View File

@@ -57,7 +57,6 @@
<Project Path="src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj" Id="1a066c63-64b3-45f8-92fe-664e1cce8077" />
<Project Path="src/common/UnitTests-CommonUtils/UnitTests-CommonUtils.vcxproj" Id="8b5cfb38-ccba-40a8-ad7a-89c57b070884" />
<Project Path="src/common/updating/updating.vcxproj" Id="17da04df-e393-4397-9cf0-84dabe11032e" />
<Project Path="src/common/updating/UnitTests/UpdatingUnitTests.vcxproj" Id="a1b2c3d4-e5f6-7890-abcd-ef1234567890" />
<Project Path="src/common/version/version.vcxproj" Id="cc6e41ac-8174-4e8a-8d22-85dd7f4851df" />
</Folder>
<Folder Name="/common/interop/">
@@ -207,10 +206,6 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Actions/Microsoft.CmdPal.Ext.Actions.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -324,10 +319,6 @@
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Indexer.UnitTests/Microsoft.CmdPal.Ext.Indexer.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Registry.UnitTests/Microsoft.CmdPal.Ext.Registry.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -802,14 +793,6 @@
<Project Path="src/modules/peek/peek/peek.vcxproj" Id="a1425b53-3d61-4679-8623-e64a0d3d0a48" />
</Folder>
<Folder Name="/modules/PowerAccent/">
<Project Path="src/modules/poweraccent/PowerAccent.Common.UnitTests/PowerAccent.Common.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/poweraccent/PowerAccent.Common/PowerAccent.Common.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/poweraccent/PowerAccent.Core/PowerAccent.Core.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
@@ -998,16 +981,9 @@
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/ShortcutGuide/">
<Project Path="src/modules/ShortcutGuide/ShortcutGuide.IndexYmlGenerator/ShortcutGuide.IndexYmlGenerator.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuide.Ui.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj" Id="e487304a-b1fb-4e6b-8e70-014051af5b99" />
<Folder Name="/modules/shortcutguide/">
<Project Path="src/modules/ShortcutGuide/ShortcutGuide/ShortcutGuide.vcxproj" Id="2edb3eb4-fa92-4bff-b2d8-566584837231" />
<Project Path="src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj" Id="2d604c07-51fc-46bb-9eb7-75aecc7f5e81" />
</Folder>
<Folder Name="/modules/Workspaces/">
<Project Path="src/modules/Workspaces/Workspaces.ModuleServices/Workspaces.ModuleServices.csproj">
@@ -1142,5 +1118,3 @@
<Project Path="src/Update/PowerToys.Update.vcxproj" Id="44ce9ae1-4390-42c5-bacc-0fd6b40aa203" />
<Project Path="tools/project_template/ModuleTemplate/ModuleTemplateCompileTest.vcxproj" Id="64a80062-4d8b-4229-8a38-dfa1d7497749" />
</Solution>

View File

@@ -29,13 +29,13 @@ PowerToys includes over 30 utilities to help you customize and optimize your Win
| [<img src="doc/images/icons/AdvancedPaste.png" alt="Advanced Paste icon" height="16"> Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [<img src="doc/images/icons/Always%20On%20Top.png" alt="Always on Top icon" height="16"> Always on Top](https://aka.ms/PowerToysOverview_AoT) | [<img src="doc/images/icons/Awake.png" alt="Awake icon" height="16"> Awake](https://aka.ms/PowerToysOverview_Awake) |
| [<img src="doc/images/icons/Color%20Picker.png" alt="Color Picker icon" height="16"> Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [<img src="doc/images/icons/Command%20Not%20Found.png" alt="Command Not Found icon" height="16"> Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [<img src="doc/images/icons/Command Palette.png" alt="Command Palette icon" height="16"> Command Palette](https://aka.ms/PowerToysOverview_CmdPal) |
| [<img src="doc/images/icons/Crop%20And%20Lock.png" alt="Crop and Lock icon" height="16"> Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [<img src="doc/images/icons/Environment%20Manager.png" alt="Environment Variables icon" height="16"> Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [<img src="doc/images/icons/FancyZones.png" alt="FancyZones icon" height="16"> FancyZones](https://aka.ms/PowerToysOverview_FancyZones) |
| [<img src="doc/images/icons/File%20Explorer%20Preview.png" alt="File Explorer Add-ons icon" height="16"> File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [<img src="doc/images/icons/File%20Locksmith.png" alt="File Locksmith icon" height="16"> File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [<img src="doc/images/icons/GrabAndMove.png" alt="Grab And Move icon" height="16"> Grab And Move](https://aka.ms/PowerToysOverview_GrabAndMove) |
| [<img src="doc/images/icons/Host%20File%20Editor.png" alt="Hosts File Editor icon" height="16"> Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) |
| [<img src="doc/images/icons/Light Switch.png" alt="Light Switch icon" height="16"> Light Switch](https://aka.ms/PowerToysOverview_LightSwitch) | [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) |
| [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) | [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) | [<img src="doc/images/icons/PowerDisplay.png" alt="PowerDisplay icon" height="16"> PowerDisplay](https://aka.ms/PowerToysOverview_PowerDisplay) |
| [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) |
| [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) |
| [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) | [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) |
| [<img src="doc/images/icons/File%20Explorer%20Preview.png" alt="File Explorer Add-ons icon" height="16"> File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [<img src="doc/images/icons/File%20Locksmith.png" alt="File Locksmith icon" height="16"> File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [<img src="doc/images/icons/Host%20File%20Editor.png" alt="Hosts File Editor icon" height="16"> Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) |
| [<img src="doc/images/icons/Image%20Resizer.png" alt="Image Resizer icon" height="16"> Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [<img src="doc/images/icons/Keyboard%20Manager.png" alt="Keyboard Manager icon" height="16"> Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [<img src="doc/images/icons/Light Switch.png" alt="Light Switch icon" height="16"> Light Switch](https://aka.ms/PowerToysOverview_LightSwitch) |
| [<img src="doc/images/icons/Find My Mouse.png" alt="Mouse Utilities icon" height="16"> Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [<img src="doc/images/icons/MouseWithoutBorders.png" alt="Mouse Without Borders icon" height="16"> Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [<img src="doc/images/icons/NewPlus.png" alt="New+ icon" height="16"> New+](https://aka.ms/PowerToysOverview_NewPlus) |
| [<img src="doc/images/icons/Peek.png" alt="Peek icon" height="16"> Peek](https://aka.ms/PowerToysOverview_Peek) | [<img src="doc/images/icons/PowerRename.png" alt="PowerRename icon" height="16"> PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [<img src="doc/images/icons/PowerToys%20Run.png" alt="PowerToys Run icon" height="16"> PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
| [<img src="doc/images/icons/PowerAccent.png" alt="Quick Accent icon" height="16"> Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [<img src="doc/images/icons/Registry%20Preview.png" alt="Registry Preview icon" height="16"> Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [<img src="doc/images/icons/MeasureTool.png" alt="Screen Ruler icon" height="16"> Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
| [<img src="doc/images/icons/Shortcut%20Guide.png" alt="Shortcut Guide icon" height="16"> Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [<img src="doc/images/icons/PowerOCR.png" alt="Text Extractor icon" height="16"> Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [<img src="doc/images/icons/Workspaces.png" alt="Workspaces icon" height="16"> Workspaces](https://aka.ms/PowerToysOverview_Workspaces) |
| [<img src="doc/images/icons/ZoomIt.png" alt="ZoomIt icon" height="16"> ZoomIt](https://aka.ms/PowerToysOverview_ZoomIt) | | |
## 📦 Installation
@@ -47,7 +47,21 @@ But to get started quickly, choose one of the installation methods below:
<summary><strong>Download the .exe file from GitHub</strong></summary>
<br/>
Go to the [PowerToys GitHub releases](https://aka.ms/installPowerToys), scroll down and select **Assets** to reveal the installation files, and choose the one that matches your architecture and install scope. For most devices, that would be _x64 per-user_.
Go to the [PowerToys GitHub releases](https://aka.ms/installPowerToys), select **Assets** to reveal the installation files, and choose the one that matches your architecture and install scope. For most devices, that would be _x64 per-user_.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.99%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysUserSetup-0.98.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.98.1/PowerToysSetup-0.98.1-arm64.exe
| Description | Filename |
| --- | --- |
| Per user - x64 | [PowerToysUserSetup-0.98.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.98.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.98.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.98.1-arm64.exe][ptMachineArm64] |
</details>
@@ -92,11 +106,11 @@ There are [community driven install methods](https://learn.microsoft.com/windows
[![What's new image](doc/images/readme/Release-Banner.png)](https://github.com/microsoft/PowerToys/releases)
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/).
To see what's new, check out the [release notes](https://github.com/microsoft/PowerToys/releases/tag/v0.98.1).
## 🛣️ Roadmap
We are planning some nice new features and improvements for the next releases a brand-new Shortcut Guide experience, ensuring it's easier to find and install Command Palette extensions and so much more! Stay tuned for [v0.100][github-next-release-work]!
We are planning some nice new features and improvements for the next releases PowerDisplay, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.99][github-next-release-work]!
## ❤️ PowerToys Community
@@ -117,4 +131,3 @@ The application logs basic diagnostic data (telemetry). For more privacy informa
[oss-CLA]: https://cla.opensource.microsoft.com
[oss-conduct-code]: CODE_OF_CONDUCT.md
[community-link]: COMMUNITY.md
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.100%22

View File

@@ -1,54 +0,0 @@
# Auto-resolve cherry-pick conflicts
param([int]$MaxAttempts = 100)
$attempts = 0
while ($attempts -lt $MaxAttempts) {
$attempts++
# Check if cherry-pick is in progress
$status = git status --porcelain
if (-not $status) {
Write-Host "Cherry-pick complete!" -ForegroundColor Green
break
}
# Get conflicted files
$conflicts = git diff --name-only --diff-filter=U
if ($conflicts) {
Write-Host "Attempt $attempts`: Resolving conflicts..." -ForegroundColor Yellow
foreach ($file in $conflicts) {
Write-Host " Resolving: $file"
git checkout --ours $file 2>$null
}
# Handle deleted files
git status --short | Where-Object { $_ -match '^DU' } | ForEach-Object {
$file = ($_ -split '\s+', 2)[1]
Write-Host " Removing deleted: $file"
git rm $file 2>$null
}
git add . 2>$null
}
# Try to continue
$result = git cherry-pick --continue 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host " Continued successfully" -ForegroundColor Green
}
elseif ($result -match 'empty') {
Write-Host " Skipping empty commit" -ForegroundColor Cyan
git cherry-pick --skip 2>&1 | Out-Null
}
else {
Write-Host " Error: $result" -ForegroundColor Red
Start-Sleep -Seconds 1
}
}
if ($attempts -ge $MaxAttempts) {
Write-Host "Max attempts reached. Check status manually." -ForegroundColor Red
}

2
deps/spdlog vendored

2
deps/spdlog.props vendored
View File

@@ -2,7 +2,7 @@
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory)spdlog\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;FMT_UNICODE=0;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
</Project>

View File

@@ -56,7 +56,7 @@ After generating the resx file, rename the existing rc and h files to ProjName.b
</Target>
```
This event runs a script which generates a resource.h and ProjName.rc in the `Generated Files` folder using the strings in all the resx files along with the existing information in resource.base.h and ProjName.base.rc. The script is [convert-resx-to-rc.ps1](https://github.com/microsoft/PowerToys/blob/main/tools/build/convert-resx-to-rc.ps1). The script uses [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert) to convert the resx file to a string table expected in the .rc file format. When the resources are added to the rc file the `IDS_` prefix is added and resource names are in uppercase (as it was originally). Any occurrences of `"` in the string resource is escaped as `""` to prevent build errors. The string tables are added to the rc file in the following format:
This event runs a script which generates a resource.h and ProjName.rc in the `Generated Files` folder using the strings in all the resx files along with the existing information in resource.base.h and ProjName.base.rc. The script is [convert-resx-to-rc.ps1](https://github.com/microsoft/PowerToys/blob/main/tools/build/convert-resx-to-rc.ps1). The script uses [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert) to convert the resx file to a string table expected in the .rc file format. When the resources are added to the rc file the `IDS_` prefix is added and resource names are in upper case (as it was originally). Any occurrences of `"` in the string resource is escaped as `""` to prevent build errors. The string tables are added to the rc file in the following format:
```
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -60,7 +60,7 @@ A markdown page is a page inside of command palette that displays markdown conte
```csharp
interface IMarkdownPage requires IPage {
String[] Bodies();
String[] Bodies(); // TODO! should this be an IBody, so we can make it observable?
IDetails Details();
IContextItem[] Commands { get; };
}

View File

@@ -1,167 +0,0 @@
# Command Palette Extension Gallery
This document describes how Command Palette (CmdPal) discovers extensions for
the in-app **Extension gallery** page.
## At a glance
- The gallery loads a single JSON feed called `extensions.json` from a remote
HTTPS URL, parses it, and renders the entries.
- The default feed lives in the external repo
**`microsoft/CmdPal-Extensions`** at
`https://aka.ms/CmdPal-ExtensionsJson`.
- Feed content + icon images are cached on disk so the page works offline and
survives short network hiccups.
- There is no WinGet discovery, no per-extension `manifest.json` fetch, and no
other network call for rendering the list.
## Implementation pointers
| Concern | File |
| --- | --- |
| Fetching, parsing, caching, pruning | `Microsoft.CmdPal.Common/ExtensionGallery/Services/ExtensionGalleryService.cs` |
| Resolving which URL to fetch | `Microsoft.CmdPal.Common/ExtensionGallery/Services/GalleryFeedUrlProvider.cs` + `Microsoft.CmdPal.UI/Helpers/GalleryServiceRegistration.cs` |
| HTTP + on-disk cache | `Microsoft.CmdPal.Common/ExtensionGallery/Services/ExtensionGalleryHttpClient.cs` (wraps `Microsoft.CmdPal.Common/Services/HttpCaching/HttpCachingClient`) |
| Feed + entry models | `Microsoft.CmdPal.Common/ExtensionGallery/Models/` |
## Feed URL resolution
`ExtensionGalleryService.GetFeedUrl()` returns, in order:
1. The user-configured URL from CmdPal settings (`SettingsModel.GalleryFeedUrl`,
exposed via the hidden `InternalPage` settings page). Any non-empty value
wins. Mostly used for local testing against a custom feed.
2. Otherwise, the built-in default
`https://aka.ms/CmdPal-ExtensionsJson`.
Local `file://` URIs are allowed too — `FetchFeedDocumentAsync` reads the file
directly and bypasses the HTTP cache.
## Feed format
The feed is a single wrapped JSON document with inline entries:
```json
{
"$schema": "https://raw.githubusercontent.com/microsoft/CmdPal-Extensions/main/.github/schemas/gallery.schema.json",
"extensions": [
{
"id": "sample-extension",
"title": "Sample Extension",
"description": "A sample extension demonstrating the gallery feed format.",
"author": { "name": "Microsoft", "url": "https://github.com/microsoft" },
"homepage": "https://github.com/microsoft/CmdPal-Extensions",
"iconUrl": "https://.../icon.png",
"screenshotUrls": ["https://.../screenshot-1.png"],
"tags": ["sample"],
"installSources": [
{ "type": "winget", "id": "Contoso.SampleExtension" },
{ "type": "msstore", "id": "9P…" },
{ "type": "url", "uri": "https://github.com/contoso/sample/releases/latest" }
],
"detection": { "packageFamilyName": "Contoso.SampleExtension_1234567890abc" }
}
]
}
```
Only the `extensions` array is read at runtime. The authoritative JSON
schema for an entry lives in the upstream feed repo
([`microsoft/CmdPal-Extensions`](https://github.com/microsoft/CmdPal-Extensions));
don't duplicate it here — it drifts.
### Required + optional entry fields
| Field | Required | Notes |
| --- | --- | --- |
| `id` | yes | Lowercase stable identifier; entries with empty id are dropped. |
| `title` | yes | Display name. |
| `description` | yes | Shown in list and detail views. |
| `author.name` | yes | `author.url` optional. |
| `installSources` | yes | At least one entry; see [Install sources](#install-sources). |
| `homepage`, `iconUrl`, `screenshotUrls`, `tags`, `detection.packageFamilyName` | no | All optional. |
Relative `iconUrl` / `screenshotUrls` are resolved against the feed URL's
directory (useful only for local / `file://` feeds during development).
## Install sources
Each entry's `installSources` is consumed by
`ExtensionGalleryItemViewModel` to decide which install affordances to show.
| `type` | Required field | Behaviour |
| --- | --- | --- |
| `winget` | `id` | Enables the "Install via WinGet" button (uses the shared WinGet service), and joins in-flight install progress + installed/update status. |
| `msstore` | `id` | Opens `ms-windows-store://pdp/?ProductId={id}`. |
| `url` | `uri` | Shown as a "GitHub" or "Website" link depending on host. |
An entry can declare any combination. Sources the runtime does not recognise
are surfaced as an "unknown source" indicator.
## Fetching and caching
`ExtensionGalleryService` uses `ExtensionGalleryHttpClient`, which wraps
`HttpCachingClient` over a file-system cache. Both the feed JSON and any
cacheable icon URLs are cached.
| Setting | Value | Defined in |
| --- | --- | --- |
| Cache root | `{AppCache}\GalleryCache\` | `ExtensionGalleryHttpClient.CacheDirectoryName` |
| Feed TTL | 4 hours | `ExtensionGalleryHttpClient.DefaultTimeToLive` |
| Icon TTL | 24 hours | `ExtensionGalleryService.IconCacheTtl` |
| HTTP timeout | 15 s | `ExtensionGalleryHttpClient` |
| `User-Agent` | `PowerToys-CmdPal/1.0` | `ExtensionGalleryHttpClient` |
`{AppCache}` resolves to `ApplicationData.Current.LocalCacheFolder` when
CmdPal runs packaged, and to
`%LOCALAPPDATA%\Microsoft\PowerToys\Microsoft.CmdPal\Cache\` when unpackaged
(see `ApplicationInfoService.DetermineCacheDirectory`).
### Fetch flow
`GetExtensionsAsync` (normal load) and `RefreshAsync` (user-initiated
refresh, `forceRefresh: true`) both go through `FetchWrappedFeedAsync`:
1. Resolve the feed URL (see above).
2. If the URL is local, read it from disk. Otherwise, hand it to
`HttpCachingClient.GetResourceAsync` which:
- Serves a fresh cached copy if one exists and TTL has not elapsed.
- Otherwise issues a conditional GET (ETag / `If-None-Match`). On `304
Not Modified` it refreshes the cache metadata and returns the cached
body.
- On network failure it returns the last-known cached body with
`UsedFallbackCache = true`, so the UI can show a "stale data" banner.
3. Parse the JSON with the source-generated `GallerySerializationContext`
(strongly-typed `GalleryRemoteIndex` — no reflection, AOT-friendly).
4. Drop entries with missing `id`, normalize relative `iconUrl` and
`screenshotUrls`, and resolve remote icon URIs through the same HTTP
cache so the UI binds to local `file://` URIs.
5. On a successful forced refresh, `PruneCachedResources` deletes cache
entries that are no longer referenced by the current feed (old feed URL
and icon URLs that dropped out of the feed).
### Fetch result flags
`GetExtensionsAsync` returns a `GalleryFetchResult` that the view model uses
for UI hints:
| Flag | Meaning |
| --- | --- |
| `FromCache` | The feed came from cache without hitting the network (TTL still valid). |
| `UsedFallbackCache` | A network request was attempted and failed, and the cached copy was served as fallback. The UI shows a stale-data info bar. |
| `RateLimited` | The origin returned `429 Too Many Requests` and no fallback was available. The UI shows a rate-limit error. |
## Authoring
- Entries for the production gallery are added to the feed repo
`microsoft/CmdPal-Extensions`.
- For editor validation of an entry, reference the schema published in the
upstream repo via the entry's `$schema` field.
- Keep `id` stable once an extension is published — users may have it
installed and the gallery keys install status by id.
- Prefer providing a `winget` source when the extension ships through App
Installer; the gallery uses it both for status ("Installed" / "Update
available") and for the in-app install button.
- `detection.packageFamilyName` lets the gallery recognise an
already-installed packaged extension before WinGet metadata resolves.

View File

@@ -76,13 +76,6 @@ functionality.
- [Status messages](#status-messages)
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
- [Addenda II: Commands with Parameters](#addenda-ii-commands-with-parameters)
- [String parameters](#string-parameters)
- [Command parameters - Invokable Commands](#command-parameters---invokable-commands)
- [Command parameters - List Commands](#command-parameters---list-commands)
- [Examples](#examples)
- [Addenda III: Rich Search (DRAFT)](#addenda-iii-rich-search-draft)
- [Nov 2025 status](#nov-2025-status)
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
- [Pinning nested commands to the dock (and top level)](#pinning-nested-commands-to-the-dock-and-top-level)
- [Class diagram](#class-diagram)
@@ -360,7 +353,7 @@ On a cold launch, DevPal will do the following:
* Start it up.
* Check if it's fresh or frozen.
* Call `TopLevelCommands`, and put all of them in the list
* Create an extension cache entry for that app.
* Create a extension cache entry for that app.
* If the provider is frozen: we can actually release the
`ICommandProvider` instance at this point.
* And of course, if we don't find all the packages we had cached, then delete
@@ -461,7 +454,7 @@ ms-windows-store://assoc/?Tags=AppExtension-com.microsoft.commandpalette
to open the store to a list of extensions. However, we can't list those
ourselves directly. Our friends in DevHome suggested it could be possible to
stand up an azure service which could query the store for us, and return a list
stand up a azure service which could query the store for us, and return a list
of extensions. This is not something that they currently have planned, nor would
it be cheap from an engineering standpoint.
@@ -1787,7 +1780,7 @@ class MyAppSettings {
/* You can save the settings to the file here */
var mySettingsFilePath = /* whatever */;
string mySettingsJson = mySettings.Settings.GetState();
// Or you could raise an event to indicate to the rest of your app that settings have changed.
// Or you could raise a event to indicate to the rest of your app that settings have changed.
}
}
@@ -2013,7 +2006,7 @@ class CommandWithOnlyProperties : IExtendedAttributesProvider { ... }
will populate the WinRT type cache in Command Palette with the type information
for `ICommandWithProperties`. In fact, if Command Palette has the
`IExtendedAttributesProvider` type info in its cache, and then later receives a new
`IExtendedAttributesProvider` type info in it's cache, and then later receives a new
`MyCommandWithProperties` object, it'll actually be able to know that
`MyCommandWithProperties` is an `IExtendedAttributesProvider`. WinRT is just weird
like that some times.
@@ -2055,183 +2048,6 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`,
developers won't have to do anything. The toolkit will just do the right thing
for them.
## Addenda II: Commands with Parameters
Extensions will often want to provide commands that accept parameters from the
user.
To support this, we're adding a new page type. The `IParametersPage` is a page
that allows an extension to define a set of parameters that the user can fill.
These parameters can be of different types, such as:
* Labels: static text that provides context or instructions.
* String parameters: text input fields where the user can type a string.
* Command parameters: interactive fields that allow the user to select from a
list of predefined commands, or just press a button to select an input.
Interleaving labels with parameters allows extensions to create rich, guided
input forms for their commands. These are a more lightweight solution than the
current adaptive card content.
```csharp
[uuid("a2590cc9-510c-4af7-b562-a6b56fe37f55")]
interface IParameterRun requires INotifyPropChanged
{
};
interface ILabelRun requires IParameterRun
{
String Text{ get; };
};
interface IParameterValueRun requires IParameterRun
{
String PlaceholderText{ get; };
Boolean NeedsValue{ get; }; // TODO! name is weird
};
interface IStringParameterRun requires IParameterValueRun
{
String Text{ get; set; };
// TODO! do we need a way to validate string inputs?
};
interface ICommandParameterRun requires IParameterValueRun
{
String DisplayText{ get; };
ICommand GetSelectValueCommand(UInt64 hostHwnd);
IIconInfo Icon{ get; }; // ? maybe
};
interface IParametersPage requires IPage
{
IParameterRun[] Parameters{ get; };
IListItem Command{ get; };
};
```
When we open a `IParametersPage`, we will render the `Parameters` in the search
box. We'll move focus to the first `IParameterRun` that is not a `ILabelRun`.
What those interactions looks like depends on the type of `IParameterRun`.
There are three basic types of inputs: strings, invokable commands, and lists.
Strings are a special case that doesn't require a command to set the value.
Lists and invokable commands are picked based on the type of the
`SelectValueCommand`. Each of these are detailed below.
When all the parameters have `NeedsValue` set to `false`, we will display a
single item to the user - the `Command` item.
### String parameters
These are rendered as a text box within the search box. The user can type into
it. Focus is moved to the next parameter when the user presses Enter or tab.
### Command parameters - Invokable Commands
These are used when the `SelectValueCommand` is an `IInvokableCommand`.
These are rendered as a button within the search box. The button text is
`DisplayText` if it is set. If it is not, we will display the
`PlaceholderText`. If the user clicks the button, we invoke the
`SelectValueCommand` (and ignore the `CommandResult`).
This is good for file pickers, date pickers, color pickers, etc. Anything that
requires a custom UI to pick a value.
When the extension has picked a value, it should set the `NeedsValue` to false.
The extension can also set the `DisplayText` and `Icon` to reflect the chosen value.
When the user presses enter with the button focused, we will also invoke the
`SelectValueCommand`.
When the user presses tab, we will move focus to the next parameter.
If the `NeedsValue` property is changed to `false` while it's focused, we will
move focus to the next parameter.
### Command parameters - List Commands
These are used when the `SelectValueCommand` is an `IListPage` - both static and
dynamic lists work similarly.
These are rendered as a text box within the search box. When the user focuses
the text box, we will display the items from the `IListPage` in the body of
CmdPal. The user can then type to filter the list. This filtering will work the
same way as any other list page in CmdPal - CmdPal will filter static lists, or
pass the query to a dynamic list.
The items in this list should all be `IListItem` objects with
`IInvokableCommands`. Putting a `IPage` into one of these items will cause the
user to navigate away from the parameters page, which would probably be
unexpected.
When the user picks an item from the list, the extension should handle that
command by bubbling an event up to the `CommandRun`, and setting the `Value`,
`DisplayText`, and `Icon` properties, and setting `NeedsValue` to false.
When the user presses enter with the text box focused, we will invoke the
command of the selected item in the list.
When the user presses tab, we will move focus to the next parameter.
If the `NeedsValue` property is changed to `false` while it's focused, we will
move focus to the next parameter.
### Examples
Lets say you had a command like "Create a note \${title} in \${folder}".
`title` is a string input, and `folder` is a static list of folders.
The extension author can then define a `IParametersPage` with four runs in it:
* A `ILabelRun` for "Create a note"
* A `IStringParameterRun` for the `title`
* A `ILabelRun` for "in"
* A `ICommandParameterRun` for the `folder`. The `Command` will be a
`IListPage`, where the items are possible folders
In this example, the user can pick the "create note" command, then type the
title, hit enter/tab, and then pick a folder from the list, then hit enter to
run the command.
Samples for the parameters page are implemented over in
[the sample extension](../../ext/SamplePagesExtension/Pages/ParameterSamples.cs)
## Addenda III: Rich Search (DRAFT)
> [!NOTE]
> _Mike_: Rich search and parameters were prototyped together, but ultimately we used two different solutions.
>
> Currently, we have a dummy implementation of draft C (ZWSP tokens), but without full API changes. Detailed [below](#nov-2025-status).
Extensions will often want to provide rich search experiences for their users.
This addenda is broken into multiple draft specs currently. These represent
different approaches to the same goals.
* **A**: [Rich Search Box](./drafts/RichSearchBox-draft-A.md)
* **B**: [Prefix Search](./drafts/PrefixSearch-draft-B.md)
* **C**: [ZWSP tokens](./drafts/PlainRichSearch-draft-C.md)
### Nov 2025 status
As of Nov 2025, we're implementing a simple version of draft C in the host.
In this version, if the extension implements `IDynamicListPage`, and also
implements `IExtendedAttributesProvider`, then they can set the `TokenSearch`
property. This will enlighten CmdPal to treat ZWSP-separated tokens in the
search text specially.
For an example, see
[this sample implementation](../../ext/SamplePagesExtension/Pages/SampleSuggestionsPage.cs).
In my head, I am still leaning towards a more full-featured version of draft C,
but with full CommandItem's in the `ISearchUpdateArgs` instead of just strings.
We'd almost need a new page type to support that, where the extension can add
`ICommandItem`s to the search box directly.
## Addenda IV: Dock bands
The "dock" is another way to surface commands to the user. This is a
@@ -2342,6 +2158,7 @@ because that method is was designed for two main purposes:
In neither of those scenarios was the full "display" of the item needed. In
pinning scenarios, however, we need everything that the user would see in the UI
for that item, which is all in the `ICommandItem`.
## Class diagram
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)
@@ -2533,7 +2350,7 @@ follow - these are not part of the current SDK spec.
> [!NOTE]
>
> A thought: what if an action returns a `CommandResult.Entity`, then that takes
> A thought: what if a action returns a `CommandResult.Entity`, then that takes
> devpal back home, but leaves the entity in the query box. This would allow for
> a Quicksilver-like "thing, do" flow. That command would prepopulate the
> parameters. So we would then filter top-level commands based on things that can

View File

@@ -2,7 +2,7 @@
This guide is for iterating on `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj`.
The extension is registered through the shared sparse package defined in `src/PackageIdentity/AppxManifest.xml`. That manifest declares `Microsoft.CmdPal.Ext.PowerToys.exe` relative to the sparse package's ExternalLocation (`WinUI3Apps\` subfolder), so the sparse package and the extension must be built for the same platform and configuration, for example `x64\Debug`.
The extension is registered through the shared sparse package defined in `src/PackageIdentity/AppxManifest.xml`. That manifest declares `Microsoft.CmdPal.Ext.PowerToys.exe` at the sparse package root, so the sparse package and the extension must be built for the same platform and configuration, for example `x64\Debug`.
## Local development loop
@@ -30,12 +30,12 @@ The extension is registered through the shared sparse package defined in `src/Pa
The command will look like this:
```powershell
Add-AppxPackage -Path "<repo>\<Platform>\<Configuration>\PowerToysSparse.msix" -ExternalLocation "<repo>\<Platform>\<Configuration>\WinUI3Apps"
Add-AppxPackage -Path "<repo>\<Platform>\<Configuration>\PowerToysSparse.msix" -ExternalLocation "<repo>\<Platform>\<Configuration>"
```
4. Build `src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj` in the same platform and configuration.
This project writes `Microsoft.CmdPal.Ext.PowerToys.exe` into the `WinUI3Apps\` subfolder of the output root, such as `x64\Debug\WinUI3Apps` or `ARM64\Debug\WinUI3Apps`. That matches the `Executable="Microsoft.CmdPal.Ext.PowerToys.exe"` entry in `src/PackageIdentity/AppxManifest.xml` resolved relative to the sparse package's ExternalLocation.
This project writes `Microsoft.CmdPal.Ext.PowerToys.exe` directly into the sparse package root, such as `x64\Debug` or `ARM64\Debug`. That matches the `Executable="Microsoft.CmdPal.Ext.PowerToys.exe"` entry in `src/PackageIdentity/AppxManifest.xml`.
5. Restart Command Palette.

View File

@@ -461,7 +461,7 @@ Editor read/write config data handler is in FancyZonesEditorCommon project.
FancyZones cpp project read/write config data handler is in FancyZonesLib project.
![Debug Step Image](../images/fancyzones/19.png)
However, the files read from and written to are those in `C:\Users\“xxxxxx”\AppData\Local\Microsoft\PowerToys\FancyZones`
However, the files write and read those are C:\Users\“xxxxxx”\AppData\Local\Microsoft\PowerToys\FancyZones
You can think of the editor as a visual config editor, which is most of its functionality. Another feature is used to set the layout for the monitor displays.

View File

@@ -3,9 +3,9 @@
- [ ] The plugin is a project under `modules\launcher\Plugins`
- [ ] Microsoft plugin project name pattern: `Microsoft.PowerToys.Run.Plugin.{PluginName}`
- [ ] Community plugin project name pattern: `Community.PowerToys.Run.Plugin.{PluginName}`
- [ ] The plugin target framework should be `net10.0-windows10.0.22621.0`
- [ ] The plugin target framework should be `net9.0-windows10.0.22621.0`
- [ ] If the plugin uses any 3rd party dependencies the project file should import `DynamicPlugin.props`
- [ ] 3rd party dependencies must be compatible with .NET 10
- [ ] 3rd party dependencies must be compatible with .NET 9
- [ ] The plugin has to contain a `plugin.json` file of the following format in its root folder:
```json

View File

@@ -75,7 +75,7 @@ There are three different score types with different start values.
| Medium score | 5000 |
| Low score | 1000 |
Each score will be decreased by one when a condition match.
Each score will decreased by one when a condition match.
| Priority | Condition | Score type |
| -------- | ----------------------------------------------------------------- | ------------ |
@@ -134,7 +134,7 @@ The plugin use only these interfaces (all inside the `Main.cs`):
| `plugin.json` | All meta-data for this plugin |
1. We need this extra wrapper class to make it possible that the JSON file can have and use a JSON schema file.
Because the JSON file must have an object as root type, instead of an array.
Because the JSON file must have a object as root type, instead of a array.
### Important project values (*.csproj)

View File

@@ -9,14 +9,12 @@
[Pull Requests](https://github.com/microsoft/PowerToys/pulls?q=is%3Apr+is%3Aopen+label%3A%22Product-Shortcut+Guide%22+)
## Overview
Shortcut Guide is a PowerToy that displays an overlay of available keyboard shortcuts when a user-set keyboard shortcut is pressed. It helps users discover and remember keyboard shortcuts for Windows and apps.
> [!NOTE]
> The spec for the manifest files is in development and will be linked here once available.
Shortcut Guide is a PowerToy that displays an overlay of available keyboard shortcuts when the Windows key is pressed and held. It provides a visual reference for Windows key combinations, helping users discover and utilize built-in Windows shortcuts.
## Usage
- Press the user-defined hotkey to display the overlay
- Press the hotkey again or press ESC to dismiss the overlay
- Press and hold the Windows key to display the overlay of available shortcuts
- Press the hotkey again to dismiss the overlay
- The overlay displays Windows shortcuts with their corresponding actions
## Build and Debug Instructions
@@ -27,89 +25,67 @@ Shortcut Guide is a PowerToy that displays an overlay of available keyboard shor
4. The executable is named PowerToys.ShortcutGuide.exe
### Debug
1. Right-click the ShortcutGuide.Ui project and select 'Set as Startup Project'
1. Right-click the ShortcutGuide project and select 'Set as Startup Project'
2. Right-click the project again and select 'Debug'
> [!NOTE]
> When run in debug mode, the window behaves differently than in release mode. It will not automatically close when loosing focus, it will be displayed on top of all other windows, and it is not hidden from the taskbar.
## Code Structure
## Project Structure
![Diagram](../images/shortcutguide/diagram.png)
The Shortcut Guide module consists of the following 4 projects:
### Core Files
### [`ShortcutGuide.Ui`](/src/modules/ShortcutGuide/ShortcutGuide.Ui/ShortcutGuide.Ui.csproj
#### [`dllmain.cpp`](/src/modules/shortcut_guide/dllmain.cpp)
Contains DLL boilerplate code. Implements the PowertoyModuleIface, including enable/disable functionality and GPO policy handling. Captures hotkey events and starts the PowerToys.ShortcutGuide.exe process to display the shortcut guide window.
This is the main UI project for the Shortcut Guide module. Upon startup it does the following tasks:
#### [`shortcut_guide.cpp`](/src/modules/shortcut_guide/shortcut_guide.cpp)
Contains the module interface code. It initializes the settings values and the keyboard event listener. Defines the OverlayWindow class, which manages the overall logic and event handling for the PowerToys Shortcut Guide.
1. Copies the built-in manifest files to the users manifest directory (overwriting existing files).
2. Generate the `index.yml` manifest file.
3. Populate the PowerToys shortcut manifest with the user-defined shortcuts.
4. Starts the UI.
#### [`overlay_window.cpp`](/src/modules/shortcut_guide/overlay_window.cpp)
Contains the code for loading the SVGs, creating and rendering of the overlay window. Manages and displays overlay windows with SVG graphics through two main classes:
- D2DOverlaySVG: Handles loading, resizing, and manipulation of SVG graphics
- D2DOverlayWindow: Manages the display and behavior of the overlay window
### Related files in PowerToys.Interop
#### [`keyboard_state.cpp`](/src/modules/shortcut_guide/keyboard_state.cpp)
Contains helper methods for checking the current state of the keyboard.
#### [`excluded_app.cpp`](/src/modules/ShortcutGuide/ShortcutGuide.CPPProject/excluded_app.cpp)
#### [`target_state.cpp`](/src/modules/shortcut_guide/target_state.cpp)
State machine that handles the keyboard events. It's responsible for deciding when to show the overlay, when to suppress the Start menu (if the overlay is displayed long enough), etc. Handles state transitions and synchronization to ensure the overlay is shown or hidden appropriately based on user interactions.
This file contains one function with the following signature:
#### [`trace.cpp`](/src/modules/shortcut_guide/trace.cpp)
Contains code for telemetry.
```cpp
__declspec(dllexport) bool IsCurrentWindowExcludedFromShortcutGuide()
```
### Supporting Files
This function checks if the current window is excluded from the Shortcut Guide overlay. It returns `true` if the current window is excluded otherwise it returns `false`.
#### [`animation.cpp`](/src/modules/shortcut_guide/animation.cpp)
Handles the timing and interpolation of animations. Calculates the current value of an animation based on elapsed time and a specified easing function.
#### [`tasklist_positions.cpp`](/src/modules/ShortcutGuide/ShortcutGuide.CPPProject/tasklist_positions.cpp)
#### [`d2d_svg.cpp`](/src/modules/shortcut_guide/d2d_svg.cpp)
Provides functionality for loading, resizing, recoloring, rendering, and manipulating SVG images using Direct2D.
This file contains helper functions to retrieve the positions of the taskbar buttons. It exports the following function:
#### [`d2d_text.cpp`](/src/modules/shortcut_guide/d2d_text.cpp)
Handles creation, resizing, alignment, and rendering of text using Direct2D and DirectWrite.
```cpp
__declspec(dllexport) TasklistButton* get_buttons(HMONITOR monitor, int* size)
```
#### [`d2d_window.cpp`](/src/modules/shortcut_guide/d2d_window.cpp)
Manages a window using Direct2D and Direct3D for rendering. Handles window creation, resizing, rendering, and destruction.
This function retrieves the positions of the taskbar buttons for a given monitor. It returns an array of `TasklistButton` structures (max 10), which contain the position and size of each button.
#### [`native_event_waiter.cpp`](/src/modules/shortcut_guide/native_event_waiter.cpp)
Waits for a named event and executes a specified action when the event is triggered. Uses a separate thread to handle event waiting and action execution.
`monitor` must be the monitor handle of the monitor containing the taskbar instance of which the buttons should be retrieved.
#### [`tasklist_positions.cpp`](/src/modules/shortcut_guide/tasklist_positions.cpp)
Handles retrieving and updating the positions and information of taskbar buttons in Windows.
`size` will contain the resulting array size.
It determines the positions through Windows `FindWindowEx` function.
For the primary taskbar it searches for:
* A window called "Shell_TrayWnd"
* that contains a window called "ReBarWindow32"
* that contains a window called "MSTaskSwWClass"
* that contains a window called "MSTaskListWClass"
For any secondary taskbar it searches for:
* A window called "Shell_SecondaryTrayWnd"
* that contains a window called "WorkerW"
* that contains a window called "MSTaskListWClass"
It then enumerates all the button elements inside "MSTaskListWClass" while skipping such with a same name (which implies the user does not use combining taskbar buttons)
If this method fails, which it will for newer versions of Windows, it falls back to searching for:
* A window called "Shell_TrayWnd" or "Shell_SecondaryTrayWnd"
* that contains a window called "Windows.UI.Composition.DesktopWindowContentBridge"
* that contains a window called "Windows.UI.Input.InputSite.WindowClass"
* the first child element
It then enumerates all the button elements inside the selected while skipping such with a same name (which implies the user does not use combining taskbar buttons) and such that do not start with "Appid:" (which are not actual taskbar buttons related to apps, but others like the widgets or the search button).
### [`ShortcutGuide.IndexYmlGenerator`](/src/modules/ShortcutGuide/ShortcutGuide.IndexYmlGenerator/)
This application generates the `index.yml` manifest file.
It is a separate project so that its code can be easier ported to WinGet in the future.
### [`ShortcutGuideModuleInterface`](/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/ShortcutGuideModuleInterface.vcxproj)
The module interface that handles opening and closing the user interface.
#### [`main.cpp`](/src/modules/shortcut_guide/main.cpp)
The entry point for the PowerToys Shortcut Guide application. Handles initialization, ensures single instance execution, manages parent process termination, creates and displays the overlay window, and runs the main event loop.
## Features and Limitations
- Currently the displayed shortcuts (Except the ones from PowerToys) are not localized.
- The overlay displays Windows shortcuts (Windows key combinations)
- The module supports localization, but only for the Windows controls on the left side of the overlay
- It's currently rated as a P3 (lower priority) module
## Future Development
- Implementing with WinGet to get new shortcut manifest files
- Adding localization support for the built-in manifest files
A community-contributed version 2 is in development that will support:
- Application-specific shortcuts based on the active application
- Additional shortcuts beyond Windows key combinations
- PowerToys shortcuts

View File

@@ -1,93 +0,0 @@
# PowerToys Installer & Update Diagnostics
A step-by-step guide for diagnosing installer and update issues reported by users.
## Quick Reference: Key Files
| File/Folder | Path | Contains |
|---|---|---|
| UpdateState.json | `%LOCALAPPDATA%\Microsoft\PowerToys\UpdateState.json` | Persisted update state machine |
| Runner logs | `%LOCALAPPDATA%\Microsoft\PowerToys\RunnerLogs\runner-log_*.log` | Startup, update checks, cleanup |
| Update logs | `%LOCALAPPDATA%\Microsoft\PowerToys\UpdateLogs\update-log_*.log` | PowerToys.Update.exe activity |
| Updates folder | `%LOCALAPPDATA%\Microsoft\PowerToys\Updates\` | Downloaded installer files |
> **Note:** These paths use `%LOCALAPPDATA%` (per-user AppData) regardless of whether PowerToys was installed per-user or per-machine. The data/settings location is always per-user.
## Update State Values
From `src/common/updating/updateState.h` (`UpdateState::State` enum):
| Value | Name | Meaning |
|---|---|---|
| 0 | upToDate | No update needed |
| 1 | errorDownloading | Download or install failed, will retry |
| 2 | readyToDownload | New version found, not yet downloaded |
| 3 | readyToInstall | Installer downloaded, waiting for user action |
| 4 | networkError | GitHub API call failed |
---
## Symptom: Old update installers accumulating on disk
### What to ask the user for
1. Contents of `UpdateState.json`
2. Runner logs (last few days from `RunnerLogs\`)
3. Update logs (from `UpdateLogs\`, if they exist)
4. List of files in `Updates\` folder (names + sizes)
### Step 1: Check the running version
In runner logs, look for the startup line:
```
[info] Scoobe: product_version=v0.XX.X last_version_run=v0.XX.X
```
- **If version < v0.73.0**: The pre-download cleanup (PR #27908) is missing. Each downloaded installer accumulates because cleanup only runs at startup when state is `upToDate`. Ask the user to manually upgrade to the latest version.
- **If version >= v0.73.0**: The pre-download cleanup exists. Accumulation should not happen under normal conditions. Continue to Step 2.
### Step 2: Check UpdateState.json
```jsonc
{"state": 3, "downloadedInstallerFilename": "powertoyssetup-0.98.1-x64.exe" /* additional fields may be present */}
```
- **state = 0 (upToDate)**: Cleanup should run at startup. If files are accumulating, check runner logs for "Failed to delete" warnings (Step 4).
- **state = 3 (readyToInstall)**: An installer is downloaded but never installed. Cleanup at startup is skipped (by design, to preserve the pending installer). On v0.73+, cleanup can still occur when a future update check triggers a new download (pre-download cleanup path).
- **state = 1 (errorDownloading)**: A previous download or install failed. Startup cleanup is skipped (state is not `upToDate`). On v0.73+, cleanup runs before the next installer download is attempted.
- **state = 2 or 4**: Startup cleanup is skipped. On v0.73+, cleanup runs before the next installer download is attempted.
### Step 3: Check if PowerToys.Update.exe has ever run
- **UpdateLogs directory missing**: This suggests `PowerToys.Update.exe` may never have been launched, or it did not progress far enough to create logs. The user may never have triggered an install, or Stage 1 may have failed before Stage 2 could run.
- **UpdateLogs exist but show only "logger is initialized"**: The exe launched but the command-line argument didn't match any action (possible argument parsing issue).
- **UpdateLogs show install activity**: The update process ran. Check for success/failure.
### Step 4: Check runner logs for cleanup evidence
Search for these patterns:
| Log pattern | Meaning |
|---|---|
| `Failed to delete installer file ... Access is denied` | File locked by AV, another process, or permissions issue |
| `Failed to delete log file ...` | Same, for old log files |
| `Discovered new version` | Periodic update check ran |
| `New version is already downloaded` | State is `readyToInstall` and filename matches — no re-download, no cleanup |
| No cleanup-related entries at all | Inconclusive by itself — `cleanup_updates()` is silent on success. Corroborate with the Updates folder contents (Step 5) and the running version (Step 1). |
### Step 5: Check the Updates folder contents
- **All different versions**: Cleanup likely did not run across multiple update cycles. Confirm with the running version (Step 1) and update state before concluding a state gate issue.
- **Duplicate filenames**: Unusual — would suggest repeated download without cleanup.
- **Single file matching `downloadedInstallerFilename`**: Normal for `readyToInstall` state.
### Common root causes
| Root cause | Evidence | Fix |
|---|---|---|
| Running pre-v0.73.0 binary | `product_version` < v0.73.0 in runner log | Manually upgrade to latest |
| State stuck at `readyToInstall` (pre-v0.73) | `"state": 3` in UpdateState.json, no UpdateLogs | Manually upgrade to latest |
| File lock preventing deletion | "Failed to delete ... Access is denied" in runner logs | Check AV software, reboot and retry |
| Update installer never launched | No UpdateLogs directory | Check if update notifications are disabled by GPO or setting |
| Install fails silently | UpdateLogs show init but no install activity | Check related issues: #46966, #46967, #46969 |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 256 KiB

View File

@@ -1,318 +0,0 @@
# WinGet Manifest Keyboard Shortcuts schema
## 1 What this spec is about
This spec provides an extension to the existing [WinGet manifest schema](https://github.com/microsoft/winget-pkgs/blob/master/doc/manifest/README.md) in form of an additional yaml file, that describes keyboard shortcuts the application provides.
These yaml files are saved on a per-user base and so called manifest interpreters can then display these manifests in a human-friendly version.
### 1.1 What this spec is not about
This spec does not provide a way to back up or save user-defined keyboard shortcuts.
## 2 Save location of manifests
### 2.1 WinGet
These files are saved online along with the other manifest files in the [WinGet Package repository](https://github.com/microsoft/winget-pkgs).
### 2.2 Locally
All manifests and one index file are saved locally under `%LocalAppData%/Microsoft/WinGet/KeyboardShortcuts`. All apps are allowed to add their manifest files there. In addition Package Managers (like WinGet) and manifest interpreters (like PowerToys Shortcut Guide) can control and add other manifests themselves.
#### 2.2.1 Downloading manifests
When WinGet or other package managers download a package, they should also download the corresponding keyboard shortcuts manifest file and save it in the local directory, given such a file exists in the WinGet repository.
The downloader is also responsible for updating the local `index.yaml` file, which contains all the information about the different manifest files that are saved in the same directory.
#### 2.2.2 Updating manifests
When a manifest interpreter starts, it should download the latest version of the manifests from the WinGet repository and save them in the local directory. If a manifest interpreter is not able to download the manifests or they do not exist, it should use the locally saved manifests.
The updater is also responsible for updating the local `index.yaml` file, which contains all the information about the different manifest files that are saved in the same directory.
> Note: WinGet must provide a way to update the keyboard shortcuts manifests given a package id.
### 2.3 File names
The file name of a keyboard shortcuts file is the WinGet package identifier, plus the locale of the strings of the file and at last the `.KBSC.yaml` file extension.
For example the package "test.bar" saves its manifest with `en-US` strings in `test.bar.en-US.KBSC.yaml`.
#### 2.3.1 No winget package available
If an application has no corresponding WinGet package its name starts with a plus (`+`) symbol.
### 2.4 Reserved namespaces
Every name starting with `+WindowsNT` is reserved for the Windows OS and its components.
## 3 File syntax
All relevant files are written in [YAML](https://yaml.org/spec).
> Note: A JSON schema will be provided as soon as the spec reaches a further step
### 3.1 Manifest Schema vNext Keyboard Shortcuts File
```
PackageName: # The package unique identifier
WindowFilter: # The filter of window processes to which the shortcuts apply to
BackgroundProcess: # Optionally allows applying WindowFilter to background processes
Shortcuts: # List of sections with keyboard shortcuts
- SectionName: # Name of the category of shortcuts
Properties: # List of shortcuts in the category
- Name: # Name of the shortcut
Description: # Optional description of the shortcut
AdditionalInfo: # Optional additional information about the shortcut
Recommended: # Optionally determines if the shortcut is displayed in a designated recommended area
Shortcut: # An array of shortcuts that need to be pressed
- Win: # Determines if the Windows Key is part of the shortcut
Ctrl: # Determines if the Ctrl Key is part of the shortcut
Shift: # Determines if the Shift Key is part of the shortcut
Alt: # Determines if the Alt Key is part of the shortcut
Keys: # Array of keys that need to be pressed
```
Per Application/Package one or more Keyboard manifests can be declared. Every manifest must have a different locale and the same `PackageName`, `WindowFilter` and `BackgroundProcess` fields.
<details>
<summary><b>PackageName</b> - The package unique identifier</summary>
Package identifier (see 2.1 for more information on the package identifier).
</details>
<details>
<summary><b>WindowFilter</b> - The filter of window processes to which the shortcuts apply to</summary>
This field declares for which process name the shortcuts should be showed (To rephrase: For which processes the shortcut will have an effect if pressed). You can use an asterisk to leave out a certain part. For example `*.PowerToys.*.exe` targets all PowerToys processes and `*` apply to any process.
</details>
<details>
<summary><b>BackgroundProcess</b> - Optionally allows applying WindowFilter to background processes.</summary>
**Optional field**
Defaults to `False`. Determines if WindowFilter should apply to background processes as well (Rephrased: When the process is running, the shortcuts will apply).
</details>
<details>
<summary><b>Shortcuts</b> - List of sections with keyboard shortcuts</summary>
List of different section (also called categories) of shortcuts.
</details>
<details>
<summary><b>SectionName</b> - Name of the category of shortcuts</summary>
Name of the section of shortcuts.
**Special sections**:
Special sections start with an identifier enclosed between `<` and `>`. This declares the category as a special display. If the interpreter of the manifest file can't understand the content this section should be left out.
</details>
<details>
<summary><b>Properties</b> - List of shortcuts in the category</summary>
</details>
<details>
<summary><b>Name</b> - Name of the shortcut</summary>
Name of the shortcut. This is the name that will be displayed in the interpreter.
</details>
<details>
<summary><b>Description</b> - Optional description of the shortcut</summary>
Optional description of the shortcut. This is the description that will be displayed by the interpreter.
</details>
<details>
<summary><b>AdditionalInfo</b> - Optional additional information about the shortcut</summary>
Array of additional information about the shortcut. This is the additional information that will be displayed by the interpreter and are not part of this manifest.
**Example**:
For example, if the shortcut is only available on a certain Windows version, this information could be added here.
```yaml
AdditionalInfo:
- MinWindowsVersion: "10.0.19041.0"
```
</details>
<details>
<summary><b>Shortcut</b> - An array of shortcuts that need to be pressed</summary>
An array of shortcuts that need to be pressed. This allows defining sequential shortcuts that need to be pressed in order to trigger the action.
</details>
<details>
<summary><b>Win</b> - Determines if the Windows Key is part of the shortcut</summary>
Refers to the left Windows Key on the keyboard.
</details>
<details>
<summary><b>Ctrl</b> - Determines if the Ctrl Key is part of the shortcut</summary>
Refers to the left Ctrl Key on the keyboard.
</details>
<details>
<summary><b>Shift</b> - Determines if the Shift Key is part of the shortcut</summary>
Refers to the left Shift Key on the keyboard.
</details>
<details>
<summary><b>Alt</b> - Determines if the Alt Key is part of the shortcut</summary>
Refers to the left Alt Key on the keyboard.
</details>
<details>
<summary><b>Recommended</b> - Optionally determines if the shortcut is displayed in a designated recommended area</summary>
**Optional field**
Defaults to `False`. Determines if the shortcut should be displayed in a designated recommended area. This is a visual hint for the user that this shortcut is important.
</details>
<details>
<summary><b>Keys</b> - Array of keys that need to be pressed</summary>
A string array of all the keys that need to be pressed. If a number is supplied, it should be read as a [KeyCode](https://learn.microsoft.com/windows/win32/inputdev/virtual-key-codes) and displayed accordingly (based on the Keyboard Layout of the user).
**Special keys**:
Special keys are enclosed between `<` and `>` and correspond to a key that should be displayed in a certain way. If the interpreter of the manifest file can't understand the content, the brackets should be left out.
|Name|Description|
|----|-----------|
|`<Office>`| Corresponds to the Office key on some Windows keyboards |
|`<Copilot>`| Corresponds to the Copilot key on some Windows keyboards |
|`<Left>`| Corresponds to the left arrow key |
|`<Right>`| Corresponds to the right arrow key |
|`<Up>`| Corresponds to the up arrow key |
|`<Down>`| Corresponds to the down arrow key |
|`<Enter>`| Corresponds to the Enter key |
|`<Space>`| Corresponds to the Space key |
|`<Tab>`| Corresponds to the Tab key |
|`<Backspace>`| Corresponds to the Backspace key |
|`<Delete>`| Corresponds to the Delete key |
|`<Insert>`| Corresponds to the Insert key |
|`<Home>`| Corresponds to the Home key |
|`<End>`| Corresponds to the End key |
|`<PrtScr>`| Corresponds to the Print Screen key |
|`<Pause>`| Corresponds to the pause key |
|`<PageUp>`| Corresponds to the Page Up key |
|`<PageDown>`| Corresponds to the Page Down key |
|`<Escape>`| Corresponds to the Escape key |
|`<Arrow>`| Corresponds to either the left, right, up or down arrow key |
|`<ArrowLR>`| Corresponds to either the left or right arrow key |
|`<ArrowUD>`| Corresponds to either the up or down arrow key |
|`<Underlined letter>`| Corresponds to any letter that is _underlined_ in the UI |
</details>
#### 3.2.2 Example
```yaml
PackageName: Microsoft.PowerToys
WindowFilter: "*"
BackgroundProcess: True
Shortcuts:
- SectionName: General
Properties:
- Name: Advanced Paste
Shortcut:
- Win: True
Ctrl: False
Alt: False
Shift: False
Keys:
- 86
Description: Open Advanced Paste window
- Name: Advanced Paste
Shortcut:
- Win: True
Ctrl: True
Alt: True
Shift: False
Keys:
- 86
Description: Paste as plain text directly
```
### 3.2 `index.yaml` file
The `index.yaml` file is a file that contains all the information about the different manifest files that are saved in the same directory. This file is only available locally and is not saved in the WinGet repository as it is specific to the user.
```yaml
DefaultShellName: # The package identifier of the default shell used in Windows
Index: # List of all manifest files
- WindowFilter: # The filter of window processes to which the shortcuts apply to
BackgroundProcess: # Optionally allows applying WindowFilter to background processes
Apps: # List of all manifest files for the filter
```
<details>
<summary><b>DefaultShellName</b> - The package identifier of the default shell used in Windows</summary>
This declares the package identifier of the default shell used in Windows. Most commonly it is `+WindowsNT.Shell`. Although not enforced, only the shell declared in the registry key `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell` should be used here.
</details>
<details>
<summary><b>Index</b> - List of all manifest files</summary>
</details>
<details>
<summary><b>WindowFilter</b> - The filter of window processes to which the shortcuts apply to</summary>
See the `WindowFilter` field in the manifest file for more information.
</details>
<details>
<summary><b>BackgroundProcess</b> - Optionally allows applying WindowFilter to background processes</summary>
**Optional field**
See the `BackgroundProcess` field in the manifest file for more information.
</details>
<details>
<summary><b>Apps</b> - List of all the package identifiers applying for the filter</summary>
</details>
#### 3.2.1 Example
```yaml
DefaultShellName: "+WindowsNT.Shell"
Index:
- Filter: "*"
BackgroundProcess: True
Apps: ["+WindowsNT.Shell", "Microsoft.PowerToys"]
- Filter: "explorer.exe"
Apps: ["+WindowsNT.WindowsExplorer"]
- Filter: "taskmgr.exe"
Apps: ["+WindowsNT.TaskManager"]
- Filter: "msedge.exe"
Apps: ["+WindowsNT.Edge"]
```

View File

@@ -4,7 +4,6 @@
#include <ProjectTelemetry.h>
#include <spdlog/sinks/base_sink.h>
#include <filesystem>
#include <fstream>
#include <string_view>
#include "../../src/common/logger/logger.h"
@@ -640,7 +639,7 @@ UINT __stdcall InstallPackageIdentityMSIXCA(MSIHANDLE hInstall)
try
{
std::wstring externalLocation = installFolderPath + L"WinUI3Apps\\"; // External content location (WinUI3Apps subfolder to isolate DACL changes from preview handler DLLs)
std::wstring externalLocation = installFolderPath; // External content location (PowerToys install folder)
Uri externalUri{ externalLocation }; // External location URI for sparse package content
Uri packageUri{ msixPath }; // The MSIX file URI
@@ -1808,223 +1807,6 @@ void initSystemLogger()
} });
}
// Naming note: the *Hardlinks* names in this CA, the matching WiX CustomAction Ids
// in Product.wxs, and the manifest filename "hardlinks.txt" are kept for continuity
// with the original PR design. The implementation uses fs::copy_file -- not
// CreateHardLinkW -- because hard-links share an inode (and DACL) between root and
// WinUI3Apps, which lets MSIX sparse-package registration propagate a rich DACL onto
// the root copy of files like hostfxr.dll and break LOW-IL prevhost.exe loads,
// turning the Monaco preview pane blank. Copies create a fresh inode in WinUI3Apps so the root
// copy keeps its simple DACL. See the in-body comment for the full RCA reference.
UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder;
hr = WcaInitialize(hInstall, "CreateWinAppSDKHardlinks");
ExitOnFailure(hr, "Failed to initialize");
hr = getInstallFolder(hInstall, installationFolder);
ExitOnFailure(hr, "Failed to get installFolder.");
{
namespace fs = std::filesystem;
const fs::path installDir(installationFolder);
const fs::path winui3Dir = installDir / L"WinUI3Apps";
const fs::path manifestPath = winui3Dir / L"hardlinks.txt";
if (!fs::exists(manifestPath))
{
WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: No hardlinks.txt manifest found, skipping.");
goto LExit;
}
std::ifstream manifestFile(manifestPath); // Read as bytes, then convert UTF-8 -> wide explicitly.
std::string narrowLine;
int created = 0;
int failed = 0;
// INSTALLFOLDER from MSI typically arrives with a trailing backslash. lexically_normal
// preserves that as an empty trailing path component, which would later make the
// per-component std::mismatch containment check below reject every legitimate entry.
// Strip any trailing separators before normalizing.
auto stripTrailingSep = [](fs::path p) {
auto s = p.native();
while (s.size() > 1 && (s.back() == L'\\' || s.back() == L'/')) s.pop_back();
return fs::path(s);
};
// Normalize once so the per-line containment check below is cheap.
const fs::path installDirNorm = stripTrailingSep(installDir).lexically_normal();
const fs::path winui3DirNorm = stripTrailingSep(winui3Dir).lexically_normal();
while (std::getline(manifestFile, narrowLine))
{
if (narrowLine.empty())
{
continue;
}
// Strip CR if the manifest uses CRLF line endings.
if (narrowLine.back() == '\r')
{
narrowLine.pop_back();
if (narrowLine.empty()) continue;
}
// Manifest is written as UTF-8 (no BOM) -- convert to wide string explicitly
// rather than relying on the locale-default codecvt of std::wifstream, which is
// the ANSI code page on Windows and would silently mangle any non-ASCII path.
const int wideLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, nullptr, 0);
if (wideLen <= 0)
{
WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Skipping non-UTF-8 entry: %hs", narrowLine.c_str());
failed++;
continue;
}
std::wstring fileName(static_cast<size_t>(wideLen) - 1, L'\0');
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, fileName.data(), wideLen);
// Defense-in-depth: reject manifest entries that would escape the install root
// via "..", absolute paths, or alternate stream syntax. lexically_normal collapses
// any "." / ".." / repeated separators, then std::mismatch verifies the resolved
// path is still rooted at installDir / winui3Dir respectively.
const fs::path source = (installDir / fileName).lexically_normal();
const fs::path target = (winui3Dir / fileName).lexically_normal();
const auto sourceIn = std::mismatch(installDirNorm.begin(), installDirNorm.end(), source.begin(), source.end());
const auto targetIn = std::mismatch(winui3DirNorm.begin(), winui3DirNorm.end(), target.begin(), target.end());
if (sourceIn.first != installDirNorm.end() || targetIn.first != winui3DirNorm.end())
{
WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Rejecting entry outside install root: %ls", fileName.c_str());
failed++;
continue;
}
if (!fs::exists(source))
{
WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Source not found: %ls", source.c_str());
failed++;
continue;
}
// Remove existing file if present (leftover from previous install)
std::error_code ec;
fs::remove(target, ec);
// Use a regular file copy (not a hard-link). Hard-links share an
// NTFS inode -- and therefore one DACL -- between root and
// WinUI3Apps, which lets MSIX sparse-package registration
// propagate the WinUI3Apps parent's rich (Capability/Package SID)
// DACL onto the root path. That trips a kernel "stricter access
// evaluation" path that blocks LOW-IL prevhost.exe from loading
// hostfxr.dll, so File Explorer Monaco preview goes blank on
// Windows 11 23H2. Copying creates a fresh inode in WinUI3Apps,
// so the root copy keeps its simple DACL while the WinUI3Apps
// copy inherits the rich DACL from its parent (matches 0.99.1
// behaviour). See Documents\PR-47233-Handoff.md for full RCA.
fs::copy_file(source, target, fs::copy_options::overwrite_existing, ec);
if (ec)
{
WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Failed to copy: %ls (%hs)", fileName.c_str(), ec.message().c_str());
failed++;
}
else
{
created++;
}
}
WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Copied %d files, %d failures", created, failed);
// Catastrophic-case escalation: if every copy failed, the WinUI3Apps tree is
// unusable (Monaco preview / context-menu shells will break). Surface this rather
// than reporting install success. Per-file failures remain tolerated.
if (created == 0 && failed > 0)
{
hr = E_FAIL;
ExitOnFailure(hr, "All WinAppSDK file copies failed; aborting install.");
}
}
LExit:
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder;
hr = WcaInitialize(hInstall, "DeleteWinAppSDKHardlinks");
ExitOnFailure(hr, "Failed to initialize");
hr = getInstallFolder(hInstall, installationFolder);
ExitOnFailure(hr, "Failed to get installFolder.");
{
namespace fs = std::filesystem;
const fs::path winui3Dir = fs::path(installationFolder) / L"WinUI3Apps";
const fs::path manifestPath = winui3Dir / L"hardlinks.txt";
if (!fs::exists(manifestPath))
{
goto LExit;
}
std::ifstream manifestFile(manifestPath); // Read as bytes; convert UTF-8 -> wide explicitly.
std::string narrowLine;
// INSTALLFOLDER from MSI typically arrives with a trailing backslash; strip it before
// normalizing so the per-line containment check doesn't false-reject every entry.
auto stripTrailingSep = [](fs::path p) {
auto s = p.native();
while (s.size() > 1 && (s.back() == L'\\' || s.back() == L'/')) s.pop_back();
return fs::path(s);
};
const fs::path winui3DirNorm = stripTrailingSep(winui3Dir).lexically_normal();
while (std::getline(manifestFile, narrowLine))
{
if (narrowLine.empty())
{
continue;
}
if (narrowLine.back() == '\r')
{
narrowLine.pop_back();
if (narrowLine.empty()) continue;
}
const int wideLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, nullptr, 0);
if (wideLen <= 0)
{
WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Skipping non-UTF-8 entry: %hs", narrowLine.c_str());
continue;
}
std::wstring fileName(static_cast<size_t>(wideLen) - 1, L'\0');
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, fileName.data(), wideLen);
// Defense-in-depth: reject entries whose resolved target escapes WinUI3Apps.
const fs::path target = (winui3Dir / fileName).lexically_normal();
const auto inWinui3 = std::mismatch(winui3DirNorm.begin(), winui3DirNorm.end(), target.begin(), target.end());
if (inWinui3.first != winui3DirNorm.end())
{
WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Rejecting entry outside WinUI3Apps: %ls", fileName.c_str());
continue;
}
std::error_code ec;
fs::remove(target, ec);
}
WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Cleaned up deduplicated copy files");
}
LExit:
er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
return WcaFinalize(er);
}
// DllMain - Initialize and cleanup WiX custom action utils.
extern "C" BOOL WINAPI DllMain(__in HINSTANCE hInst, __in ULONG ulReason, __in LPVOID)
{

View File

@@ -36,5 +36,3 @@ EXPORTS
SetBundleInstallLocationCA
InstallPackageIdentityMSIXCA
UninstallPackageIdentityMSIXCA
CreateWinAppSDKHardlinksCA
DeleteWinAppSDKHardlinksCA

View File

@@ -6,16 +6,13 @@
<?define BaseApplicationsFilesPath=$(var.BinDir)\?>
<Fragment>
<!-- winmd must be in WinUI3Apps (ExternalLocation) for WinRT COM proxy/stub resolution -->
<DirectoryRef Id="WinUI3AppsInstallFolder">
<DirectoryRef Id="INSTALLFOLDER">
<Component Id="Microsoft_CommandPalette_Extensions_winmd" Guid="304AD25A-A986-4058-940E-61DB79EBD78C" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="Microsoft_CommandPalette_Extensions_winmd" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="Microsoft.CommandPalette.Extensions.winmd" Source="$(var.BinDir)WinUI3Apps\Microsoft.CommandPalette.Extensions.winmd" />
<File Id="Microsoft.CommandPalette.Extensions.winmd" Source="$(var.BinDir)Microsoft.CommandPalette.Extensions.winmd" />
</Component>
</DirectoryRef>
<DirectoryRef Id="INSTALLFOLDER">
<!-- Generated by generateFileComponents.ps1 -->
<!--BaseApplicationsFiles_Component_Def-->
</DirectoryRef>

View File

@@ -67,11 +67,8 @@
<RegistryValue Type="string" Name="svgs_icons" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="icon.ico" Source="$(var.BinDir)svgs\icon.ico" />
<File Id="iconUpdate.ico" Source="$(var.BinDir)svgs\iconUpdate.ico" />
<File Id="PowerToysWhite.ico" Source="$(var.BinDir)svgs\PowerToysWhite.ico" />
<File Id="PowerToysWhiteUpdate.ico" Source="$(var.BinDir)svgs\PowerToysWhiteUpdate.ico" />
<File Id="PowerToysDark.ico" Source="$(var.BinDir)svgs\PowerToysDark.ico" />
<File Id="PowerToysDarkUpdate.ico" Source="$(var.BinDir)svgs\PowerToysDarkUpdate.ico" />
</Component>
</Directory>
</DirectoryRef>

View File

@@ -4,11 +4,11 @@
<?define KeyboardManagerAssetsFiles=?>
<?define KeyboardManagerAssetsWinUI3Files=?>
<?define KeyboardManagerAssetsFilesPath=$(var.BinDir)\WinUI3Apps\Assets\KeyboardManager\?>
<?define KeyboardManagerAssetsFilesPath=$(var.BinDir)\Assets\KeyboardManager\?>
<?define KeyboardManagerAssetsWinUI3FilesPath=$(var.BinDir)\WinUI3Apps\Assets\KeyboardManagerEditor\?>
<Fragment>
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<DirectoryRef Id="BaseApplicationsAssetsFolder">
<Directory Id="KeyboardManagerAssetsInstallFolder" Name="KeyboardManager" />
</DirectoryRef>
<DirectoryRef Id="WinUI3AppsAssetsFolder">

View File

@@ -112,8 +112,6 @@
<Custom Action="SetInstallCmdPalPackageParam" Before="InstallCmdPalPackage" />
<Custom Action="SetUninstallCommandNotFoundParam" Before="UninstallCommandNotFound" />
<Custom Action="SetUpgradeCommandNotFoundParam" Before="UpgradeCommandNotFound" />
<Custom Action="SetCreateWinAppSDKHardlinksParam" Before="CreateWinAppSDKHardlinks" />
<Custom Action="SetDeleteWinAppSDKHardlinksParam" Before="DeleteWinAppSDKHardlinks" />
<Custom Action="SetApplyModulesRegistryChangeSetsParam" Before="ApplyModulesRegistryChangeSets" />
<Custom Action="SetInstallPackageIdentityMSIXParam" Before="InstallPackageIdentityMSIX" />
@@ -126,7 +124,6 @@
<Custom Action="SetBundleInstallLocationData" Before="SetBundleInstallLocation" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
<Custom Action="SetBundleInstallLocation" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED" />
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="CreateWinAppSDKHardlinks" After="InstallFiles" Condition="NOT Installed OR WIX_UPGRADE_DETECTED OR REINSTALL" />
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallPackageIdentityMSIX" After="InstallFiles" Condition="NOT Installed AND WINDOWSBUILDNUMBER &gt;= 22000" />
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
@@ -140,7 +137,6 @@
<?endif?>
<Custom Action="TelemetryLogInstallSuccess" After="InstallFinalize" Condition="NOT Installed" />
<Custom Action="TelemetryLogUninstallSuccess" After="InstallFinalize" Condition="Installed and (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="DeleteWinAppSDKHardlinks" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UnApplyModulesRegistryChangeSets" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="UnRegisterContextMenuPackages" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
<Custom Action="CleanImageResizerRuntimeRegistry" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot;)" />
@@ -193,10 +189,8 @@
<CustomAction Id="SetUpgradeCommandNotFoundParam" Property="UpgradeCommandNotFound" Value="[INSTALLFOLDER]" />
<CustomAction Id="SetCreateWinAppSDKHardlinksParam" Property="CreateWinAppSDKHardlinks" Value="[INSTALLFOLDER]" />
<CustomAction Id="CreateWinAppSDKHardlinks" Return="check" Impersonate="yes" Execute="deferred" DllEntry="CreateWinAppSDKHardlinksCA" BinaryRef="PTCustomActions" />
<CustomAction Id="SetDeleteWinAppSDKHardlinksParam" Property="DeleteWinAppSDKHardlinks" Value="[INSTALLFOLDER]" />
<CustomAction Id="DeleteWinAppSDKHardlinks" Return="ignore" Impersonate="yes" Execute="deferred" DllEntry="DeleteWinAppSDKHardlinksCA" BinaryRef="PTCustomActions" />
<CustomAction Id="SetCreatePTInteropHardlinksParam" Property="CreatePTInteropHardlinks" Value="[INSTALLFOLDER]" />

View File

@@ -9,7 +9,7 @@
<Fragment>
<!-- Resource directories should be added only if the installer is built on the build farm -->
<?ifdef env.IsPipeline?>
<?foreach ParentDirectory in INSTALLFOLDER;WinUI3AppsInstallFolder;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
<?foreach ParentDirectory in INSTALLFOLDER;HistoryPluginFolder;CalculatorPluginFolder;FolderPluginFolder;ProgramPluginFolder;ShellPluginFolder;IndexerPluginFolder;UnitConverterPluginFolder;ValueGeneratorPluginFolder;UriPluginFolder;WindowWalkerPluginFolder;OneNotePluginFolder;RegistryPluginFolder;VSCodeWorkspacesPluginFolder;ServicePluginFolder;SystemPluginFolder;TimeDatePluginFolder;WindowsSettingsPluginFolder;WindowsTerminalPluginFolder;WebSearchPluginFolder;PowerToysPluginFolder?>
<DirectoryRef Id="$(var.ParentDirectory)">
<!-- Resource file directories -->
<?foreach Language in $(var.LocLanguageList)?>
@@ -361,11 +361,11 @@
</RegistryKey>
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
</Component>
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" Guid="$(var.CompGUIDPrefix)23">
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)23">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\WinUI3Apps\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
</Component>
<?undef IdSafeLanguage?>
<?undef CompGUIDPrefix?>
@@ -433,7 +433,6 @@
<?define IdSafeLanguage = $(var.Language)?>
<?endif?>
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)INSTALLFOLDER" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" On="uninstall" />
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" Directory="Resource$(var.IdSafeLanguage)WinUI3AppsInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)CalculatorPluginFolder" Directory="Resource$(var.IdSafeLanguage)CalculatorPluginFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)FolderPluginFolder" Directory="Resource$(var.IdSafeLanguage)FolderPluginFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderResourcesResource$(var.IdSafeLanguage)ProgramPluginFolder" Directory="Resource$(var.IdSafeLanguage)ProgramPluginFolder" On="uninstall" />

View File

@@ -17,9 +17,6 @@
<?define SettingsV2IconsModelsFiles=?>
<?define SettingsV2IconsModelsFilesPath=$(var.BinDir)WinUI3Apps\Assets\Settings\Icons\Models\?>
<?define SettingsV2AssetsCmdPalFiles=?>
<?define SettingsV2AssetsCmdPalFilesPath=$(var.BinDir)WinUI3Apps\Assets\Settings\CmdPal\?>
<Fragment>
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<Directory Id="SettingsV2AssetsInstallFolder" Name="Settings">
@@ -30,7 +27,6 @@
<Directory Id="SettingsV2AssetsModulesInstallFolder" Name="Modules">
<Directory Id="SettingsV2OOBEAssetsModulesInstallFolder" Name="OOBE" />
</Directory>
<Directory Id="SettingsV2AssetsCmdPalInstallFolder" Name="CmdPal" />
</Directory>
</DirectoryRef>
@@ -59,11 +55,6 @@
<!--SettingsV2IconsModelsFiles_Component_Def-->
</DirectoryRef>
<DirectoryRef Id="SettingsV2AssetsCmdPalInstallFolder" FileSource="$(var.SettingsV2AssetsCmdPalFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--SettingsV2AssetsCmdPalFiles_Component_Def-->
</DirectoryRef>
<DirectoryRef Id="SettingsAppAssetsScriptsFolder" FileSource="$(var.SettingsV2AssetsFilesPath)\Scripts\">
<Component Id="CommandNotFound_Scripts" Guid="898EFA1E-EDD3-4F4B-8C7F-4A14B0D05B02" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
@@ -89,7 +80,6 @@
<RemoveFolder Id="RemoveFolderSettingsV2IconsModelsInstallFolder" Directory="SettingsV2IconsModelsInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderSettingsV2AssetsModulesInstallFolder" Directory="SettingsV2AssetsModulesInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderSettingsV2OOBEAssetsModulesInstallFolder" Directory="SettingsV2OOBEAssetsModulesInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderSettingsV2AssetsCmdPalInstallFolder" Directory="SettingsV2AssetsCmdPalInstallFolder" On="uninstall" />
<RemoveFolder Id="RemoveFolderSettingsAppAssetsScriptsFolder" Directory="SettingsAppAssetsScriptsFolder" On="uninstall" />
</Component>
<ComponentRef Id="CommandNotFound_Scripts" />

View File

@@ -2,25 +2,26 @@
<?include $(sys.CURRENTDIR)\Common.wxi?>
<?define ShortcutGuideAssetsFiles=?>
<?define ShortcutGuideAssetsFilesPath=$(var.BinDir)WinUI3Apps\Assets\ShortcutGuide\?>
<?define ShortcutGuideSvgFiles=?>
<?define ShortcutGuideSvgFilesPath=$(var.BinDir)\Assets\ShortcutGuide\?>
<Fragment>
<DirectoryRef Id="WinUI3AppsAssetsFolder">
<Directory Id="ShortcutGuideAssetsFolder" Name="ShortcutGuide" />
<!-- Shortcut guide files -->
<DirectoryRef Id="BaseApplicationsAssetsFolder">
<Directory Id="ShortcutGuideSvgsInstallFolder" Name="ShortcutGuide" />
</DirectoryRef>
<DirectoryRef Id="ShortcutGuideAssetsFolder" FileSource="$(var.ShortcutGuideAssetsFilesPath)">
<DirectoryRef Id="ShortcutGuideSvgsInstallFolder" FileSource="$(var.ShortcutGuideSvgFilesPath)">
<!-- Generated by generateFileComponents.ps1 -->
<!--ShortcutGuideAssetsFiles_Component_Def-->
<!--ShortcutGuideSvgFiles_Component_Def-->
</DirectoryRef>
<!-- Shortcut guide -->
<ComponentGroup Id="ShortcutGuideComponentGroup" >
<Component Id="RemoveShortcutGuideFolder" Guid="AD1ABC55-B593-4A60-A86A-BA8C0ED493A5" Directory="ShortcutGuideAssetsFolder" >
<ComponentGroup Id="ShortcutGuideComponentGroup">
<Component Id="RemoveShortcutGuideFolder" Guid="AD1ABC55-B593-4A60-A86A-BA8C0ED493A5" Directory="ShortcutGuideSvgsInstallFolder">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="RemoveShortcutGuideFolder" Value="" KeyPath="yes"/>
<RegistryValue Type="string" Name="RemoveShortcutGuideFolder" Value="" KeyPath="yes" />
</RegistryKey>
<RemoveFolder Id="RemoveFolderShortcutGuideAssetsInstallFolder" Directory="ShortcutGuideAssetsFolder" On="uninstall"/>
<RemoveFolder Id="RemoveFolderShortcutGuideSvgsInstallFolder" Directory="ShortcutGuideSvgsInstallFolder" On="uninstall" />
</Component>
</ComponentGroup>

View File

@@ -7,18 +7,11 @@
<Fragment>
<DirectoryRef Id="WinUI3AppsInstallFolder">
<Component Id="WinUI3Apps_Hardlinks_Manifest" Guid="F7A2C3D1-8E4B-4F6A-9D2E-1B3C5A7F8E90" Bitness="always64">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="WinUI3Apps_Hardlinks_Manifest" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="WinUI3Apps_hardlinks_txt" Source="$(var.BinDir)\WinUI3Apps\hardlinks.txt" />
</Component>
<!-- Generated by generateFileComponents.ps1 -->
<!--WinUI3ApplicationsFiles_Component_Def-->
</DirectoryRef>
<ComponentGroup Id="WinUI3ApplicationsComponentGroup">
<ComponentRef Id="WinUI3Apps_Hardlinks_Manifest" />
</ComponentGroup>
</Fragment>

View File

@@ -30,10 +30,6 @@ Function Generate-FileList() {
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri")
# MFC DLLs leak into the output via WindowsAppSDKSelfContained but no PowerToys binary imports them.
# Verified with dumpbin /dependents across all 2176 binaries — zero consumers.
$fileExclusionList += @("mfc140.dll", "mfc140u.dll", "mfcm140.dll", "mfcm140u.dll")
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")
if ($fileDepsJson -eq [string]::Empty) {
@@ -89,16 +85,11 @@ Function Generate-FileComponents() {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'fileList',
Justification = 'variable is used in another scope')]
$fileList = $matches[2] -split ';' | Where-Object { $_ -ne '' }
$fileList = $matches[2] -split ';'
return
}
}
if ($null -eq $fileList -or $fileList.Count -eq 0) {
# No files to generate components for — leave placeholder intact
return
}
$componentId = "$($fileListName)_Component"
$componentDefs = "`r`n"
@@ -163,67 +154,6 @@ Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSSc
#WinUI3Applications
Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
# Deduplicate: Remove files from WinUI3Apps that are identical to root (same name + same hash).
# These will be re-created as plain file copies at install time by CreateWinAppSDKHardlinksCA.
# (The CA's name is historical: it now uses fs::copy_file rather than CreateHardLinkW to avoid
# DACL contamination across the shared inode -- see CustomAction.cpp for details.)
$rootPath = "$PSScriptRoot..\..\..\$platform\Release"
$winui3Path = "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps"
$winui3WxsPath = "$PSScriptRoot\WinUI3Applications.wxs"
$winui3Wxs = Get-Content $winui3WxsPath -Raw
$manifestPath = Join-Path $winui3Path "hardlinks.txt"
if ($winui3Wxs -match "\<\?define WinUI3ApplicationsFiles=([^?]*)\?\>") {
$winui3FileList = $matches[1] -split ';' | Where-Object { $_ -ne '' }
$hardlinkFiles = @()
# Read the BaseApplications WXS file list so we only deduplicate files that the MSI
# is actually deploying to the install root. If a file was stripped from BaseApplications
# by an earlier step (e.g., the ImageResizer leaked-apphost workaround above), the
# install-time CA's source would be missing and both copies would disappear.
$baseAppsWxs = Get-Content $baseAppWxsPath -Raw
$baseAppsFileList = @()
if ($baseAppsWxs -match "\<\?define BaseApplicationsFiles=([^?]*)\?\>") {
$baseAppsFileList = $matches[1] -split ';' | Where-Object { $_ -ne '' }
}
foreach ($file in $winui3FileList) {
# Skip files that were intentionally not deployed to root by the build
if ($baseAppsFileList -notcontains $file) { continue }
$rootFile = Join-Path $rootPath $file
$winui3File = Join-Path $winui3Path $file
if ((Test-Path $rootFile) -and (Test-Path $winui3File)) {
$rootHash = (Get-FileHash $rootFile -Algorithm SHA256).Hash
$winui3Hash = (Get-FileHash $winui3File -Algorithm SHA256).Hash
if ($rootHash -eq $winui3Hash) {
$hardlinkFiles += $file
}
}
}
if ($hardlinkFiles.Count -gt 0) {
# Remove deduplicated files from WinUI3Apps file list
$remainingFiles = $winui3FileList | Where-Object { $_ -notin $hardlinkFiles }
if ($remainingFiles.Count -eq 0) {
# All files are duplicates — keep at least a dummy entry won't be emitted
# Generate-FileComponents handles empty defines by producing no <File> entries
$winui3Wxs = $winui3Wxs -replace "\<\?define WinUI3ApplicationsFiles=[^?]*\?\>", "<?define WinUI3ApplicationsFiles=?>"
} else {
$winui3Wxs = $winui3Wxs -replace "\<\?define WinUI3ApplicationsFiles=[^?]*\?\>", "<?define WinUI3ApplicationsFiles=$($remainingFiles -join ';')?>"
}
Set-Content -Path $winui3WxsPath -Value $winui3Wxs
Write-Host "Deduplicated $($hardlinkFiles.Count) files from WinUI3Apps (will be copied at install time)"
}
# Always write hardlinks.txt (may be empty — CA handles that gracefully)
# Write as UTF-8 without BOM so the install-time CA can read it via std::ifstream
# + MultiByteToWideChar(CP_UTF8) without dealing with PS-version-dependent default
# encodings or a leading BOM.
[System.IO.File]::WriteAllLines($manifestPath, [string[]]$hardlinkFiles, (New-Object System.Text.UTF8Encoding($false)))
}
Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs
#AdvancedPaste
@@ -261,7 +191,7 @@ Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFil
Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs
#KeyboardManager
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsFiles -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\KeyboardManager"
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsFiles -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\KeyboardManager"
Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsWinUI3Files -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\KeyboardManagerEditor"
Generate-FileComponents -fileListName "KeyboardManagerAssetsFiles" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs
Generate-FileComponents -fileListName "KeyboardManagerAssetsWinUI3Files" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs
@@ -397,8 +327,8 @@ Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePat
## Plugins
#ShortcutGuide
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideAssetsFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ShortcutGuide\"
Generate-FileComponents -fileListName "ShortcutGuideAssetsFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot
Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideSvgFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ShortcutGuide\"
Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs
#Settings
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\"
@@ -406,13 +336,11 @@ Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsModulesFiles -w
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\OOBE\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsFluentIconsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2IconsModelsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\Models\"
Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsCmdPalFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\CmdPal\"
Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
Generate-FileComponents -fileListName "SettingsV2IconsModelsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
Generate-FileComponents -fileListName "SettingsV2AssetsCmdPalFiles" -wxsFilePath $PSScriptRoot\Settings.wxs
#Workspaces
Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\"

View File

@@ -8,10 +8,10 @@ SET VCToolsVersion=!VCToolsVersion!
SET ClearDevCommandPromptEnvVars=false
rem In case of Release we should not use Debug CRT in VCRT forwarders
msbuild !PTRoot!\src\modules\previewpane\MonacoPreviewHandler\MonacoPreviewHandler.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml -p:TargetFramework=net10.0-windows10.0.26100.0
msbuild !PTRoot!\src\modules\previewpane\MonacoPreviewHandler\MonacoPreviewHandler.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml -p:TargetFramework=net9.0-windows10.0.26100.0
msbuild !PTRoot!\src\modules\previewpane\MarkdownPreviewHandler\MarkdownPreviewHandler.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml -p:TargetFramework=net10.0-windows10.0.26100.0
msbuild !PTRoot!\src\modules\previewpane\MarkdownPreviewHandler\MarkdownPreviewHandler.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml -p:TargetFramework=net9.0-windows10.0.26100.0
msbuild !PTRoot!\src\modules\previewpane\SvgPreviewHandler\SvgPreviewHandler.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml -p:TargetFramework=net10.0-windows10.0.26100.0
msbuild !PTRoot!\src\modules\previewpane\SvgPreviewHandler\SvgPreviewHandler.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml -p:TargetFramework=net9.0-windows10.0.26100.0
msbuild !PTRoot!\src\modules\previewpane\SvgThumbnailProvider\SvgThumbnailProvider.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml -p:TargetFramework=net10.0-windows10.0.26100.0
msbuild !PTRoot!\src\modules\previewpane\SvgThumbnailProvider\SvgThumbnailProvider.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml -p:TargetFramework=net9.0-windows10.0.26100.0

View File

@@ -62,7 +62,7 @@
<Import Project="$(RepoRoot)deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -70,6 +70,6 @@
</PropertyGroup>
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@@ -4,7 +4,7 @@
<Import Project=".\Common.Dotnet.PrepareGeneratedFolder.targets" />
<PropertyGroup>
<CoreTargetFramework>net10.0</CoreTargetFramework>
<CoreTargetFramework>net9.0</CoreTargetFramework>
<WindowsSdkPackageVersion>10.0.26100.68-preview</WindowsSdkPackageVersion>
<TargetFramework>$(CoreTargetFramework)-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
@@ -12,17 +12,6 @@
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
</PropertyGroup>
<!--
Opt out of CsWinRT 2.2 IIDOptimizer. On .NET 10 / CsWinRT 2.2 the tool exits with code -1
after producing "0 IID calculations/fetches patched", which generates a noisy MSB3073
warning for every CsWinRT-consuming project. The IIDOptimizer is a runtime-perf optimization
that interns GUID lookups; disabling it just means a small first-call cost. This switch
causes Microsoft.Windows.CsWinRT.IIDOptimizer.targets to not be imported at all.
-->
<PropertyGroup>
<CsWinRTIIDOptimizerOptOut>true</CsWinRTIIDOptimizerOptOut>
</PropertyGroup>
<!-- Common from the debug / release items -->
<PropertyGroup>
<WarningLevel>4</WarningLevel>
@@ -52,54 +41,7 @@
<!-- this may need to be removed on future CsWinRT upgrades-->
<Target Name="RemoveCsWinRTPackageAnalyzer" BeforeTargets="CoreCompile">
<ItemGroup>
<Analyzer Remove="@(Analyzer)" Condition="%(Analyzer.NuGetPackageId) == 'Microsoft.Windows.CsWinRT'" />
<Analyzer Remove="@(Analyzer)" Condition="%(Analyzer.NuGetPackageId) == 'Microsoft.Windows.CsWinRT'" />
</ItemGroup>
</Target>
<!--
Ensure any referenced C++/WinRT (.vcxproj) projects are fully built BEFORE the CsWinRT
source generator runs in this csproj. On a clean machine the SDK-style ProjectReference
graph does not guarantee that the producing vcxproj has emitted its .winmd before the
consuming C# Compile / source-generator stage starts in a parallel solution build,
which manifests as CS0246 on the projected namespace (e.g. 'PowerToys.Interop').
Forcing a serialized Build of the .vcxproj references here closes that race.
We hook BEFORE ResolveProjectReferences so the produced .winmd is visible to
CsWinRTRemoveWinMDReferences (which moves it into @(CsWinRTInputs)) and we also
delete a possibly stale cswinrt.rsp so CsWinRTGenerateProjection re-invokes
cswinrt.exe instead of incrementally skipping.
-->
<Target Name="EnsureNativeWinMDProjectionInputsBuilt"
BeforeTargets="ResolveProjectReferences;ResolveAssemblyReferences;CsWinRTPrepareProjection;CsWinRTGenerateProjection"
Condition="'@(ProjectReference)' != '' and '$(DesignTimeBuild)' != 'true' and '$(BuildingProject)' != 'false'">
<ItemGroup>
<_NativeWinMDProjectionRef Include="@(ProjectReference)" Condition="'%(Extension)' == '.vcxproj'" />
</ItemGroup>
<MSBuild Projects="@(_NativeWinMDProjectionRef)"
Properties="Configuration=$(Configuration);Platform=$(Platform)"
Targets="Build"
BuildInParallel="false"
Condition="'@(_NativeWinMDProjectionRef)' != ''" />
<!-- Force CsWinRTGenerateProjection to re-run so stale-rsp incremental skip cannot
leave us without generated .cs files when the .winmd has just been (re)produced. -->
<Delete Files="$(CsWinRTGeneratedFilesDir)cswinrt.rsp;$(CsWinRTGeneratedFilesDir)cswinrt_internal.rsp"
Condition="'@(_NativeWinMDProjectionRef)' != '' and '$(CsWinRTGeneratedFilesDir)' != ''" />
<!-- Mark that we need to delete the rsp again once CsWinRTGeneratedFilesDir is fully resolved
(some projects set it to $(OutDir) which is not evaluated this early). -->
<PropertyGroup>
<_DeleteStaleCsWinRTRspPending>true</_DeleteStaleCsWinRTRspPending>
</PropertyGroup>
</Target>
<!--
Second pass: after CsWinRTPrepareProjection has resolved $(CsWinRTGeneratedFilesDir) to its
final value (which may depend on $(OutDir)), delete any stale cswinrt.rsp so the
CsWinRTGenerateProjection target's incremental-skip cannot leave us without generated .cs files.
-->
<Target Name="DeleteStaleCsWinRTRspAfterPrepare"
AfterTargets="CsWinRTPrepareProjection"
BeforeTargets="CsWinRTGenerateProjection"
Condition="'$(_DeleteStaleCsWinRTRspPending)' == 'true' and '$(CsWinRTGeneratedFilesDir)' != ''">
<Delete Files="$(CsWinRTGeneratedFilesDir)cswinrt.rsp;$(CsWinRTGeneratedFilesDir)cswinrt_internal.rsp" />
</Target>
</Project>

View File

@@ -38,7 +38,17 @@
</Capabilities>
<Applications>
<Application Id="PowerToys.SettingsUI" Executable="PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
<Application Id="PowerToys.OCR" Executable="PowerToys.PowerOCR.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.OCR"
Description="PowerToys OCR Module"
BackgroundColor="transparent"
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png"
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.SettingsUI"
Description="PowerToys Settings UI"
@@ -48,7 +58,7 @@
AppListEntry="none">
</uap:VisualElements>
</Application>
<Application Id="PowerToys.ImageResizerUI" Executable="PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PowerToys.ImageResizer"
Description="PowerToys Image Resizer UI"

View File

@@ -417,7 +417,6 @@ if ($NoSign) {
Write-BuildLog "Identity Name: $($script:Config.IdentityName)" -Level Info
}
$winUI3AppsDir = Join-Path $outDir "WinUI3Apps"
Write-BuildLog "Register sparse package:" -Level Info
Write-BuildLog " Add-AppxPackage -Path `"$msixPath`" -ExternalLocation `"$winUI3AppsDir`"" -Level Warning
Write-BuildLog "(If already installed and you changed manifest only): Add-AppxPackage -Register `"$manifestPath`" -ExternalLocation `"$winUI3AppsDir`" -ForceApplicationShutdown" -Level Warning
Write-BuildLog " Add-AppxPackage -Path `"$msixPath`" -ExternalLocation `"$outDir`"" -Level Warning
Write-BuildLog "(If already installed and you changed manifest only): Add-AppxPackage -Register `"$manifestPath`" -ExternalLocation `"$outDir`" -ForceApplicationShutdown" -Level Warning

View File

@@ -4,9 +4,9 @@ This document describes how to build, sign, register, and consume the shared spa
## Package overview
The sparse package lives under `src/PackageIdentity`. It produces a payload-free MSIX whose `Identity` matches `Microsoft.PowerToys.SparseApp`. The manifest contains one entry per Win32 surface that should run with identity (for example Settings, Image Resizer, CmdPal Extension).
The sparse package lives under `src/PackageIdentity`. It produces a payload-free MSIX whose `Identity` matches `Microsoft.PowerToys.SparseApp`. The manifest contains one entry per Win32 surface that should run with identity (for example Settings, PowerOCR, Image Resizer).
> The MSIX contains only metadata. When the package is registered you must point `-ExternalLocation` to the `WinUI3Apps` subfolder of the output folder that hosts the Win32 binaries (for example `x64\Release\WinUI3Apps`). This isolates the DACL changes that MSIX registration applies on Windows 23H2/24H2 to the `WinUI3Apps` folder, keeping the root install folder clean for preview handler DLLs.
> The MSIX contains only metadata. When the package is registered you must point `-ExternalLocation` to the output folder that hosts the Win32 binaries (for example `x64\Release`).
## Building the sparse package locally
@@ -53,17 +53,16 @@ After `PowerToysSparse.msix` is generated:
# First time registration
$repoRoot = "C:/git/PowerToys"
$outputRoot = Join-Path $repoRoot "x64/Release"
$externalLocation = Join-Path $outputRoot "WinUI3Apps"
Add-AppxPackage -Path (Join-Path $outputRoot "PowerToysSparse.msix") -ExternalLocation $externalLocation
Add-AppxPackage -Path (Join-Path $outputRoot "PowerToysSparse.msix") -ExternalLocation $outputRoot
# Re-register after manifest tweaks only
Add-AppxPackage -Register (Join-Path $repoRoot "src/PackageIdentity/AppxManifest.xml") -ExternalLocation $externalLocation -ForceApplicationShutdown
Add-AppxPackage -Register (Join-Path $repoRoot "src/PackageIdentity/AppxManifest.xml") -ExternalLocation $outputRoot -ForceApplicationShutdown
# Remove the sparse identity
Get-AppxPackage -Name Microsoft.PowerToys.SparseApp | Remove-AppxPackage
```
`-ExternalLocation` should match the `WinUI3Apps` subfolder that contains the Win32 executables declared in the manifest. Re-run registration whenever the manifest or executable layout changes.
`-ExternalLocation` should match the output folder that contains the Win32 executables declared in the manifest. Re-run registration whenever the manifest or executable layout changes.
## CI-specific guidance
@@ -73,7 +72,7 @@ Get-AppxPackage -Name Microsoft.PowerToys.SparseApp | Remove-AppxPackage
## Consuming the identity from other components
1. Add a new `<Application>` entry inside `src/PackageIdentity/AppxManifest.xml`. Use a unique `Id` (for example `PowerToys.MyModuleUI`) and set `Executable` to the Win32 binary relative to the `-ExternalLocation` (`WinUI3Apps` subfolder).
1. Add a new `<Application>` entry inside `src/PackageIdentity/AppxManifest.xml`. Use a unique `Id` (for example `PowerToys.MyModuleUI`) and set `Executable` to the Win32 binary relative to the `-ExternalLocation` root.
2. Ensure the binary is copied into the platform/configuration output folder (`x64\Release`, `ARM64\Debug`, etc.) so the sparse package can locate it.
3. Embed a sparse identity manifest in the Win32 binary so it binds to the MSIX identity at runtime. The manifest must declare an `<msix>` element with `packageName="Microsoft.PowerToys.SparseApp"`, `applicationId` matching the `<Application Id>`, and a `publisher` that matches the sparse package. Keep the manifests publisher in sync with `src/PackageIdentity/.user/PowerToysSparse.publisher.txt` (emitted by `BuildSparsePackage.ps1`). See `src/modules/imageresizer/ui/ImageResizerUI.csproj` for an example that points `ApplicationManifest` to `ImageResizerUI.dev.manifest` for local builds and switches to `ImageResizerUI.prod.manifest` when `$(CIBuild)` is `true`.
4. Register or re-register the sparse package so Windows learns about the new application Id.

View File

@@ -14,8 +14,6 @@
#include <common/updating/updating.h>
#include <common/updating/updateState.h>
#include <common/updating/installer.h>
#include <common/updating/configBackup.h>
#include <common/updating/updateLifecycle.h>
#include <common/utils/elevation.h>
#include <common/utils/HttpClient.h>
@@ -23,8 +21,6 @@
#include <common/utils/resources.h>
#include <common/utils/timeutil.h>
#include <wil/resource.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/logger/logger.h>
@@ -40,59 +36,17 @@ using namespace cmdArg;
namespace fs = std::filesystem;
void CleanupStaleTempUpdaters()
{
// Remove orphaned PowerToys.Update.*.exe files from previous runs
try
{
std::error_code ec;
const auto tempDir = fs::temp_directory_path();
for (const auto& entry : fs::directory_iterator(tempDir, ec))
{
if (ec)
{
break;
}
if (!entry.is_regular_file())
{
continue;
}
const auto filename = entry.path().filename().wstring();
if (filename.starts_with(L"PowerToys.Update.") && filename.ends_with(L".exe"))
{
// Skip our own file (current PID)
const auto ownFilename = L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe";
if (filename == ownFilename)
{
continue;
}
fs::remove(entry.path(), ec);
// Failure to delete is expected if another updater is still running
}
}
}
catch (...)
{
// Best-effort cleanup; don't block the update
}
}
std::optional<fs::path> CopySelfToTempDir()
{
CleanupStaleTempUpdaters();
std::error_code error;
auto dst_path = fs::temp_directory_path() / (L"PowerToys.Update." + std::to_wstring(GetCurrentProcessId()) + L".exe");
auto dst_path = fs::temp_directory_path() / "PowerToys.Update.exe";
fs::copy_file(get_module_filename(), dst_path, fs::copy_options::overwrite_existing, error);
if (error)
{
return std::nullopt;
}
return dst_path;
return std::move(dst_path);
}
std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
@@ -103,9 +57,34 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
auto state = UpdateState::read();
// Handle readyToInstall first — the installer is already on disk,
// so we don't need a GitHub API call (which may fail if offline).
if (state.state == UpdateState::readyToInstall)
const auto new_version_info = std::move(get_github_version_info_async()).get();
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
else if (state.state == UpdateState::readyToInstall)
{
fs::path installer{ get_pending_updates_path() / state.downloadedInstallerFilename };
if (fs::is_regular_file(installer))
@@ -118,44 +97,12 @@ std::optional<fs::path> ObtainInstaller(bool& isUpToDate)
return std::nullopt;
}
}
if (state.state == UpdateState::upToDate)
else if (state.state == UpdateState::upToDate)
{
isUpToDate = true;
return std::nullopt;
}
const auto new_version_info = std::move(get_github_version_info_async()).get();
// Check for error BEFORE dereferencing — the old code crashed here
// when GitHub API was unreachable (new_version_info held an error string).
if (!new_version_info)
{
Logger::error(L"Couldn't obtain github version info: {}", new_version_info.error());
return std::nullopt;
}
if (std::holds_alternative<version_up_to_date>(*new_version_info))
{
isUpToDate = true;
Logger::error("Invoked with -update_now argument, but no update was available");
return std::nullopt;
}
if (state.state == UpdateState::readyToDownload || state.state == UpdateState::errorDownloading)
{
// Cleanup old updates before downloading the latest
updating::cleanup_updates();
auto downloaded_installer = std::move(download_new_version_async(std::get<new_version_download_info>(*new_version_info))).get();
if (!downloaded_installer)
{
Logger::error("Couldn't download new installer");
}
return downloaded_installer;
}
Logger::error("Invoked with -update_now argument, but update state was invalid");
return std::nullopt;
}
@@ -169,32 +116,13 @@ bool InstallNewVersionStage1(fs::path installer)
if (pt_main_window != nullptr)
{
// Get the process that owns the tray window so we can wait for it to exit
DWORD ptProcessId = 0;
GetWindowThreadProcessId(pt_main_window, &ptProcessId);
// Use SendMessageTimeoutW to avoid blocking indefinitely if the
// tray window thread is hung or unresponsive.
DWORD_PTR result = 0;
SendMessageTimeoutW(pt_main_window, WM_CLOSE, 0, 0, SMTO_ABORTIFHUNG, 5000, &result);
// Wait for PT to actually exit before launching installer.
// Without this, the installer may find PT files locked.
if (ptProcessId != 0)
{
wil::unique_handle ptProcess{ OpenProcess(SYNCHRONIZE, FALSE, ptProcessId) };
if (ptProcess)
{
WaitForSingleObject(ptProcess.get(), 10000); // 10 second timeout
}
}
SendMessageW(pt_main_window, WM_CLOSE, 0, 0);
}
// Pass the install directory so Stage 2 can relaunch PowerToys after install
const std::wstring installDir = get_module_folderpath();
std::wstring arguments = updating::BuildStage2Arguments(
UPDATE_NOW_LAUNCH_STAGE2, installer, fs::path(installDir));
std::wstring arguments{ UPDATE_NOW_LAUNCH_STAGE2 };
arguments += L" \"";
arguments += installer.c_str();
arguments += L"\"";
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = copy_in_temp->c_str();
@@ -262,16 +190,9 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
LPWSTR* args = CommandLineToArgvW(GetCommandLineW(), &nArgs);
if (!args || nArgs < 2)
{
if (args)
{
LocalFree(args);
}
return 1;
}
// D3 fix: ensure args is freed on all exit paths
auto freeArgs = wil::scope_exit([&] { LocalFree(args); });
std::wstring_view action{ args[1] };
std::filesystem::path logFilePath(PTSettingsHelper::get_root_save_folder_location());
@@ -280,11 +201,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
if (action == UPDATE_NOW_LAUNCH_STAGE1)
{
// Backup config files before the update to protect against corruption
Logger::info("Backing up config files before update");
auto backupResult = updating::BackupConfigFiles(fs::path(PTSettingsHelper::get_root_save_folder_location()));
Logger::info("Config backup complete: {} files backed up, {} errors", backupResult.filesBackedUp, backupResult.errors);
bool isUpToDate = false;
auto installerPath = ObtainInstaller(isUpToDate);
bool failed = !installerPath.has_value();
@@ -301,12 +217,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
}
else if (action == UPDATE_NOW_LAUNCH_STAGE2)
{
if (nArgs < 3)
{
Logger::error("Stage 2 invoked without installer path argument");
return 1;
}
using namespace std::string_view_literals;
const bool failed = !InstallNewVersionStage2(args[2]);
if (failed)
@@ -317,39 +227,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
state.state = UpdateState::errorDownloading;
});
}
// Always check for corrupted configs after Stage 2, regardless
// of install success/failure. A failed install may still corrupt configs.
Logger::info("Checking for corrupted config files after update");
auto restoreResult = updating::RestoreCorruptedConfigs(fs::path(PTSettingsHelper::get_root_save_folder_location()));
Logger::info("Config restore check complete: {}/{} files restored, {} errors",
restoreResult.filesRestored, restoreResult.filesChecked, restoreResult.errors);
if (!failed)
{
// Relaunch PowerToys from the install directory
if (updating::CanRelaunchAfterUpdate(nArgs))
{
std::wstring ptExePath = updating::BuildPowerToysExePath(args[3]);
Logger::info(L"Relaunching PowerToys after update: {}", ptExePath);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC };
sei.lpFile = ptExePath.c_str();
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = UPDATE_REPORT_SUCCESS;
if (!ShellExecuteExW(&sei))
{
Logger::error(L"Failed to relaunch PowerToys after update");
}
}
else
{
Logger::warn("Install directory not provided to Stage 2 - cannot relaunch PowerToys");
}
}
return failed;
}

View File

@@ -68,7 +68,7 @@
<Import Project="$(RepoRoot)deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -76,6 +76,6 @@
</PropertyGroup>
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

View File

@@ -36,12 +36,12 @@
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -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

View File

@@ -5,17 +5,11 @@
#include <sstream>
#include <cmath>
#include <limits>
#include <random>
namespace ExprtkCalculator::internal
{
static double factorial(const double n)
{
if (std::isnan(n) || std::isinf(n))
{
return std::numeric_limits<double>::quiet_NaN();
}
// Only allow non-negative integers
if (n < 0.0 || std::floor(n) != n)
{
@@ -26,80 +20,13 @@ namespace ExprtkCalculator::internal
static double sign(const double n)
{
// The sign of NaN is undefined.
if (std::isnan(n))
{
return std::numeric_limits<double>::quiet_NaN();
}
if (n > 0.0) return 1.0;
if (n < 0.0) return -1.0;
return 0.0;
}
// rand(): returns a uniformly distributed random double in [0, 1)
struct rand_func : public exprtk::ifunction<double>
{
std::mt19937_64 rng;
std::uniform_real_distribution<double> dist;
rand_func() :
exprtk::ifunction<double>(0),
rng(std::random_device{}()),
dist(0.0, 1.0)
{}
inline double operator()() override
{
return dist(rng);
}
};
// randi(n): returns a uniformly distributed random integer in [0, n-1]
struct randi_func : public exprtk::ifunction<double>
{
std::mt19937_64 rng;
randi_func() :
exprtk::ifunction<double>(1),
rng(std::random_device{}())
{}
inline double operator()(const double& n) override
{
if (std::isnan(n) || std::isinf(n))
{
return std::numeric_limits<double>::quiet_NaN();
}
constexpr double maxLongLongAsDouble = static_cast<double>(std::numeric_limits<long long>::max());
if (n < 1.0 || n >= maxLongLongAsDouble)
{
return std::numeric_limits<double>::quiet_NaN();
}
if (std::floor(n) != n)
{
return std::numeric_limits<double>::quiet_NaN();
}
std::uniform_int_distribution<long long> dist(0, static_cast<long long>(n) - 1);
return static_cast<double>(dist(rng));
}
};
std::wstring ToWStringFullPrecision(double value)
{
if (std::isnan(value))
{
return L"NaN";
}
if (std::isinf(value))
{
return value > 0 ? L"inf" : L"-inf";
}
std::wostringstream oss;
oss.imbue(std::locale::classic());
oss << std::fixed << std::setprecision(15) << value;
@@ -120,13 +47,6 @@ namespace ExprtkCalculator::internal
symbol_table.add_function("factorial", factorial);
symbol_table.add_function("sign", sign);
// thread_local ensures each thread has its own RNG instance (seeded once,
// state preserved across calls) without requiring locks.
static thread_local rand_func rand_fn;
static thread_local randi_func randi_fn;
symbol_table.add_function("rand", rand_fn);
symbol_table.add_function("randi", randi_fn);
exprtk::expression<double> expression;
expression.register_symbol_table(symbol_table);
@@ -145,7 +65,7 @@ namespace ExprtkCalculator::internal
parser.settings().disable_all_inequality_ops(); // Disable inequality operators like <, >, <=, >=, !=, etc.
if (!parser.compile(expressionText, expression))
return L"ParseError";
return L"NaN";
return ToWStringFullPrecision(expression.value());
}

View File

@@ -4,7 +4,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
<RootNamespace>Microsoft.PowerToys.Common.UI.Controls</RootNamespace>
<AssemblyName>PowerToys.Common.UI.Controls</AssemblyName>
<UseWinUI>true</UseWinUI>
@@ -17,7 +17,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />

View File

@@ -1,223 +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.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.UI.Windowing;
using Windows.Graphics;
using WinUIEx;
namespace Microsoft.PowerToys.Common.UI.Controls.Flyout;
/// <summary>
/// Shared helper for positioning and sizing flyout-style WinUI 3 windows
/// (e.g. Quick Access, PowerDisplay) that are pinned to a corner of the work area.
///
/// The public API takes sizes in device-independent pixels (DIP). The helper resolves the
/// target monitor's effective DPI and converts to physical pixels. All window positioning
/// uses absolute screen physical-pixel coordinates via
/// <see cref="AppWindow.MoveAndResize(RectInt32)"/> — the same pattern used by the original
/// Settings.UI flyout, which proved reliable across multi-monitor and mixed-DPI setups.
/// </summary>
public static partial class FlyoutWindowHelper
{
private const uint MdtEffectiveDpi = 0;
private const int DefaultDpi = 96;
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int X;
public int Y;
}
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetCursorPos(out POINT lpPoint);
[LibraryImport("shcore.dll")]
private static partial int GetDpiForMonitor(nint hMonitor, uint dpiType, out uint dpiX, out uint dpiY);
/// <summary>
/// Get the DPI scale factor (1.0 = 100%, 1.25 = 125%, 1.5 = 150%, 2.0 = 200%) for a window.
/// </summary>
public static double GetDpiScale(WindowEx window)
{
ArgumentNullException.ThrowIfNull(window);
return (double)window.GetDpiForWindow() / DefaultDpi;
}
/// <summary>
/// Get the DPI scale factor for a given <see cref="DisplayArea"/>.
/// Resolves DPI from the underlying monitor handle so the value reflects the
/// target display, regardless of which monitor the window is currently on.
/// </summary>
public static double GetDpiScale(DisplayArea displayArea)
{
ArgumentNullException.ThrowIfNull(displayArea);
return (double)GetEffectiveDpi(global::Microsoft.UI.Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId)) / DefaultDpi;
}
/// <summary>
/// Convert device-independent pixels (DIP) to physical pixels (rounding up).
/// </summary>
public static int ScaleToPhysicalPixels(int dip, double dpiScale)
{
return (int)Math.Ceiling(dip * dpiScale);
}
/// <summary>
/// Convert physical pixels to device-independent pixels (DIP) (rounding down).
/// </summary>
public static int ScaleToDip(int physicalPixels, double dpiScale)
{
return (int)Math.Floor(physicalPixels / dpiScale);
}
/// <summary>
/// Look up the <see cref="DisplayArea"/> currently containing the mouse cursor.
/// </summary>
public static bool TryGetDisplayAreaAtCursor(out DisplayArea? displayArea)
{
displayArea = null;
if (!GetCursorPos(out var cursorPos))
{
return false;
}
displayArea = DisplayArea.GetFromPoint(new PointInt32(cursorPos.X, cursorPos.Y), DisplayAreaFallback.Nearest);
return displayArea is not null;
}
/// <summary>
/// Position a flyout-style window at the bottom-right corner of the work area on the
/// monitor under the mouse cursor.
/// </summary>
public static void PositionWindowBottomRight(
WindowEx window,
int widthDip,
int heightDip,
int rightMarginDip = 0,
int bottomMarginDip = 0)
{
ArgumentNullException.ThrowIfNull(window);
if (!TryGetDisplayAreaAtCursor(out var displayArea) || displayArea is null)
{
Logger.LogWarning("FlyoutWindowHelper.PositionWindowBottomRight: unable to determine display from cursor; skipping positioning");
return;
}
PositionWindowBottomRight(window, displayArea, widthDip, heightDip, rightMarginDip, bottomMarginDip);
}
/// <summary>
/// Position a flyout-style window at the bottom-right corner of the specified display
/// area's work area. Use this overload when the caller has already resolved the target
/// <see cref="DisplayArea"/> (e.g. the cursor monitor) so size and placement are computed
/// from the same source.
///
/// Internally moves the window in two steps to avoid <c>WM_DPICHANGED</c> double-scaling
/// when the target monitor has a different DPI than the one the window was previously on:
/// first a 1×1 teleport into the target display, then the real position+size while the
/// window is already on that monitor (no DPI boundary crossing).
/// </summary>
public static void PositionWindowBottomRight(
WindowEx window,
DisplayArea displayArea,
int widthDip,
int heightDip,
int rightMarginDip = 0,
int bottomMarginDip = 0)
{
ArgumentNullException.ThrowIfNull(window);
ArgumentNullException.ThrowIfNull(displayArea);
double dpiScale = GetDpiScale(displayArea);
var work = displayArea.WorkArea;
int w = ScaleToPhysicalPixels(widthDip, dpiScale);
int h = ScaleToPhysicalPixels(heightDip, dpiScale);
int marginRight = ScaleToPhysicalPixels(rightMarginDip, dpiScale);
int marginBottom = ScaleToPhysicalPixels(bottomMarginDip, dpiScale);
// Clamp size so the window never extends past the work area minus margins.
// Guards against the bottom/right edge spilling into the taskbar when rounding
// (Math.Ceiling above) would push it just past the boundary.
int maxW = Math.Max(0, work.Width - marginRight);
int maxH = Math.Max(0, work.Height - marginBottom);
w = Math.Min(w, maxW);
h = Math.Min(h, maxH);
// Absolute screen physical-pixel coordinates. WorkArea is in screen coordinates,
// so for non-primary monitors WorkArea.X/Y will be non-zero (and may be negative).
int x = work.X + work.Width - w - marginRight;
int y = work.Y + work.Height - h - marginBottom;
MoveAndResizeOnDisplay(window, displayArea, new RectInt32(x, y, w, h));
}
/// <summary>
/// Center a window within the specified display area's work area.
/// Uses a 1×1 teleport into the target display first to avoid WM_DPICHANGED
/// double-scaling when crossing monitors with different DPI.
/// </summary>
public static void CenterWindowOnDisplay(
WindowEx window,
DisplayArea displayArea,
int widthDip,
int heightDip)
{
ArgumentNullException.ThrowIfNull(window);
ArgumentNullException.ThrowIfNull(displayArea);
double dpiScale = GetDpiScale(displayArea);
var work = displayArea.WorkArea;
int w = Math.Min(ScaleToPhysicalPixels(widthDip, dpiScale), work.Width);
int h = Math.Min(ScaleToPhysicalPixels(heightDip, dpiScale), work.Height);
int x = work.X + ((work.Width - w) / 2);
int y = work.Y + ((work.Height - h) / 2);
MoveAndResizeOnDisplay(window, displayArea, new RectInt32(x, y, w, h));
}
/// <summary>
/// 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>
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;
if (needsTeleport)
{
var work = targetDisplay.WorkArea;
window.AppWindow.MoveAndResize(new RectInt32(work.X, work.Y, 1, 1));
}
window.AppWindow.MoveAndResize(finalRect);
}
private static int GetEffectiveDpi(nint hMonitor)
{
if (hMonitor == 0)
{
return DefaultDpi;
}
var hr = GetDpiForMonitor(hMonitor, MdtEffectiveDpi, out var dpiX, out _);
return hr >= 0 && dpiX > 0 ? (int)dpiX : DefaultDpi;
}
}

View File

@@ -1,92 +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.Runtime.InteropServices;
using WinUIEx;
namespace Microsoft.PowerToys.Common.UI.Controls.Window;
/// <summary>
/// Subclasses a window's WndProc and invokes a preprocessor callback for every
/// message before the default window procedure runs. Useful for routing low-level
/// Win32 messages (e.g. <c>WM_HOTKEY</c>) into managed handlers without depending
/// on the WinUI XAML message loop.
/// </summary>
/// <remarks>
/// Usage:
/// <code>
/// _hook = new WindowMessageHook(window, (uMsg, wParam, lParam) =>
/// _hotkeyService.HandleMessage(uMsg, wParam));
/// </code>
/// Dispose to restore the original WndProc.
/// </remarks>
public sealed partial class WindowMessageHook : IDisposable
{
// Called for every message before default processing. Return true to swallow.
private readonly Func<uint, nuint, nint, bool> _preProcessor;
private const int GwlWndProc = -4;
private readonly nint _hwnd;
private nint _originalWndProc;
private WndProcDelegate? _wndProcDelegate;
private bool _disposed;
private delegate nint WndProcDelegate(nint hwnd, uint uMsg, nuint wParam, nint lParam);
[LibraryImport("user32.dll", EntryPoint = "SetWindowLongPtrW")]
private static partial nint SetWindowLongPtr(nint hWnd, int nIndex, nint dwNewLong);
[LibraryImport("user32.dll", EntryPoint = "CallWindowProcW")]
private static partial nint CallWindowProc(nint lpPrevWndFunc, nint hWnd, uint msg, nuint wParam, nint lParam);
/// <summary>
/// Initializes a new instance of the <see cref="WindowMessageHook"/> class
/// and subclasses the supplied window's WndProc.
/// </summary>
/// <param name="window">Window to subclass.</param>
/// <param name="preProcessor">Callback invoked for every message before the
/// default WndProc. Receives <c>(uMsg, wParam, lParam)</c>. Return
/// <see langword="true"/> to swallow the message.</param>
public WindowMessageHook(WindowEx window, Func<uint, nuint, nint, bool> preProcessor)
{
ArgumentNullException.ThrowIfNull(window);
ArgumentNullException.ThrowIfNull(preProcessor);
_hwnd = window.GetWindowHandle();
_preProcessor = preProcessor;
_wndProcDelegate = WndProc;
var ptr = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate);
_originalWndProc = SetWindowLongPtr(_hwnd, GwlWndProc, ptr);
}
private nint WndProc(nint hwnd, uint uMsg, nuint wParam, nint lParam)
{
if (_preProcessor(uMsg, wParam, lParam))
{
return 0;
}
return CallWindowProc(_originalWndProc, hwnd, uMsg, wParam, lParam);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
if (_originalWndProc != 0)
{
SetWindowLongPtr(_hwnd, GwlWndProc, _originalWndProc);
_originalWndProc = 0;
}
_wndProcDelegate = null;
}
}

View File

@@ -14,6 +14,7 @@
<ItemGroup>
<PackageReference Include="Markdig.Signed" />
<PackageReference Include="System.Text.Encoding.CodePages" />
<PackageReference Include="UTF.Unknown" />
</ItemGroup>
</Project>

View File

@@ -2,35 +2,43 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ManagedCommon
{
public static class IdRecoveryHelper
{
/// <summary>
/// Ensures that all items in the provided list have unique IDs. Duplicate IDs are replaced
/// with the next available unique ID.
/// Fixes invalid IDs in the given list by assigning unique values.
/// It ensures that all IDs are non-empty and unique, correcting any duplicates or empty IDs.
/// </summary>
/// <param name="items">The list of items that may contain duplicate IDs.</param>
/// <param name="items">The list of items that may contain invalid IDs.</param>
public static void RecoverInvalidIds<T>(IEnumerable<T> items)
where T : class, IHasId
{
var seenIds = new HashSet<int>();
int nextAvailableId = 0;
var idSet = new HashSet<int>();
int newId = 0;
var sortedItems = items.OrderBy(i => i.Id).ToList(); // Sort items by ID for consistent processing
foreach (var item in items)
// Iterate through the list and fix invalid IDs
foreach (var item in sortedItems)
{
// If this ID is already used, assign a new unique ID.
if (!seenIds.Add(item.Id))
// If the ID is invalid or already exists in the set (duplicate), assign a new unique ID
if (!idSet.Add(item.Id))
{
// Find the next unused ID.
while (!seenIds.Add(nextAvailableId))
// Find the next available unique ID
while (idSet.Contains(newId))
{
nextAvailableId++;
newId++;
}
item.Id = nextAvailableId;
item.Id = newId;
idSet.Add(newId); // Add the newly assigned ID to the set
}
}
}

View File

@@ -55,7 +55,7 @@
<Import Project="$(RepoRoot)deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -63,6 +63,6 @@
</PropertyGroup>
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -38,7 +38,7 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -46,6 +46,6 @@
</PropertyGroup>
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.260126.7\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(RepoRoot)packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.260126.7" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
</packages>

Some files were not shown because too many files have changed in this diff Show More