Compare commits

..

3 Commits

264 changed files with 2394 additions and 6711 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

@@ -127,7 +127,6 @@ HOLDSPACE
HOLDBACKSPACE
IDIGNORE
KBDLLHOOKSTRUCT
keydowns
keyevent
LAlt
LBUTTON
@@ -330,18 +329,14 @@ MRUINFO
REGSTR
# Misc Win32 APIs and PInvokes
DEFAULTTONEAREST
INVOKEIDLIST
LCMAP
MEMORYSTATUSEX
ABE
Mdt
HTCAPTION
POSCHANGED
QPC
QUERYPOS
SETAUTOHIDEBAR
ULW
WINDOWPOS
WINEVENTPROC
WORKERW
@@ -396,10 +391,3 @@ Nonpaged
# XAML
Untargeted
# Program names
SEARCHHOST
SHELLEXPERIENCEHOST
SHELLHOST
STARTMENUEXPERIENCEHOST
WIDGETBOARD

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
(\\?['"]|")[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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +1,5 @@
# 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
@@ -84,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
@@ -175,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])
@@ -191,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
@@ -208,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, ".*?"\)
@@ -230,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

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

@@ -0,0 +1,232 @@
name: Scheduled Issue Product Labeling
on:
schedule:
- cron: "20 */6 * * *" # Every 6 hours at :20
workflow_dispatch: # Allow manual trigger
permissions:
models: read
issues: write
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
label-issues:
runs-on: ubuntu-latest
steps:
- name: Label issues missing Product labels
uses: actions/github-script@v7
with:
script: |
// ── Product label mapping ──────────────────────────────────
// Canonical list of Product-* labels used in this repo,
// derived from .github/skills/release-note-generation/references/step2-labeling.md
const PRODUCT_LABELS = [
"Product-Advanced Paste",
"Product-Always on Top",
"Product-Awake",
"Product-ColorPicker",
"Product-Command not found",
"Product-Command Palette",
"Product-CropAndLock",
"Product-Cursor Wrap",
"Product-Environment Variables",
"Product-FancyZones",
"Product-File Explorer",
"Product-File Locksmith",
"Product-Find My Mouse",
"Product-Hosts",
"Product-Image Resizer",
"Product-Keyboard Manager",
"Product-LightSwitch",
"Product-Mouse Highlighter",
"Product-Mouse Jump",
"Product-Mouse Pointer Crosshairs",
"Product-Mouse Without Borders",
"Product-New+",
"Product-Peek",
"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",
];
// Map from bug-report "Area(s) with issue?" dropdown values
// to Product-* labels (used as strong hints when the issue body
// contains the area dropdown answer).
const AREA_TO_LABEL = {
"Advanced Paste": "Product-Advanced Paste",
"Always on Top": "Product-Always on Top",
"Awake": "Product-Awake",
"ColorPicker": "Product-ColorPicker",
"Command not found": "Product-Command not found",
"Command Palette": "Product-Command Palette",
"Crop and Lock": "Product-CropAndLock",
"Environment Variables": "Product-Environment Variables",
"FancyZones": "Product-FancyZones",
"FancyZones Editor": "Product-FancyZones",
"File Locksmith": "Product-File Locksmith",
"File Explorer: Preview Pane": "Product-File Explorer",
"File Explorer: Thumbnail preview": "Product-File Explorer",
"Hosts File Editor": "Product-Hosts",
"Image Resizer": "Product-Image Resizer",
"Keyboard Manager": "Product-Keyboard Manager",
"Light Switch": "Product-LightSwitch",
"Mouse Utilities": "Product-Find My Mouse",
"Mouse Without Borders": "Product-Mouse Without Borders",
"New+": "Product-New+",
"Peek": "Product-Peek",
"PowerRename": "Product-PowerRename",
"PowerToys Run": "Product-PowerToys Run",
"Quick Accent": "Product-Quick Accent",
"Registry Preview": "Product-Registry Preview",
"Screen ruler": "Product-Screen Ruler",
"Shortcut Guide": "Product-Shortcut Guide",
"TextExtractor": "Product-Text Extractor",
"Workspaces": "Product-Workspaces",
"ZoomIt": "Product-ZoomIt",
};
// ── Helpers ────────────────────────────────────────────────
function hasProductLabel(labels) {
return labels.some((l) => l.name.startsWith("Product-"));
}
// Try to extract the area from the structured bug-report body
// (the "Area(s) with issue?" dropdown).
function extractAreaFromBody(body) {
if (!body) return null;
// The rendered issue body contains a heading followed by the selected values
const areaMatch = body.match(
/### Area\(s\) with issue\?\s*\n+(.+?)(?:\n###|\n\n|$)/s
);
if (!areaMatch) return null;
const areaText = areaMatch[1].trim();
if (areaText === "_No response_" || areaText === "General") return null;
// Could be comma-separated; take the first specific one
const areas = areaText.split(",").map((a) => a.trim());
for (const area of areas) {
if (AREA_TO_LABEL[area]) return AREA_TO_LABEL[area];
}
return null;
}
// Use GitHub Models to classify an issue when the dropdown area
// is not available or is "General".
const MAX_BODY_LENGTH = 3000; // Truncate body to stay within model token limits while keeping enough context
const MAX_COMPLETION_TOKENS = 60; // Enough for a Product-* label name with some margin
async function classifyWithAI(title, body) {
const truncatedBody = (body || "").slice(0, MAX_BODY_LENGTH);
const labelList = PRODUCT_LABELS.join("\n- ");
const prompt = `You are a GitHub issue triager for the microsoft/PowerToys repository.
Given the issue title and body below, determine which ONE Product label best fits.
Reply with ONLY the label name (e.g. "Product-FancyZones") or "UNKNOWN" if you cannot determine it.
Available labels:
- ${labelList}
Issue title: ${title}
Issue body:
${truncatedBody}`;
try {
const response = await fetch(
"https://models.github.ai/inference/chat/completions",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "openai/gpt-4o",
messages: [{ role: "user", content: prompt }],
max_tokens: MAX_COMPLETION_TOKENS,
temperature: 0,
}),
}
);
if (!response.ok) {
core.warning(`AI classification failed: ${response.status} ${response.statusText}`);
return null;
}
const data = await response.json();
const answer = data.choices?.[0]?.message?.content?.trim();
if (!answer || answer === "UNKNOWN") return null;
// Validate the answer is a known label
if (PRODUCT_LABELS.includes(answer)) return answer;
// Try fuzzy match (the model may include extra text)
const found = PRODUCT_LABELS.find((l) => answer.includes(l));
return found || null;
} catch (err) {
core.warning(`AI classification error: ${err.message}`);
return null;
}
}
// ── Main ───────────────────────────────────────────────────
const MAX_ISSUES = 50; // Process up to 50 issues per run
let labeled = 0;
let skipped = 0;
core.info("Searching for open issues with Needs-Triage but no Product-* label...");
// Paginate through open issues labeled Needs-Triage
for await (const response of github.paginate.iterator(
github.rest.issues.listForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
labels: "Needs-Triage",
sort: "created",
direction: "desc",
per_page: 100,
}
)) {
for (const issue of response.data) {
if (labeled + skipped >= MAX_ISSUES) break;
// Skip pull requests (the API returns them too)
if (issue.pull_request) continue;
if (hasProductLabel(issue.labels)) continue;
core.info(`Processing #${issue.number}: ${issue.title}`);
// 1) Try structured area dropdown first (fast, no AI needed)
let label = extractAreaFromBody(issue.body);
// 2) Fall back to AI classification
if (!label) {
label = await classifyWithAI(issue.title, issue.body);
}
if (label) {
core.info(` → Applying "${label}" to #${issue.number}`);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: [label],
});
labeled++;
} else {
core.info(` → Could not determine product label for #${issue.number}, skipping.`);
skipped++;
}
}
if (labeled + skipped >= MAX_ISSUES) break;
}
core.info(`Done. Labeled: ${labeled}, Skipped: ${skipped}`);

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:

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

@@ -264,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",

View File

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

View File

@@ -319,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" />

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
@@ -50,18 +50,18 @@ But to get started quickly, choose one of the installation methods below:
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.100%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.99.0/PowerToysUserSetup-0.99.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.99.0/PowerToysUserSetup-0.99.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.99.0/PowerToysSetup-0.99.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.99.0/PowerToysSetup-0.99.0-arm64.exe
[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.99.0-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.99.0-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.99.0-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.99.0-arm64.exe][ptMachineArm64] |
| 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>
@@ -106,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/tag/v0.99.0).
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

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

View File

@@ -353,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
@@ -454,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.
@@ -1780,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.
}
}
@@ -2006,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.
@@ -2350,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

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

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

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

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

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

@@ -191,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
@@ -336,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

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

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

@@ -3,7 +3,7 @@
#define HKEY_WINDOWS_THEME L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
// disabling warning 4702 - unreachable code
// prevent the warning after the call off an infinite loop function
// prevent the warning after the call off a infinite loop function
#pragma warning(push)
#pragma warning(disable : 4702)
DWORD WINAPI _checkTheme(LPVOID lpParam)

View File

@@ -68,7 +68,7 @@ namespace Microsoft.PowerToys.UITest
}
/// <summary>
/// Gets a value indicating whether or not the UI element is Enabled.
/// Gets a value indicating whether the UI element is Enabled or not.
/// </summary>
public bool Enabled
{

View File

@@ -93,7 +93,7 @@ namespace Microsoft.PowerToys.UITest
}
/// <summary>
/// Exit an exe by Name.
/// Exit a exe by Name.
/// </summary>
/// <param name="processName">The path to the application executable.</param>
public void ExitExeByName(string processName)
@@ -114,7 +114,7 @@ namespace Microsoft.PowerToys.UITest
}
/// <summary>
/// Exit an exe.
/// Exit a exe.
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
public void ExitExe(string appPath)

View File

@@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.UITest
public static class VisualAssert
{
/// <summary>
/// Asserts current visual state of the element is equal to base line image.
/// Asserts current visual state of the element is equal with base line image.
/// To use this VisualAssert, you need to set Window Theme to Light-Mode to avoid Theme color difference in baseline image.
/// Such limitation could be removed either Auto-generate baseline image for both Light & Dark mode
/// </summary>

View File

@@ -37,7 +37,7 @@ public:
}
if (this->interrupted)
{
//Just returns an empty string if the queue was interrupted.
//Just returns a empty string if the queue was interrupted.
return std::wstring(L"");
}
std::wstring message = this->message_queue.front();

View File

@@ -13,15 +13,8 @@ namespace Microsoft.Interop.Tests
[TestClass]
public class InteropTests : IDisposable
{
// Pipe names are machine-global, so two concurrent test runs on the same CI agent
// (or a leaked handle from a prior run) would deadlock if we used a shared constant.
// Suffix with process id + a GUID so every test run gets its own pair.
private const string PipePrefix = @"\\.\pipe\";
private static readonly string PipeSuffix = $"{Environment.ProcessId}_{Guid.NewGuid():N}";
private static readonly string ServerSidePipe = $"{PipePrefix}serverside_{PipeSuffix}";
private static readonly string ClientSidePipe = $"{PipePrefix}clientside_{PipeSuffix}";
private static readonly TimeSpan MessageWaitTimeout = TimeSpan.FromSeconds(30);
private const string ServerSidePipe = "\\\\.\\pipe\\serverside";
private const string ClientSidePipe = "\\\\.\\pipe\\clientside";
internal TwoWayPipeMessageIPCManaged ClientPipe { get; set; }
@@ -61,11 +54,7 @@ namespace Microsoft.Interop.Tests
Thread.Sleep(100);
ClientPipe.Send(testString);
// Bounded wait so a broken pipe handshake fails the test quickly
// instead of hanging the CI agent until the job-level timeout.
var timeoutMessage = $"Pipe callback was not invoked within {MessageWaitTimeout.TotalSeconds}s. Server='{ServerSidePipe}' Client='{ClientSidePipe}'.";
Assert.IsTrue(reset.WaitOne(MessageWaitTimeout), timeoutMessage);
reset.WaitOne();
serverPipe.End();
}

View File

@@ -78,7 +78,7 @@ If enabled, per-user installation is not allowed.
If disabled or not configured, per-user installation is allowed.
</string>
<string id="DisableAutomaticUpdateDownloadDescription">This policy configures whether or not the automatic download and installation of available updates is disabled. (On metered connections updates are never downloaded.)
<string id="DisableAutomaticUpdateDownloadDescription">This policy configures whether the automatic download and installation of available updates is disabled or not. (On metered connections updates are never downloaded.)
If enabled, automatic download and installation is disabled.
@@ -94,7 +94,7 @@ Note: The notification about new major versions is always displayed.
This policy has no effect if the update notification is disabled by the policy "Disable Action Center notification for new updates" or the user setting.
</string>
<string id="DisableNewUpdateToastDescription">This policy configures whether or not the action center notification for new updates is shown.
<string id="DisableNewUpdateToastDescription">This policy configures whether the action center notification for new updates is shown or not.
If enabled, the notification is disabled.

View File

@@ -910,12 +910,6 @@ public:
return powertoys_gpo::getConfiguredAdvancedPasteEnabledValue();
}
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
return false;
}
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);

View File

@@ -75,12 +75,6 @@ public:
return powertoys_gpo::getConfiguredCropAndLockEnabledValue();
}
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
return false;
}
// Return JSON with the configuration options.
// These are the settings shown on the settings page along with their current values.
virtual bool get_config(wchar_t* buffer, int* buffer_size) override

View File

@@ -226,12 +226,6 @@ public:
return powertoys_gpo::getConfiguredEnvironmentVariablesEnabledValue();
}
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
return false;
}
virtual bool get_config(wchar_t* /*buffer*/, int* /*buffer_size*/) override
{
return false;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

After

Width:  |  Height:  |  Size: 766 B

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,9 @@
#include <shellapi.h>
#include <commctrl.h>
#include <TraceLoggingProvider.h>
#include <atomic>
#include <limits>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>

View File

@@ -243,12 +243,6 @@ public:
return powertoys_gpo::getConfiguredHostsFileEditorEnabledValue();
}
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
return false;
}
virtual bool get_config(wchar_t* /*buffer*/, int* /*buffer_size*/) override
{
return false;

View File

@@ -45,18 +45,21 @@ void LightSwitchStateManager::OnManualOverride()
Logger::info(L"[LightSwitchStateManager] Manual override triggered");
_state.isManualOverride = !_state.isManualOverride;
// ModuleInterface has already flipped the Windows theme before signaling this event,
// regardless of which direction isManualOverride just toggled. Sync cached state and
// notify PowerDisplay on every call so the profile follows every hotkey press — the
// previous "if entering" gate silently dropped every even-numbered press.
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
// When entering manual override, sync internal theme state to match the current system
// The hotkey handler in ModuleInterface has already toggled the theme, so we read the new state
if (_state.isManualOverride)
{
_state.isSystemLightActive = GetCurrentSystemTheme();
_state.isAppsLightActive = GetCurrentAppsTheme();
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
(_state.isSystemLightActive ? L"light" : L"dark"),
(_state.isAppsLightActive ? L"light" : L"dark"));
NotifyPowerDisplay(_state.isSystemLightActive);
// Notify PowerDisplay about the theme change triggered by hotkey
// The theme has already been applied by ModuleInterface, we just need to notify PowerDisplay
NotifyPowerDisplay(_state.isSystemLightActive);
}
EvaluateAndApplyIfNeeded();
}

View File

@@ -201,7 +201,7 @@ namespace LightSwitch.UITests
}
/// <summary>
/// Perform an update time test operation
/// Perform a update time test operation
/// </summary>
public static void PerformUpdateTimeTest(UITestBase testBase)
{
@@ -257,7 +257,7 @@ namespace LightSwitch.UITests
}
/// <summary>
/// Perform an update manual location test operation
/// Perform a update manual location test operation
/// </summary>
public static void PerformUserSelectedLocationTest(UITestBase testBase)
{
@@ -300,7 +300,7 @@ namespace LightSwitch.UITests
}
/// <summary>
/// Perform an update geolocation test operation
/// Perform a update geolocation test operation
/// </summary>
public static void PerformGeolocationTest(UITestBase testBase)
{
@@ -335,7 +335,7 @@ namespace LightSwitch.UITests
}
/// <summary>
/// Perform an update time test operation
/// Perform a update time test operation
/// </summary>
public static void PerformOffsetTest(UITestBase testBase)
{

View File

@@ -368,7 +368,7 @@ LRESULT CALLBACK Highlighter::MouseHookProc(int nCode, WPARAM wParam, LPARAM lPa
}
instance->AddDrawingPoint(MouseButton::Right);
instance->m_rightButtonPressed = true;
// same as for the left button, start a timer to reposition ourselves to topmost position
// same as for the left button, start a timer for reposition ourselves to topmost position
if (instance->m_timer_id == 0)
{
instance->m_timer_id = SetTimer(instance->m_hwnd, BRING_TO_FRONT_TIMER_ID, 10, nullptr);

View File

@@ -75,7 +75,7 @@ namespace
// for a window, it is just limited. If there is no WS_MAXIMIZEBOX using
// WinKey + Up just won't maximize the window. Similarly, without
// WS_MINIMIZEBOX the window will not get minimized. A "Save As..." dialog
// is an example of such window - it can be snapped to both sides and to
// is a example of such window - it can be snapped to both sides and to
// all screen corners, but will not get maximized nor minimized.
// For now, since ShortcutGuide can only disable entire "Windows Controls"
// group, we require that the window supports all the options.

View File

@@ -92,12 +92,6 @@ public:
return powertoys_gpo::getConfiguredWorkspacesEnabledValue();
}
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
return false;
}
// Return JSON with the configuration options.
// These are the settings shown on the settings page along with their current values.
virtual bool get_config(_Out_ PWSTR buffer, _Out_ int* buffer_size) override

View File

@@ -9784,10 +9784,7 @@ LRESULT APIENTRY MainWndProc(
{
if (!RegisterHotKey(hWnd, ZOOM_HOTKEY, g_ToggleMod, g_ToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified zoom toggle hotkey is already in use.\nSelect a different zoom toggle hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified zoom toggle hotkey is already in use.\nSelect a different zoom toggle hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
@@ -9796,10 +9793,7 @@ LRESULT APIENTRY MainWndProc(
if (!RegisterHotKey(hWnd, LIVE_HOTKEY, g_LiveZoomToggleMod, g_LiveZoomToggleKey & 0xFF) ||
!RegisterHotKey(hWnd, LIVE_DRAW_HOTKEY, g_LiveZoomToggleMod ^ MOD_SHIFT, g_LiveZoomToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified live-zoom toggle hotkey is already in use.\nSelect a different zoom toggle hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified live-zoom toggle hotkey is already in use.\nSelect a different zoom toggle hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
@@ -9807,10 +9801,7 @@ LRESULT APIENTRY MainWndProc(
{
if (!RegisterHotKey(hWnd, DRAW_HOTKEY, g_DrawToggleMod, g_DrawToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified draw w/out zoom hotkey is already in use.\nSelect a different draw w/out zoom hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified draw w/out zoom hotkey is already in use.\nSelect a different draw w/out zoom hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
@@ -9818,10 +9809,7 @@ LRESULT APIENTRY MainWndProc(
{
if (!RegisterHotKey(hWnd, BREAK_HOTKEY, g_BreakToggleMod, g_BreakToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified break timer hotkey is already in use.\nSelect a different break timer hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified break timer hotkey is already in use.\nSelect a different break timer hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
@@ -9830,10 +9818,7 @@ LRESULT APIENTRY MainWndProc(
if (!RegisterHotKey(hWnd, DEMOTYPE_HOTKEY, g_DemoTypeToggleMod, g_DemoTypeToggleKey & 0xFF) ||
!RegisterHotKey(hWnd, DEMOTYPE_RESET_HOTKEY, (g_DemoTypeToggleMod ^ MOD_SHIFT), g_DemoTypeToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified live-type hotkey is already in use.\nSelect a different live-type hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified live-type hotkey is already in use.\nSelect a different live-type hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
@@ -9842,10 +9827,7 @@ LRESULT APIENTRY MainWndProc(
if (!RegisterHotKey(hWnd, SNIP_HOTKEY, g_SnipToggleMod, g_SnipToggleKey & 0xFF) ||
!RegisterHotKey(hWnd, SNIP_SAVE_HOTKEY, (g_SnipToggleMod ^ MOD_SHIFT), g_SnipToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified snip hotkey is already in use.\nSelect a different snip hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified snip hotkey is already in use.\nSelect a different snip hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
@@ -9855,10 +9837,7 @@ LRESULT APIENTRY MainWndProc(
if (!RegisterHotKey(hWnd, SNIP_PANORAMA_HOTKEY, g_SnipPanoramaToggleMod | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF) ||
!RegisterHotKey(hWnd, SNIP_PANORAMA_SAVE_HOTKEY, ( g_SnipPanoramaToggleMod ^ MOD_SHIFT ) | MOD_NOREPEAT, g_SnipPanoramaToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified panorama snip hotkey is already in use.\nSelect a different panorama snip hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified panorama snip hotkey is already in use.\nSelect a different panorama snip hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
@@ -9866,10 +9845,7 @@ LRESULT APIENTRY MainWndProc(
{
if (!RegisterHotKey(hWnd, SNIP_OCR_HOTKEY, g_SnipOcrToggleMod, g_SnipOcrToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified snip OCR hotkey is already in use.\nSelect a different snip OCR hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified snip OCR hotkey is already in use.\nSelect a different snip OCR hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}
@@ -9879,10 +9855,7 @@ LRESULT APIENTRY MainWndProc(
!RegisterHotKey(hWnd, RECORD_CROP_HOTKEY, (g_RecordToggleMod ^ MOD_SHIFT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF) ||
!RegisterHotKey(hWnd, RECORD_WINDOW_HOTKEY, (g_RecordToggleMod ^ MOD_ALT) | MOD_NOREPEAT, g_RecordToggleKey & 0xFF))
{
if(!g_StartedByPowerToys)
{
MessageBox(hWnd, L"The specified record hotkey is already in use.\nSelect a different record hotkey.", APPNAME, MB_ICONERROR);
}
MessageBox(hWnd, L"The specified record hotkey is already in use.\nSelect a different record hotkey.", APPNAME, MB_ICONERROR);
showOptions = TRUE;
}
}

View File

@@ -26,7 +26,6 @@
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Calc.UnitTests\\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Indexer.UnitTests\\Microsoft.CmdPal.Ext.Indexer.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Registry.UnitTests\\Microsoft.CmdPal.Ext.Registry.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj",
"src\\modules\\cmdpal\\Tests\\Microsoft.CmdPal.Ext.Shell.UnitTests\\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj",
@@ -63,4 +62,4 @@
"src\\settings-ui\\Settings.UI.Library\\Settings.UI.Library.csproj"
]
}
}
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Common.Messages;
/// <summary>
/// Message to request hiding the window.
/// </summary>
public sealed partial record HideWindowMessage;

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.UI.ViewModels;
@@ -12,21 +11,9 @@ public record AppStateModel
///////////////////////////////////////////////////////////////////////////
// STATE HERE
// Make sure that any new types you add are added to JsonSerializationContext!
private RecentCommandsManager? _recentCommands = new();
public RecentCommandsManager RecentCommands { get; init; } = new();
public RecentCommandsManager RecentCommands
{
get => _recentCommands ?? new();
init => _recentCommands = value;
}
private ImmutableList<string>? _runHistory = ImmutableList<string>.Empty;
public ImmutableList<string> RunHistory
{
get => _runHistory ?? ImmutableList<string>.Empty;
init => _runHistory = value;
}
public ImmutableList<string> RunHistory { get; init; } = ImmutableList<string>.Empty;
// END SETTINGS
///////////////////////////////////////////////////////////////////////////

View File

@@ -86,14 +86,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public CommandItemViewModel? SecondaryCommand => _secondaryMoreCommand;
public bool CanOpenContextMenu =>
// BEAR LOADING: A visible synthetic primary command makes the item
// context-openable immediately, even if out-of-proc MoreCommands are still
// hydrating. Without this fast path, the first open request can race slow
// menu initialization and get dropped.
_defaultCommandContextItemViewModel?.ShouldBeVisible == true ||
_moreCommandsSnapshot.Any(item => item is CommandItemViewModel command && command.ShouldBeVisible);
public bool CanOpenContextMenu => AllCommands.Any(item => item is CommandItemViewModel command && command.ShouldBeVisible);
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
@@ -139,15 +132,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
return;
}
var command = model.Command;
Command = new(command, PageContext);
Command = new(model.Command, PageContext);
Command.FastInitializeProperties();
_itemTitle = model.Title;
Subtitle = model.Subtitle;
_titleCache.Invalidate();
_subtitleCache.Invalidate();
TryCreateDefaultCommandContextItem(command);
Initialized |= InitializedState.FastInitialized;
}
@@ -224,7 +215,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
BuildAndInitMoreCommands();
TryCreateDefaultCommandContextItem(model.Command);
TryCreateDefaultCommandContextItem(model);
lock (_moreCommandsLock)
{
@@ -325,8 +316,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
{
case nameof(Command):
Command.PropertyChanged -= Command_PropertyChanged;
var command = model.Command;
Command = new(command, PageContext);
Command = new(model.Command, PageContext);
Command.InitializeProperties();
Command.PropertyChanged += Command_PropertyChanged;
@@ -342,7 +332,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
else
{
TryCreateDefaultCommandContextItem(command);
TryCreateDefaultCommandContextItem(model);
}
UpdateProperty(nameof(Name));
@@ -417,7 +407,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
else
{
TryCreateDefaultCommandContextItem(model.Command);
TryCreateDefaultCommandContextItem(model);
}
break;
@@ -437,22 +427,19 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
/// When a new instance is created, the snapshot is refreshed and
/// <see cref="AllCommands"/> is notified.
/// </summary>
private void TryCreateDefaultCommandContextItem(ICommand? commandModel)
private void TryCreateDefaultCommandContextItem(ICommandItem model)
{
if (_defaultCommandContextItemViewModel is not null)
{
return;
}
// We only synthesize the primary entry when the command is already
// usable; a null/empty primary must still fall back to late
// MoreCommands-based opening.
if (string.IsNullOrEmpty(Command.Name) || commandModel is null)
if (string.IsNullOrEmpty(model.Command?.Name))
{
return;
}
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(commandModel), PageContext)
_defaultCommandContextItemViewModel = new CommandContextItemViewModel(new CommandContextItem(model.Command!), PageContext)
{
_itemTitle = Name,
Subtitle = Subtitle,

View File

@@ -46,6 +46,45 @@ public partial class DockBandSettingsViewModel : ObservableObject
public IconInfoViewModel Icon => _adapter.IconViewModel;
private ShowLabelsOption _showLabels;
public ShowLabelsOption ShowLabels
{
get => _showLabels;
set
{
if (value != _showLabels)
{
_showLabels = value;
var newShowTitles = value switch
{
ShowLabelsOption.Default => (bool?)null,
ShowLabelsOption.ShowLabels => true,
ShowLabelsOption.HideLabels => false,
_ => null,
};
UpdateModel(_dockSettingsModel with { ShowTitles = newShowTitles });
}
}
}
private ShowLabelsOption FetchShowLabels()
{
if (_dockSettingsModel.ShowLabels == null)
{
return ShowLabelsOption.Default;
}
return _dockSettingsModel.ShowLabels.Value ? ShowLabelsOption.ShowLabels : ShowLabelsOption.HideLabels;
}
// used to map to ComboBox selection
public int ShowLabelsIndex
{
get => (int)ShowLabels;
set => ShowLabels = (ShowLabelsOption)value;
}
private DockPinSide PinSide
{
get => _pinSide;
@@ -99,6 +138,7 @@ public partial class DockBandSettingsViewModel : ObservableObject
_bandViewModel = bandViewModel;
_settingsService = settingsService;
_pinSide = FetchPinSide();
_showLabels = FetchShowLabels();
}
private DockPinSide FetchPinSide()

View File

@@ -61,11 +61,6 @@ public sealed partial class DockViewModel
}
Logger.LogDebug("Starting DockBands_CollectionChanged");
// Refresh settings so newly pinned/unpinned bands are visible.
// Pin/unpin operations save with hotReload:false (to avoid
// double-updates), so _settings can be stale here.
_settings = _settingsService.Settings.DockSettings;
SetupBands();
Logger.LogDebug("Ended DockBands_CollectionChanged");
}
@@ -559,7 +554,7 @@ public sealed partial class DockViewModel
}
// Create settings for the new band
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId };
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId, ShowLabels = null };
var dockSettings = _settings;
// Create the band view model

View File

@@ -44,15 +44,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
private readonly Lock _listLock = new();
private readonly IContextMenuFactory _contextMenuFactory;
// Reentrancy guard for FilteredItems mutations. WinUI3's ListView processes
// CollectionChanged synchronously, and its layout pass can pump the message
// loop — which lets a second DoOnUiThread task start mutating FilteredItems
// while the first is still mid-update. C# lock is reentrant (same thread
// re-acquires), so _listLock cannot prevent this. Instead we use a boolean
// flag and defer the latest update until the in-flight one finishes.
private bool _isUpdatingFilteredItems;
private Action? _pendingFilteredItemsUpdate;
[ThreadStatic]
private static Dictionary<ListViewModel, int>? _getItemsDepthByViewModel;
@@ -194,7 +185,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// But for all normal pages, we should run our fuzzy match on them.
lock (_listLock)
{
RunFilteredItemsUpdate(ApplyFilterUnderLock);
ApplyFilterUnderLock();
}
ItemsUpdated?.Invoke(this, new ItemsUpdatedEventArgs(true));
@@ -511,14 +502,14 @@ public partial class ListViewModel : PageViewModel, IDisposable
if (!_isDynamic)
{
// A static list? Great! Just run the filter.
RunFilteredItemsUpdate(ApplyFilterUnderLock);
ApplyFilterUnderLock();
}
else
{
// A dynamic list? Even better! Just stick everything into
// FilteredItems. The extension already did any filtering it cared about.
var snapshot = Items.Where(i => !i.IsInErrorState).ToList();
RunFilteredItemsUpdate(() => ListHelpers.InPlaceUpdateList(FilteredItems, snapshot));
ListHelpers.InPlaceUpdateList(FilteredItems, snapshot);
}
UpdateEmptyContent();
@@ -581,50 +572,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
/// </summary>
private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, SearchTextBox));
/// <summary>
/// Executes an action that mutates <see cref="FilteredItems"/> with a
/// reentrancy guard. WinUI3's native XAML renderer can pump the
/// message loop while processing a <c>CollectionChanged</c>
/// notification, which allows a second queued UI-thread task to begin
/// mutating the same collection before the first task finishes. This
/// causes heap corruption inside the native ItemsRepeater / ListView
/// and manifests as an access-violation in ntdll.dll.
///
/// The guard detects reentrancy (same UI thread re-entering) and
/// stores only the <em>latest</em> pending action. Once the
/// in-flight mutation completes, the pending action (if any) executes
/// immediately, ensuring the UI always converges to the newest state
/// without overlapping mutations.
/// </summary>
private void RunFilteredItemsUpdate(Action updateAction)
{
if (_isUpdatingFilteredItems)
{
// Reentrant call — store only the latest; earlier stale
// updates are intentionally dropped.
_pendingFilteredItemsUpdate = updateAction;
return;
}
_isUpdatingFilteredItems = true;
try
{
updateAction();
// Drain any update that was enqueued while we were running.
while (_pendingFilteredItemsUpdate is not null)
{
var pending = _pendingFilteredItemsUpdate;
_pendingFilteredItemsUpdate = null;
pending();
}
}
finally
{
_isUpdatingFilteredItems = false;
}
}
private Dictionary<IListItem, ListItemViewModel> ReadVmCache() => Volatile.Read(ref _vmCache);
private static bool IsCurrentThreadUiThread()
@@ -1121,15 +1068,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
Items.Clear();
RunFilteredItemsUpdate(() =>
foreach (var item in FilteredItems)
{
foreach (var item in FilteredItems)
{
item.SafeCleanup();
}
item.SafeCleanup();
}
FilteredItems.Clear();
});
FilteredItems.Clear();
}
PublishVmCache(new(VmCacheComparer));

View File

@@ -199,15 +199,8 @@ public partial class ExtensionService : IExtensionService, IDisposable
var extensions = await GetInstalledAppExtensionsAsync();
foreach (var extension in extensions)
{
try
{
var wrappers = await CreateWrappersForExtension(extension);
UpdateExtensionsListsFromWrappers(wrappers);
}
catch (Exception ex)
{
Logger.LogError($"Failed to load extension '{extension.DisplayName}': {ex.Message}");
}
var wrappers = await CreateWrappersForExtension(extension);
UpdateExtensionsListsFromWrappers(wrappers);
}
}
@@ -252,15 +245,8 @@ public partial class ExtensionService : IExtensionService, IDisposable
List<ExtensionWrapper> wrappers = [];
foreach (var classId in classIds)
{
try
{
var extensionWrapper = CreateExtensionWrapper(extension, cmdPalProvider, classId);
wrappers.Add(extensionWrapper);
}
catch (Exception ex)
{
Logger.LogError($"Failed to create wrapper for extension '{extension.DisplayName}' classId '{classId}': {ex.Message}");
}
var extensionWrapper = CreateExtensionWrapper(extension, cmdPalProvider, classId);
wrappers.Add(extensionWrapper);
}
return wrappers;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
@@ -18,24 +18,12 @@ public record ProviderSettings
public bool IsEnabled { get; init; } = true;
private ImmutableDictionary<string, FallbackSettings>? _fallbackCommands
public ImmutableDictionary<string, FallbackSettings> FallbackCommands { get; init; }
= ImmutableDictionary<string, FallbackSettings>.Empty;
public ImmutableDictionary<string, FallbackSettings> FallbackCommands
{
get => _fallbackCommands ?? ImmutableDictionary<string, FallbackSettings>.Empty;
init => _fallbackCommands = value;
}
private ImmutableList<string>? _pinnedCommandIds
public ImmutableList<string> PinnedCommandIds { get; init; }
= ImmutableList<string>.Empty;
public ImmutableList<string> PinnedCommandIds
{
get => _pinnedCommandIds ?? ImmutableList<string>.Empty;
init => _pinnedCommandIds = value;
}
[JsonIgnore]
public string ProviderId { get; init; } = string.Empty;
@@ -49,6 +37,7 @@ public record ProviderSettings
{
}
[JsonConstructor]
public ProviderSettings(bool isEnabled)
{
IsEnabled = isEnabled;

View File

@@ -1,20 +1,16 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.UI.ViewModels;
public record RecentCommandsManager : IRecentCommandsManager
{
private ImmutableList<HistoryItem>? _history = ImmutableList<HistoryItem>.Empty;
internal ImmutableList<HistoryItem> History
{
get => _history ?? ImmutableList<HistoryItem>.Empty;
init => _history = value;
}
[JsonInclude]
internal ImmutableList<HistoryItem> History { get; init; } = ImmutableList<HistoryItem>.Empty;
public RecentCommandsManager()
{

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System.Collections.Immutable;
using System.Text.Json;
using System.Text.Json.Serialization;
using Windows.UI;
@@ -19,7 +18,9 @@ public record DockSettings
{
public DockSide Side { get; init; } = DockSide.Top;
public DockSize DockSize { get; init; } = DockSize.Default;
public DockSize DockSize { get; init; } = DockSize.Small;
public DockSize DockIconsSize { get; init; } = DockSize.Small;
public bool AlwaysOnTop { get; set; } = true;
@@ -45,7 +46,7 @@ public record DockSettings
public string? BackgroundImagePath { get; init; }
// </Theme settings>
private ImmutableList<DockBandSettings>? _startBands = ImmutableList.Create(
public ImmutableList<DockBandSettings> StartBands { get; init; } = ImmutableList.Create(
new DockBandSettings
{
ProviderId = "com.microsoft.cmdpal.builtin.core",
@@ -55,24 +56,12 @@ public record DockSettings
{
ProviderId = "WinGet",
CommandId = "com.microsoft.cmdpal.winget",
ShowTitles = false,
ShowLabels = false,
});
public ImmutableList<DockBandSettings> StartBands
{
get => _startBands ?? ImmutableList<DockBandSettings>.Empty;
init => _startBands = value;
}
public ImmutableList<DockBandSettings> CenterBands { get; init; } = ImmutableList<DockBandSettings>.Empty;
private ImmutableList<DockBandSettings>? _centerBands = ImmutableList<DockBandSettings>.Empty;
public ImmutableList<DockBandSettings> CenterBands
{
get => _centerBands ?? ImmutableList<DockBandSettings>.Empty;
init => _centerBands = value;
}
private ImmutableList<DockBandSettings>? _endBands = ImmutableList.Create(
public ImmutableList<DockBandSettings> EndBands { get; init; } = ImmutableList.Create(
new DockBandSettings
{
ProviderId = "PerformanceMonitor",
@@ -84,12 +73,6 @@ public record DockSettings
CommandId = "com.microsoft.cmdpal.timedate.dockBand",
});
public ImmutableList<DockBandSettings> EndBands
{
get => _endBands ?? ImmutableList<DockBandSettings>.Empty;
init => _endBands = value;
}
public bool ShowLabels { get; init; } = true;
[JsonIgnore]
@@ -121,6 +104,16 @@ public record DockBandSettings
/// </summary>
public bool? ShowSubtitles { get; init; }
/// <summary>
/// Gets a value for backward compatibility. Maps to ShowTitles.
/// </summary>
[System.Text.Json.Serialization.JsonIgnore]
public bool? ShowLabels
{
get => ShowTitles;
init => ShowTitles = value;
}
/// <summary>
/// Resolves the effective value of <see cref="ShowTitles"/> for this band.
/// If this band doesn't have a specific value set, we'll fall back to the
@@ -144,50 +137,11 @@ public enum DockSide
Bottom = 3,
}
[JsonConverter(typeof(DockSizeJsonConverter))]
public enum DockSize
{
Default,
Compact,
}
/// <summary>
/// Custom converter for <see cref="DockSize"/> that preserves backward
/// compatibility with previously-persisted values. Earlier builds shipped a
/// <c>Small</c>/<c>Medium</c>/<c>Large</c> enum; those values are migrated to
/// <see cref="DockSize.Default"/> so existing settings.json files continue to
/// load instead of failing the entire deserialization.
/// </summary>
internal sealed class DockSizeJsonConverter : JsonConverter<DockSize>
{
public override DockSize Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var value = reader.GetString();
if (Enum.TryParse<DockSize>(value, ignoreCase: true, out var parsed))
{
return parsed;
}
// Legacy values from the original Small/Medium/Large enum, or any
// other unknown string — fall back to Default so the user's
// settings file remains loadable after upgrading.
return DockSize.Default;
}
if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt32(out var number))
{
return Enum.IsDefined(typeof(DockSize), number) ? (DockSize)number : DockSize.Default;
}
return DockSize.Default;
}
public override void Write(Utf8JsonWriter writer, DockSize value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
Small,
Medium,
Large,
}
public enum DockBackdrop

View File

@@ -55,14 +55,8 @@ public record HotkeySettings// : ICmdLineRepresentable
// This is currently needed for FancyZones, we need to unify these two objects
// see src\common\settings_objects.h
private string? _key = string.Empty;
[JsonPropertyName("key")]
public string Key
{
get => _key ?? string.Empty;
init => _key = value;
}
public string Key { get; init; } = string.Empty;
public override string ToString()
{

View File

@@ -39,41 +39,17 @@ public record SettingsModel
public bool AllowExternalReload { get; init; }
private ImmutableDictionary<string, ProviderSettings>? _providerSettings
public ImmutableDictionary<string, ProviderSettings> ProviderSettings { get; init; }
= ImmutableDictionary<string, ProviderSettings>.Empty;
public ImmutableDictionary<string, ProviderSettings> ProviderSettings
{
get => _providerSettings ?? ImmutableDictionary<string, ProviderSettings>.Empty;
init => _providerSettings = value;
}
public string[] FallbackRanks { get; init; } = [];
private string[]? _fallbackRanks = [];
public string[] FallbackRanks
{
get => _fallbackRanks ?? [];
init => _fallbackRanks = value;
}
private ImmutableDictionary<string, CommandAlias>? _aliases
public ImmutableDictionary<string, CommandAlias> Aliases { get; init; }
= ImmutableDictionary<string, CommandAlias>.Empty;
public ImmutableDictionary<string, CommandAlias> Aliases
{
get => _aliases ?? ImmutableDictionary<string, CommandAlias>.Empty;
init => _aliases = value;
}
private ImmutableList<TopLevelHotkey>? _commandHotkeys
public ImmutableList<TopLevelHotkey> CommandHotkeys { get; init; }
= ImmutableList<TopLevelHotkey>.Empty;
public ImmutableList<TopLevelHotkey> CommandHotkeys
{
get => _commandHotkeys ?? ImmutableList<TopLevelHotkey>.Empty;
init => _commandHotkeys = value;
}
public MonitorBehavior SummonOn { get; init; } = MonitorBehavior.ToMouse;
public bool DisableAnimations { get; init; } = true;
@@ -86,13 +62,7 @@ public record SettingsModel
public bool EnableDock { get; init; }
private DockSettings? _dockSettings = new();
public DockSettings DockSettings
{
get => _dockSettings ?? new();
init => _dockSettings = value;
}
public DockSettings DockSettings { get; init; } = new();
// Theme settings
public UserTheme Theme { get; init; } = UserTheme.Default;

View File

@@ -196,7 +196,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
{
ProviderId = this.CommandProviderId,
CommandId = this.Id,
ShowTitles = true,
ShowLabels = true,
};
}
@@ -225,7 +225,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
IsFallback = topLevelType == TopLevelType.Fallback;
IsDockBand = topLevelType == TopLevelType.DockBand;
ExtensionHost = extensionHost;
if (IsFallback && commandItem is IFallbackCommandItem2 fallback)
if (IsFallback && commandItem is FallbackCommandItem fallback)
{
_fallbackId = fallback.Id;
}

View File

@@ -4,7 +4,7 @@
using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI;
using Microsoft.CmdPal.Common.Messages;
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Commands;

View File

@@ -17,9 +17,12 @@
<UserControl.Resources>
<ResourceDictionary>
<StackLayout x:Key="ItemsOrientationLayout" Orientation="{x:Bind ItemsOrientation, Mode=OneWay}" />
<StackLayout
x:Key="ItemsOrientationLayout"
Orientation="{x:Bind ItemsOrientation, Mode=OneWay}"
Spacing="4" />
<ItemsPanelTemplate x:Key="HorizontalItemsPanel">
<StackPanel Orientation="Horizontal" />
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="VerticalItemsPanel">
<StackPanel Orientation="Vertical" Spacing="4" />
@@ -73,7 +76,7 @@
<Style x:Key="DockBandListViewItemStyle" TargetType="ListViewItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="Margin" Value="0,0,4,0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
@@ -206,13 +209,13 @@
<Grid
x:Name="RootGrid"
Background="Transparent"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
RightTapped="RootGrid_RightTapped">
<!-- Dock content with Start / Center / End sections -->
<local:DockContentControl
x:Name="ContentGrid"
Margin="4"
Background="Transparent"
IsEditMode="{x:Bind IsEditMode, Mode=OneWay}"
RightTapped="RootGrid_RightTapped">
<local:DockContentControl.StartSource>
@@ -244,6 +247,7 @@
<FontIcon FontSize="12" Glyph="&#xE710;" />
</Button>
</local:DockContentControl.StartActionButton>
<local:DockContentControl.CenterSource>
<ListView
x:Name="CenterListView"
@@ -278,8 +282,6 @@
<ListView
x:Name="EndListView"
MinWidth="48"
MinHeight="0"
HorizontalContentAlignment="Stretch"
DragEnter="BandListView_DragEnter"
DragItemsCompleted="BandListView_DragItemsCompleted"
DragItemsStarting="BandListView_DragItemsStarting"
@@ -309,6 +311,7 @@
</Button>
</local:DockContentControl.EndActionButton>
</local:DockContentControl>
<TeachingTip
x:Name="EditButtonsTeachingTip"
MinWidth="0"
@@ -341,7 +344,7 @@
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Top" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ContentGrid.Margin" Value="4,0,4,0" />
<Setter Target="ContentGrid.Margin" Value="4,0,4,4" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DockOnBottom">
@@ -388,25 +391,6 @@
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<!--
Compact overrides: zeroes margins/borders set by DockOrientation.
Declared after DockOrientation so its setters win when both groups
target the same property.
-->
<VisualStateGroup x:Name="DockSizeStates">
<VisualState x:Name="DefaultSize" />
<VisualState x:Name="CompactSize">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSize, Mode=OneWay}" To="Compact" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ContentGrid.Margin" Value="0" />
<Setter Target="ContentGrid.Padding" Value="0" />
<Setter Target="RootGrid.BorderThickness" Value="0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -47,15 +47,6 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
set => SetValue(DockSideProperty, value);
}
public static readonly DependencyProperty DockSizeProperty =
DependencyProperty.Register(nameof(DockSize), typeof(DockSize), typeof(DockControl), new PropertyMetadata(DockSize.Default));
public DockSize DockSize
{
get => (DockSize)GetValue(DockSizeProperty);
set => SetValue(DockSizeProperty, value);
}
public static readonly DependencyProperty IsEditModeProperty =
DependencyProperty.Register(nameof(IsEditMode), typeof(bool), typeof(DockControl), new PropertyMetadata(false, OnIsEditModeChanged));
@@ -243,10 +234,7 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
{
DockSide = settings.Side;
// Compact mode is only supported for Top/Bottom positions
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
var effectiveSize = isHorizontal ? settings.DockSize : DockSize.Default;
DockSize = effectiveSize;
ItemsOrientation = isHorizontal ? Orientation.Horizontal : Orientation.Vertical;
@@ -302,11 +290,6 @@ public sealed partial class DockControl : UserControl, IRecipient<CloseContextMe
ShowTitlesMenuItem.IsChecked = _editModeContextBand.ShowTitles;
ShowSubtitlesMenuItem.IsChecked = _editModeContextBand.ShowSubtitles;
// Hide subtitle toggle in compact mode — no subtitle in the template
ShowSubtitlesMenuItem.Visibility = DockSize == DockSize.Compact
? Visibility.Collapsed
: Visibility.Visible;
PreparePopupForShow(EditModeContextMenu, dockItem);
EditModeContextMenu.ShowAt(
dockItem,

View File

@@ -43,7 +43,7 @@
<CornerRadius x:Key="DockItemCornerRadius">4</CornerRadius>
<Thickness x:Key="DockItemPadding">4,0,4,0</Thickness>
<Thickness x:Key="DockItemMargin">2,0,2,0</Thickness>
<Style BasedOn="{StaticResource DefaultDockItemControlStyle}" TargetType="local:DockItemControl" />
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
@@ -60,13 +60,12 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DockItemControl">
<Grid
x:Name="PART_RootGrid"
Padding="{StaticResource DockItemMargin}"
Background="Transparent">
<Grid x:Name="PART_HitTestGrid" Background="Transparent">
<Grid
x:Name="PART_BackPlate"
x:Name="PART_RootGrid"
MinWidth="32"
MinHeight="30"
Margin="{TemplateBinding InnerMargin}"
Padding="{TemplateBinding Padding}"
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
@@ -129,20 +128,20 @@
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="PART_BackPlate.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
<Setter Target="PART_BackPlate.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="PART_BackPlate.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
<Setter Target="PART_BackPlate.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="PART_BackPlate.Background" Value="{ThemeResource DockItemBackground}" />
<Setter Target="PART_BackPlate.BorderBrush" Value="{ThemeResource DockItemBackground}" />
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackground}" />
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBackground}" />
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
<Setter Target="TitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
<Setter Target="SubtitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
@@ -193,16 +192,6 @@
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="CompactStates">
<VisualState x:Name="DefaultLayout" />
<VisualState x:Name="Compact">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Padding" Value="0" />
<Setter Target="SubtitleText.Visibility" Value="Collapsed" />
<Setter Target="TitleText.Margin" Value="0,-1,0,0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>

View File

@@ -84,35 +84,12 @@ public sealed partial class DockItemControl : Control
set => SetValue(TextVisibilityProperty, value);
}
public static readonly DependencyProperty IsCompactProperty =
DependencyProperty.Register(nameof(IsCompact), typeof(bool), typeof(DockItemControl), new PropertyMetadata(false, OnIsCompactPropertyChanged));
public bool IsCompact
{
get => (bool)GetValue(IsCompactProperty);
set => SetValue(IsCompactProperty, value);
}
private static void OnIsCompactPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DockItemControl control)
{
control.UpdateCompactState();
}
}
private void UpdateCompactState()
{
VisualStateManager.GoToState(this, IsCompact ? "Compact" : "DefaultLayout", true);
}
private const string IconPresenterName = "IconPresenter";
private FrameworkElement? _iconPresenter;
private DockControl? _parentDock;
private ToolTip? _toolTip;
private long _dockSideCallbackToken = -1;
private long _dockSizeCallbackToken = -1;
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
@@ -145,14 +122,6 @@ public sealed partial class DockItemControl : Control
private void UpdateTextVisibilityState()
{
// When TextVisibility is Collapsed, always hide text and collapse the
// grid column/spacing so the icon-only layout doesn't waste space.
if (TextVisibility == Visibility.Collapsed)
{
VisualStateManager.GoToState(this, "TextHidden", true);
return;
}
// Determine which visual state to use based on title/subtitle presence
var stateName = (HasTitle, HasSubtitle) switch
{
@@ -215,7 +184,6 @@ public sealed partial class DockItemControl : Control
UpdateIconVisibility();
UpdateToolTip();
UpdateAlignment();
UpdateCompactState();
}
private void UpdateToolTip()
@@ -281,14 +249,10 @@ public sealed partial class DockItemControl : Control
{
_parentDock = dock;
UpdateInnerMarginForDockSide(dock.DockSide);
UpdateCompactFromParent(dock);
UpdateAllVisibility();
_dockSideCallbackToken = dock.RegisterPropertyChangedCallback(
DockControl.DockSideProperty,
OnParentDockSideChanged);
_dockSizeCallbackToken = dock.RegisterPropertyChangedCallback(
DockControl.DockSizeProperty,
OnParentDockSizeChanged);
}
UpdateToolTip();
@@ -302,24 +266,12 @@ public sealed partial class DockItemControl : Control
private void DockItemControl_Unloaded(object sender, RoutedEventArgs e)
{
if (_parentDock is not null)
if (_parentDock is not null && _dockSideCallbackToken >= 0)
{
if (_dockSideCallbackToken >= 0)
{
_parentDock.UnregisterPropertyChangedCallback(
DockControl.DockSideProperty,
_dockSideCallbackToken);
_dockSideCallbackToken = -1;
}
if (_dockSizeCallbackToken >= 0)
{
_parentDock.UnregisterPropertyChangedCallback(
DockControl.DockSizeProperty,
_dockSizeCallbackToken);
_dockSizeCallbackToken = -1;
}
_parentDock.UnregisterPropertyChangedCallback(
DockControl.DockSideProperty,
_dockSideCallbackToken);
_dockSideCallbackToken = -1;
_parentDock = null;
}
@@ -331,23 +283,11 @@ public sealed partial class DockItemControl : Control
{
if (sender is DockControl dock)
{
UpdateInnerMarginForDockSide(dock.DockSide);
UpdateAlignment();
}
}
private void OnParentDockSizeChanged(DependencyObject sender, DependencyProperty dp)
{
if (sender is DockControl dock)
{
UpdateCompactFromParent(dock);
}
}
private void UpdateCompactFromParent(DockControl dock)
{
IsCompact = dock.DockSize == DockSize.Compact;
}
private void UpdateInnerMarginForDockSide(DockSide side)
{
// Push the visual (PART_RootGrid) inward on the screen-edge side so
@@ -356,7 +296,7 @@ public sealed partial class DockItemControl : Control
// DockControl's ContentGrid on the screen-edge side.
InnerMargin = side switch
{
DockSide.Top => new Thickness(0, 0, 0, 0),
DockSide.Top => new Thickness(0, 4, 0, 0),
DockSide.Bottom => new Thickness(0, 0, 0, 4),
DockSide.Left => new Thickness(8, 0, 0, 0),
DockSide.Right => new Thickness(0, 0, 8, 0),

View File

@@ -13,8 +13,9 @@ internal static class DockSettingsToViews
{
return size switch
{
DockSize.Default => 86,
DockSize.Compact => 86,
DockSize.Small => 128,
DockSize.Medium => 192,
DockSize.Large => 256,
_ => throw new NotImplementedException(),
};
}
@@ -23,8 +24,9 @@ internal static class DockSettingsToViews
{
return size switch
{
DockSize.Default => 38,
DockSize.Compact => 24,
DockSize.Small => 38,
DockSize.Medium => 54,
DockSize.Large => 76,
_ => throw new NotImplementedException(),
};
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
x:Class="Microsoft.CmdPal.UI.Dock.DockWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

View File

@@ -77,7 +77,7 @@ public sealed partial class DockWindow : WindowEx,
_settingsService = serviceProvider.GetRequiredService<ISettingsService>();
_settingsService.SettingsChanged += SettingsChangedHandler;
_settings = mainSettings.DockSettings;
_lastSize = EffectiveDockSize(_settings);
_lastSize = _settings.DockSize;
viewModel = serviceProvider.GetService<DockViewModel>()!;
_themeService = serviceProvider.GetRequiredService<IThemeService>();
@@ -97,10 +97,6 @@ public sealed partial class DockWindow : WindowEx,
overlappedPresenter.IsResizable = false;
}
// immediately when we're created: make sure to remove our window frame
// and shadow. We don't _always_ get an Activated when we're first
// created.
UpdateWindowFrame();
this.Activated += DockWindow_Activated;
WeakReferenceMessenger.Default.Register<BringToTopMessage>(this);
@@ -148,12 +144,6 @@ public sealed partial class DockWindow : WindowEx,
}
private void DockWindow_Activated(object sender, WindowActivatedEventArgs args)
{
UpdateWindowFrame();
UpdateTopmostState();
}
private void UpdateWindowFrame()
{
// These are used for removing the very subtle shadow/border that we get from Windows 11
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
@@ -162,6 +152,8 @@ public sealed partial class DockWindow : WindowEx,
BOOL value = false;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL));
}
UpdateTopmostState();
}
private HWND GetWindowHandle(Window window)
@@ -182,7 +174,7 @@ public sealed partial class DockWindow : WindowEx,
if (_appBarData.hWnd != IntPtr.Zero)
{
var sameEdge = _appBarData.uEdge == side;
var sameSize = _lastSize == EffectiveDockSize(_settings);
var sameSize = _lastSize == _settings.DockSize;
if (sameEdge && sameSize)
{
UpdateTopmostState();
@@ -340,7 +332,7 @@ public sealed partial class DockWindow : WindowEx,
// Stash the last size we created the bar at, so we know when to hot-
// reload it
_lastSize = EffectiveDockSize(_settings);
_lastSize = _settings.DockSize;
UpdateWindowPosition();
}
@@ -392,9 +384,15 @@ public sealed partial class DockWindow : WindowEx,
var dpi = PInvoke.GetDpiForWindow(_hwnd);
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
// Get system border metrics
var borderWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXBORDER);
var edgeWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXEDGE);
var frameWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXFRAME);
var scaleFactor = dpi / 96.0;
var effectiveSize = EffectiveDockSize(_settings);
UpdateAppBarDataForEdge(_settings.Side, effectiveSize, scaleFactor);
UpdateAppBarDataForEdge(_settings.Side, _settings.DockSize, scaleFactor);
// Query and set position
PInvoke.SHAppBarMessage(PInvoke.ABM_QUERYPOS, ref _appBarData);
@@ -408,16 +406,16 @@ public sealed partial class DockWindow : WindowEx,
switch (_settings.Side)
{
case DockSide.Top:
_appBarData.rc.bottom = _appBarData.rc.top + (int)(DockSettingsToViews.HeightForSize(effectiveSize) * scaleFactor);
_appBarData.rc.bottom = _appBarData.rc.top + (int)(DockSettingsToViews.HeightForSize(_settings.DockSize) * scaleFactor);
break;
case DockSide.Bottom:
_appBarData.rc.top = _appBarData.rc.bottom - (int)(DockSettingsToViews.HeightForSize(effectiveSize) * scaleFactor);
_appBarData.rc.top = _appBarData.rc.bottom - (int)(DockSettingsToViews.HeightForSize(_settings.DockSize) * scaleFactor);
break;
case DockSide.Left:
_appBarData.rc.right = _appBarData.rc.left + (int)(DockSettingsToViews.WidthForSize(effectiveSize) * scaleFactor);
_appBarData.rc.right = _appBarData.rc.left + (int)(DockSettingsToViews.WidthForSize(_settings.DockSize) * scaleFactor);
break;
case DockSide.Right:
_appBarData.rc.left = _appBarData.rc.right - (int)(DockSettingsToViews.WidthForSize(effectiveSize) * scaleFactor);
_appBarData.rc.left = _appBarData.rc.right - (int)(DockSettingsToViews.WidthForSize(_settings.DockSize) * scaleFactor);
break;
}
@@ -430,28 +428,23 @@ public sealed partial class DockWindow : WindowEx,
// PInvoke.SHAppBarMessage(ABM_SETSTATE, ref _appBarData);
// PInvoke.SHAppBarMessage(PInvoke.ABM_SETAUTOHIDEBAR, ref _appBarData);
// The dock window is borderless (SetBorderAndTitleBar(false, false),
// IsResizable = false) so no frame compensation is needed — the
// app bar rect matches the window rect exactly.
// Account for system borders when moving the window
// Adjust position to account for window frame/border
var adjustedLeft = _appBarData.rc.left - frameWidth;
var adjustedTop = _appBarData.rc.top - frameWidth;
var adjustedWidth = (_appBarData.rc.right - _appBarData.rc.left) + (2 * frameWidth);
var adjustedHeight = (_appBarData.rc.bottom - _appBarData.rc.top) + (2 * frameWidth);
// Move the actual window
PInvoke.MoveWindow(
_hwnd,
_appBarData.rc.left,
_appBarData.rc.top,
_appBarData.rc.right - _appBarData.rc.left,
_appBarData.rc.bottom - _appBarData.rc.top,
adjustedLeft,
adjustedTop,
adjustedWidth,
adjustedHeight,
true);
}
/// <summary>
/// Compact mode is only supported for Top/Bottom dock positions.
/// For Left/Right, always use Default size.
/// </summary>
private static DockSize EffectiveDockSize(DockSettings settings)
{
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
return isHorizontal ? settings.DockSize : DockSize.Default;
}
private void UpdateAppBarDataForEdge(DockSide side, DockSize size, double scaleFactor)
{
Logger.LogDebug("UpdateAppBarDataForEdge");
@@ -594,21 +587,11 @@ public sealed partial class DockWindow : WindowEx,
}
}
// Handle WM_GETMINMAXINFO to allow the dock to be smaller than
// the default minimum window size (SM_CYMINTRACK ~36px).
// Handle WM_GETMINMAXINFO to control window size limits
else if (msg == PInvoke.WM_GETMINMAXINFO)
{
// Call the original WndProc first so it fills default values,
// then override the minimum tracking size.
var result = PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
unsafe
{
var minMaxInfo = (MINMAXINFO*)lParam.Value;
minMaxInfo->ptMinTrackSize.X = 1;
minMaxInfo->ptMinTrackSize.Y = 1;
}
return result;
// We can modify the min/max tracking info here if needed
// For now, let it pass through but we could restrict max size
}
// Handle the AppBarMessage message

View File

@@ -43,8 +43,6 @@ public sealed partial class ListPage : Page,
private ListItemViewModel? _stickySelectedItem;
private ListItemViewModel? _lastPushedToVm;
private long _pendingContextMenuOpenRequestId;
private Action? _cancelPendingContextMenuOpen;
// A single search-text change can produce multiple ItemsUpdated calls
// dispatched as separate UI-thread callbacks. A later "soft" update
@@ -126,8 +124,6 @@ public sealed partial class ListPage : Page,
{
base.OnNavigatingFrom(e);
CancelPendingContextMenuOpen();
WeakReferenceMessenger.Default.Unregister<NavigateNextCommand>(this);
WeakReferenceMessenger.Default.Unregister<NavigatePreviousCommand>(this);
WeakReferenceMessenger.Default.Unregister<NavigateLeftCommand>(this);
@@ -287,7 +283,17 @@ public sealed partial class ListPage : Page,
ViewModel?.UpdateSelectedItemCommand.Execute(item);
var pos = e.GetPosition(element);
RequestContextMenuOpen(item, element, pos);
_ = DispatcherQueue.TryEnqueue(
() =>
{
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
new OpenContextMenuMessage(
element,
FlyoutPlacementMode.BottomEdgeAlignedLeft,
pos,
ContextMenuFilterLocation.Top));
});
}
}
@@ -1008,14 +1014,21 @@ public sealed partial class ListPage : Page,
pos = new(0, element.ActualHeight);
}
ViewModel?.UpdateSelectedItemCommand.Execute(item);
RequestContextMenuOpen(item, element, pos);
_ = DispatcherQueue.TryEnqueue(
() =>
{
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
new OpenContextMenuMessage(
element,
FlyoutPlacementMode.BottomEdgeAlignedLeft,
pos,
ContextMenuFilterLocation.Top));
});
e.Handled = true;
}
private void Items_OnContextCanceled(UIElement sender, RoutedEventArgs e)
{
CancelPendingContextMenuOpen();
_ = DispatcherQueue.TryEnqueue(() => WeakReferenceMessenger.Default.Send<CloseContextMenuMessage>());
}
@@ -1197,87 +1210,6 @@ public sealed partial class ListPage : Page,
scroll.ChangeView(horizontalOffset: null, verticalOffset: 0, zoomFactor: null, disableAnimation: true);
}
private void RequestContextMenuOpen(ListItemViewModel item, FrameworkElement element, Point pos)
{
// BEAR LOADING: Right-click can arrive before the selected item's slow
// context-menu hydration completes, especially for out-of-proc
// providers. Keep this exact open request alive until the same
// selected item becomes context-openable instead of dropping the first
// click.
CancelPendingContextMenuOpen();
var requestId = Interlocked.Increment(ref _pendingContextMenuOpenRequestId);
System.ComponentModel.PropertyChangedEventHandler? onItemChanged = null;
Action? detach = null;
detach = () =>
{
if (onItemChanged is not null)
{
item.PropertyChanged -= onItemChanged;
}
if (ReferenceEquals(_cancelPendingContextMenuOpen, detach))
{
_cancelPendingContextMenuOpen = null;
}
};
onItemChanged = (_, args) =>
{
if (args.PropertyName is nameof(ListItemViewModel.CanOpenContextMenu) or nameof(ListItemViewModel.AllCommands) &&
TryOpenContextMenuIfReady(item, element, pos, requestId))
{
detach();
}
};
item.PropertyChanged += onItemChanged;
_cancelPendingContextMenuOpen = detach;
if (TryOpenContextMenuIfReady(item, element, pos, requestId))
{
detach();
}
}
private bool TryOpenContextMenuIfReady(ListItemViewModel item, FrameworkElement element, Point pos, long requestId)
{
// Ignore stale requests so rapid selection changes or cancelled opens
// can't resurrect an old context menu on the wrong item.
if (requestId != Volatile.Read(ref _pendingContextMenuOpenRequestId) ||
!ReferenceEquals(ItemView.SelectedItem, item) ||
!item.CanOpenContextMenu)
{
return false;
}
_ = DispatcherQueue.TryEnqueue(
() =>
{
if (requestId != Volatile.Read(ref _pendingContextMenuOpenRequestId) ||
!ReferenceEquals(ItemView.SelectedItem, item))
{
return;
}
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
new OpenContextMenuMessage(
element,
FlyoutPlacementMode.BottomEdgeAlignedLeft,
pos,
ContextMenuFilterLocation.Top));
});
return true;
}
private void CancelPendingContextMenuOpen()
{
Interlocked.Increment(ref _pendingContextMenuOpenRequestId);
_cancelPendingContextMenuOpen?.Invoke();
_cancelPendingContextMenuOpen = null;
}
private IDisposable SuppressSelectionChangedScope()
{
_suppressSelectionChanged = true;

View File

@@ -8,8 +8,8 @@ using CmdPalKeyboardService;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Messages;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events;

View File

@@ -38,11 +38,6 @@
<DefineConstants>$(DefineConstants)</DefineConstants>
</PropertyGroup>
<!-- Added to ensure telemetry events are triggered from AOT build -->
<PropertyGroup>
<EventSourceSupport>true</EventSourceSupport>
</PropertyGroup>
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
<!-- <PropertyGroup>
<EnableCmdPalAOT>true</EnableCmdPalAOT>

View File

@@ -94,7 +94,6 @@ WM_WINDOWPOSCHANGING
WM_SHOWWINDOW
WM_SIZE
WM_GETMINMAXINFO
MINMAXINFO
SetWinEventHook
WINDOW_STYLE
SC_MINIMIZE

View File

@@ -67,20 +67,6 @@
</ComboBox>
</controls:SettingsCard>
<!-- Dock Size (only for Top/Bottom positions) -->
<controls:SettingsCard
x:Name="DockSizeSettingsCard"
x:Uid="DockAppearance_DockSize_SettingsCard"
HeaderIcon="{ui:FontIcon Glyph=&#xE799;}">
<ComboBox
x:Name="DockSizeComboBox"
MinWidth="{StaticResource SettingActionControlMinWidth}"
SelectedIndex="{x:Bind SelectedDockSizeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="DockAppearance_DockSize_Default" />
<ComboBoxItem x:Uid="DockAppearance_DockSize_Compact" />
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard x:Uid="DockAppearance_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE793;}">
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">

View File

@@ -42,9 +42,7 @@ public sealed partial class DockSettingsPage : Page
{
// Initialize UI controls to match current settings
DockPositionComboBox.SelectedIndex = SelectedSideIndex;
DockSizeComboBox.SelectedIndex = SelectedDockSizeIndex;
BackdropComboBox.SelectedIndex = SelectedBackdropIndex;
UpdateDockSizeCardVisibility();
}
private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)
@@ -110,11 +108,7 @@ public sealed partial class DockSettingsPage : Page
public int SelectedSideIndex
{
get => SideToSelectedIndex(ViewModel.Dock_Side);
set
{
ViewModel.Dock_Side = SelectedIndexToSide(value);
UpdateDockSizeCardVisibility();
}
set => ViewModel.Dock_Side = SelectedIndexToSide(value);
}
public int SelectedBackdropIndex
@@ -132,16 +126,18 @@ public sealed partial class DockSettingsPage : Page
// Conversion methods for ComboBox bindings
private static int DockSizeToSelectedIndex(DockSize size) => size switch
{
DockSize.Default => 0,
DockSize.Compact => 1,
DockSize.Small => 0,
DockSize.Medium => 1,
DockSize.Large => 2,
_ => 0,
};
private static DockSize SelectedIndexToDockSize(int index) => index switch
{
0 => DockSize.Default,
1 => DockSize.Compact,
_ => DockSize.Default,
0 => DockSize.Small,
1 => DockSize.Medium,
2 => DockSize.Large,
_ => DockSize.Small,
};
private static int SideToSelectedIndex(DockSide side) => side switch
@@ -176,13 +172,6 @@ public sealed partial class DockSettingsPage : Page
_ => DockBackdrop.Acrylic,
};
private void UpdateDockSizeCardVisibility()
{
var side = ViewModel.Dock_Side;
var isTopOrBottom = side == DockSide.Top || side == DockSide.Bottom;
DockSizeSettingsCard.Visibility = isTopOrBottom ? Visibility.Visible : Visibility.Collapsed;
}
private List<TopLevelViewModel> GetAllBands()
{
var allBands = new List<TopLevelViewModel>();

View File

@@ -939,18 +939,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="DockAppearance_DockPosition_Bottom.Content" xml:space="preserve">
<value>Bottom</value>
</data>
<data name="DockAppearance_DockSize_SettingsCard.Header" xml:space="preserve">
<value>Size</value>
</data>
<data name="DockAppearance_DockSize_SettingsCard.Description" xml:space="preserve">
<value>Choose the dock size; subtitles of dock items are hidden in compact mode</value>
</data>
<data name="DockAppearance_DockSize_Default.Content" xml:space="preserve">
<value>Default</value>
</data>
<data name="DockAppearance_DockSize_Compact.Content" xml:space="preserve">
<value>Compact</value>
</data>
<data name="top_level_pin_command_name" xml:space="preserve">
<value>Pin to home</value>
<comment>Command name for pinning an item to the top level list of commands</comment>

View File

@@ -5,7 +5,6 @@
using System.Globalization;
using System.Linq;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Foundation;
@@ -19,49 +18,47 @@ public class CloseOnEnterTests
public void PrimaryIsCopy_WhenCloseOnEnterTrue()
{
var settings = new Settings(closeOnEnter: true);
TypedEventHandler<object, object> handleSave = (s, e) => { };
TypedEventHandler<object, object> handleReplace = (s, e) => { };
var item = ResultHelper.CreateResultForPage(
var item = ResultHelper.CreateResult(
4m,
CultureInfo.CurrentCulture,
CultureInfo.CurrentCulture,
"2+2",
settings,
handleSave,
handleReplace);
Assert.IsNotNull(item);
Assert.IsInstanceOfType(item.Command, typeof(CopyTextCommand));
Assert.IsTrue(item.MoreCommands.OfType<CommandContextItem>().All(command => command.Command is not SaveCommand));
var result = ((CopyTextCommand)item.Command).Result;
Assert.AreEqual(CommandResultKind.ShowToast, result.Kind);
var toastArgs = result.Args as ToastArgs;
Assert.IsNotNull(toastArgs);
Assert.AreEqual(CommandResultKind.Hide, ((CommandResult)toastArgs.Result).Kind);
var firstMore = item.MoreCommands.First();
Assert.IsInstanceOfType(firstMore, typeof(CommandContextItem));
Assert.IsInstanceOfType(((CommandItem)firstMore).Command, typeof(SaveCommand));
}
[TestMethod]
public void PrimaryIsCopy_WhenCloseOnEnterFalse()
public void PrimaryIsSave_WhenCloseOnEnterFalse()
{
var settings = new Settings(closeOnEnter: false);
TypedEventHandler<object, object> handleSave = (s, e) => { };
TypedEventHandler<object, object> handleReplace = (s, e) => { };
var item = ResultHelper.CreateResultForPage(
var item = ResultHelper.CreateResult(
4m,
CultureInfo.CurrentCulture,
CultureInfo.CurrentCulture,
"2+2",
settings,
handleSave,
handleReplace);
Assert.IsNotNull(item);
Assert.IsInstanceOfType(item.Command, typeof(CopyTextCommand));
Assert.IsTrue(item.MoreCommands.OfType<CommandContextItem>().All(command => command.Command is not SaveCommand));
Assert.IsInstanceOfType(item.Command, typeof(SaveCommand));
var result = ((CopyTextCommand)item.Command).Result;
Assert.AreEqual(CommandResultKind.ShowToast, result.Kind);
var toastArgs = result.Args as ToastArgs;
Assert.IsNotNull(toastArgs);
Assert.AreEqual(CommandResultKind.KeepOpen, ((CommandResult)toastArgs.Result).Kind);
var firstMore = item.MoreCommands.First();
Assert.IsInstanceOfType(firstMore, typeof(CommandContextItem));
Assert.IsInstanceOfType(((CommandItem)firstMore).Command, typeof(CopyTextCommand));
}
}

View File

@@ -1,98 +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.Globalization;
using System.Linq;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CmdPal.Ext.Calc.Pages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class PrimaryActionTests
{
[TestMethod]
public void PrimaryActionPaste_UsesPasteAsPrimaryAndCopyAsSecondary()
{
var settings = new Settings(primaryAction: PrimaryAction.Paste);
TypedEventHandler<object, object> handleReplace = (_, _) => { };
var item = ResultHelper.CreateResultForPage(
4m,
CultureInfo.CurrentCulture,
CultureInfo.CurrentCulture,
"2+2",
settings,
handleReplace);
Assert.IsNotNull(item);
Assert.IsInstanceOfType(item.Command, typeof(CalculatorPasteCommand));
var firstMore = item.MoreCommands.OfType<CommandContextItem>().FirstOrDefault();
Assert.IsNotNull(firstMore);
Assert.IsInstanceOfType(((CommandItem)firstMore).Command, typeof(CalculatorCopyCommand));
}
[TestMethod]
public void HistoryItemsUsePasteWhenPrimaryActionPaste()
{
var settings = new Settings(primaryAction: PrimaryAction.Paste);
settings.AddHistoryItem(new HistoryItem("2+2", "4", DateTime.UtcNow));
var page = new CalculatorListPage(settings);
var historyItem = page.GetItems().FirstOrDefault(item => item.Title == "4");
Assert.IsNotNull(historyItem);
Assert.IsInstanceOfType(historyItem.Command, typeof(CalculatorPasteCommand));
}
[DataTestMethod]
[DataRow(false)]
[DataRow(true)]
public void FallbackItemsUseCalculatorCommandsForCopyAndPaste(bool saveFallbackResultsToHistory)
{
var settings = new Settings(saveFallbackResultsToHistory: saveFallbackResultsToHistory);
var page = new CalculatorListPage(settings);
var item = new FallbackCalculatorItem(settings, page);
item.UpdateQuery("2+2");
Assert.IsInstanceOfType(item.Command, typeof(CalculatorCopyCommand));
Assert.IsInstanceOfType(GetFallbackSecondaryCommand(item), typeof(CalculatorPasteCommand));
}
[DataTestMethod]
[DataRow(false)]
[DataRow(true)]
public void FallbackItemsRespectPrimaryActionWhenHistorySavingToggles(bool saveFallbackResultsToHistory)
{
var settings = new Settings(
primaryAction: PrimaryAction.Paste,
saveFallbackResultsToHistory: saveFallbackResultsToHistory);
var page = new CalculatorListPage(settings);
var item = new FallbackCalculatorItem(settings, page);
item.UpdateQuery("2+2");
Assert.IsInstanceOfType(item.Command, typeof(CalculatorPasteCommand));
Assert.IsInstanceOfType(GetFallbackSecondaryCommand(item), typeof(CalculatorCopyCommand));
}
private static ICommand GetFallbackSecondaryCommand(FallbackCalculatorItem item)
{
var secondaryCommand = item.MoreCommands
.OfType<CommandContextItem>()
.Skip(1)
.Select(contextItem => ((CommandItem)contextItem).Command)
.FirstOrDefault();
Assert.IsNotNull(secondaryCommand);
return secondaryCommand;
}
}

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